$30 off During Our Annual Pro Sale. View Details »

SwiftSyntaxMacrosに入門してみた

とち🐹
September 26, 2023

 SwiftSyntaxMacrosに入門してみた

とち🐹

September 26, 2023
Tweet

More Decks by とち🐹

Other Decks in Programming

Transcript

  1. 4XJGU4ZOUBY.BDSPTʹ
    ೖ໳ͯ͠Έͨ
    5PTIJZB,PCBZBTIJ
    ͱͪ!UPDIJ@

    View Slide

  2. ϚΫϩʹೖ໳ͨ͠Ϟνϕʔγϣϯ
    w QPJOUGSFFDPTXJGUEFQFOEFODJFTͱ͍͏ϥΠϒϥϦͰ͸ɺQSPUPDPMͰ͸ͳ͘

    TUSVDUΛར༻ͯ͠ΠϯλʔϑΣʔεΛهड़͢Δ͜ͱ͕ਪ঑͞Ε͍ͯΔ
    protocol APIClient {


    func fetchUserName(userId: Int) async throws -> String


    func setUserFlag(userId: Int, flag: Bool) async throws


    }
    struct APIClient {


    var fetchUserName: @Sendable (_ userId: Int) async throws -> String


    var setUserFlag: @Sendable (_ userId: Int, _ flag: Bool) async throws -> Void


    }
    w VCFSNPDLPMPͷΑ͏ͳɺϞοΫͷίʔυΛࣗಈੜ੒͢Δ࢓૊ΈΛ

    TUSVDUελΠϧͷΠϯλʔϑΣʔεͰ΋ར༻ͨ͘͠ͳͬͨ

    View Slide

  3. ࠓճ࡞੒͢Δ!(FOFSBUF.PDLϚΫϩ
    @GenerateMock


    struct APIClient {


    var fetch: @Sendable (Int) async throws -> String


    }
    struct APIClient {


    var fetch: @Sendable (Int) async throws -> String


    static func mock(_ mock: Mock) -> Self {


    Self (fetch: mock.fetch)


    }


    class Mock {


    private(set) var fetchCallCount = 0


    private(set) var fetchArgValues: [(Int)] = []


    var fetchHandler: ((Int) async throws -> String)?


    @Sendable fileprivate func fetch(_ arg0: Int) async throws -> String {


    fetchCallCount += 1


    fetchArgValues.append((arg0))


    return try await fetchHandler!(arg0)


    }


    }


    }
    ϚΫϩΛ෇͚Δͱɺͭͷϝϯόʔ͕TUSVDUʹ௥Ճ͞ΕΔ
    ϞοΫΦϒδΣΫτΛੜ੒͢ΔTUBUJDؔ਺NPDL @

    ֤Ϋϩʔδϟ͕
    ɾݺ͹Εͨճ਺
    ɾݺ͹Εͨ࣌ͷҾ਺
    ɾݺ͹ΕͨΒ࣮ߦ͍ͨ͠ॲཧ
    Λอ࣋͢ΔΫϥε.PDL

    View Slide

  4. public struct GenerateMockMacro: MemberMacro {


    public static func expansion(


    of node: AttributeSyntax,


    providingMembersOf declaration: some DeclGroupSyntax,


    in context: some MacroExpansionContext


    ) throws -> [DeclSyntax] {


    return [DeclSyntax("")]


    }


    }
    w .FNCFS.BDSPͷFYQBOTJPOϝιου಺ʹɺϚΫϩͷ࣮૷Λॻ͍͍ͯ͘
    ϚΫϩΛ৽ن࡞੒͢Δ
    w 9DPEFͰ1BDLBHFΛ৽ن࡞੒ͯ͠4XJGU.BDSPςϯϓϨʔτΛબ୒
    ͜͜Ͱฦͨ͠4ZOUBY͕ɺϚΫϩͰల։͞ΕΔ
    @attached(member, names: named(mock(_:)), named(Mock))


    public macro GenerateMock() = #externalMacro(module: "GenerateMockMacros", type: "GenerateMockMacro")
    w ࠓճ͸TUSVDUʹϝϯόʔΛ௥Ճ͍ͨ͠ͷͰ!BUUBDIFE NFNCFS
    Λઃఆ

    View Slide

  5. ςετΛ༻ҙ͢Δ
    w ςετΛ܁Γฦ࣮͠ߦ͠ͳ͕Βɺগͣͭ͠ϚΫϩΛ૊Έཱ͍ͯͯ͘
    final class GenerateMockTests: XCTestCase {


    func testMacro() throws {


    assertMacroExpansion(#"""


    @GenerateMock


    struct APIClient {


    var fetch: @Sendable (Int) async throws -> String


    }


    """#,


    expandedSource: #"""


    struct APIClient {


    var fetch: @Sendable (Int) async throws -> String


    }


    """#,


    macros: ["GenerateMock": GenerateMockMacro.self]


    )


    }


    }
    ·ͩԿ΋࣮૷͍ͯ͠ͳ͍ͷͰɺ
    TUSVDUΛͦͷ··ग़ྗ͢Ε͹
    ςετ͕௨Δ

    View Slide

  6. ςετΛॻ͖׵͑Δ
    w ςετʹTUBUJDؔ਺Λ௥Ճͯ͠ɺظ଴௨Γͷࠩ෼Ͱࣦഊ͢Δ͜ͱΛ֬ೝ
    assertMacroExpansion(#"""


    @GenerateMock


    struct APIClient {


    var fetch: @Sendable (Int) async throws -> String


    }


    """#,


    expandedSource: #"""


    struct APIClient {


    var fetch: @Sendable (Int) async throws -> String


    static func mock(_ mock: Mock) -> Self {


    Self (fetch: mock.fetch)


    }


    }


    """#,


    macros: testMacros


    )

    View Slide

  7. 4XJGU"45&YQMPSFSͰߏจΛௐ΂Δ
    w IUUQTTXJGUBTUFYQMPSFSDPN
    ͳΔ΄Ͳʜɻ
    ·ͣ͸'VODUJPO%FDM͔Β
    ࢝ΊΕ͹ྑͦ͞͏ͩͳ🧐

    View Slide

  8. 'VODUJPO%FDM4ZOUBYΛߏங͢Δ
    w ͱΓ͋͑ͣ'VODUJPO%FDM4ZOUBYΛฦͯ͠ΈΔ
    return [


    DeclSyntax(


    FunctionDeclSyntax(


    name: <#T##TokenSyntax#>,


    signature: <#T##FunctionSignatureSyntax#>


    )


    )


    ]
    name: TokenSyntax(stringLiteral: “mock"),


    signature: FunctionSignatureSyntax(


    parameterClause: <#T##FunctionParameterClauseSyntax#>


    )
    w OBNFͱTJHOBUVSFͱ͍͏ύϥϝʔλΛٻΊΒΕͨͷͰɺຒΊͯΈΔ
    w TJHOBUVSF͸ɺ͞ΒʹQBSBNFUFS$MBVTFͱ͍͏΋ͷΛٻΊ͖ͯͨ

    View Slide

  9. QBSBNFUFS$MBVTFΛຒΊΔ
    w ίʔυิ׬ʹཔΓͳ͕Βɺ4XJGU"45&YQMPSFSͰௐ΂ͨ಺༰ΛຒΊ͍ͯ͘
    parameterClause: FunctionParameterClauseSyntax(


    parameters: FunctionParameterListSyntax([


    FunctionParameterSyntax(stringLiteral: "_ mock: Mock")


    ])


    )
    ࣍͸ઌ಄ʹTUBUJDΛ෇͚ͯΈΑ͏

    View Slide

  10. TUBUJDΛ෇͚Δ
    w 'VODUJPO%FDM4ZOUBYʹNPEJ
    fi
    FSTΛ௥Ճ͢Δ
    ࣍͸4FMGΛฦͯ͠ΈΑ͏
    FunctionDeclSyntax(


    modifiers: DeclModifierListSyntax([


    DeclModifierSyntax(name: "static")


    ]),


    name: TokenSyntax(stringLiteral: "mock"),


    signature: FunctionSignatureSyntax(…)


    )

    View Slide

  11. 4FMGΛฦ͢
    w 'VODUJPO4JHOBUVSF4ZOUBYʹSFUVSO$MBVTFΛ௥Ճ͢Δ
    ͋ͱ͸ؔ਺ʹίʔυϒϩοΫΛ௥Ճ͢Δ͚ͩʂ
    signature: FunctionSignatureSyntax(


    parameterClause: FunctionParameterClauseSyntax(…),


    returnClause: ReturnClauseSyntax(


    type: IdentifierTypeSyntax(name: "Self")


    )


    )

    View Slide

  12. ͜Μͳײ͡ͷϊϦͰ૊Έཱ͍ͯͯ͘ΜͰ͕͢ʜ
    struct APIClient {


    var fetch: @Sendable (Int) async throws -> String


    static func mock(_ mock: Mock) -> Self {


    Self (fetch: mock.fetch)


    }


    }
    w ϚΫϩͷೖྗΛղੳ͢Δඞཁ͕͋Δ
    w ͜ΕΒͷ໊લʢGFUDIʣ͸ ͔͜͜Βऔ͖͍ͬͯͨ

    View Slide

  13. ϚΫϩೖྗΛղੳ͢Δ
    w EFCVH%FTDSJQUJPOΛQSJOU͢Δ
    StructDeclSyntax


    ├─attributes: AttributeListSyntax


    │ ╰─[0]: AttributeSyntax


    │ ├─atSign: atSign


    │ ╰─attributeName: IdentifierTypeSyntax


    │ ╰─name: identifier("GenerateMock")


    ├─modifiers: DeclModifierListSyntax


    ├─structKeyword: keyword(SwiftSyntax.Keyword.struct)


    ├─name: identifier("APIClient")


    ╰─memberBlock: MemberBlockSyntax


    ├─leftBrace: leftBrace


    ├─members: MemberBlockItemListSyntax


    │ ╰─[0]: MemberBlockItemSyntax


    │ ╰─decl: VariableDeclSyntax


    │ ├─attributes: AttributeListSyntax


    │ ├─modifiers: DeclModifierListSyntax


    │ ├─bindingSpecifier: keyword(SwiftSyntax.Keyword.var)


    │ ╰─bindings: PatternBindingListSyntax


    │ ╰─[0]: PatternBindingSyntax


    │ ├─pattern: IdentifierPatternSyntax


    │ │ ╰─identifier: identifier("fetch")


    │ ╰─typeAnnotation: TypeAnnotationSyntax


    │ ├─colon: colon


    │ ╰─type: AttributedTypeSyntax


    │ ├─attributes: AttributeListSyntax


    │ │ ╰─[0]: AttributeSyntax


    │ │ ├─atSign: atSign


    │ │ ╰─attributeName: IdentifierTypeSyntax


    │ │ ╰─name: identifier("Sendable")


    │ ╰─baseType: FunctionTypeSyntax


    │ ├─leftParen: leftParen


    │ ├─parameters: TupleTypeElementListSyntax


    │ │ ╰─[0]: TupleTypeElementSyntax


    │ │ ╰─type: IdentifierTypeSyntax


    │ │ ╰─name: identifier("Int")


    │ ├─rightParen: rightParen


    │ ├─effectSpecifiers: TypeEffectSpecifiersSyntax


    │ │ ├─asyncSpecifier: keyword(SwiftSyntax.Keyword.async)


    │ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)


    │ ╰─returnClause: ReturnClauseSyntax


    │ ├─arrow: arrow


    │ ╰─type: IdentifierTypeSyntax


    │ ╰─name: identifier("String")


    ╰─rightBrace: rightBrace
    public static func expansion(


    of node: AttributeSyntax,


    providingMembersOf declaration: some DeclGroupSyntax,


    in context: some MacroExpansionContext


    ) throws -> [DeclSyntax] {


    print(declaration.debugDescription)


    return [DeclSyntax(…)]


    }
    w ϚΫϩʹೖྗ͞Εͨίʔυͷߏจ໦͕

    ΞεΩʔΞʔτܗࣜͰग़ྗ͞ΕΔ

    View Slide

  14. Ϋϩʔδϟͷม਺໊Λऔಘ͢Δ
    StructDeclSyntax


    ╰─memberBlock: MemberBlockSyntax


    ├─members: MemberBlockItemListSyntax


    │ ╰─[0]: MemberBlockItemSyntax


    │ ╰─decl: VariableDeclSyntax


    │ ╰─bindings: PatternBindingListSyntax


    │ ╰─[0]: PatternBindingSyntax


    │ ├─pattern: IdentifierPatternSyntax


    │ │ ╰─identifier: identifier("fetch")
    guard let structDecl = declaration.as(StructDeclSyntax.self),


    let variable = structDecl.memberBlock.members.first?.decl.as(VariableDeclSyntax.self),


    let identifierPattern = variable.bindings.first?.pattern.as(IdentifierPatternSyntax.self)


    else { throw CustomError.message("...") }


    print(identifierPattern.identifier) // => "fetch"
    w దٓΩϟετΛར༻͠ͳ͕Βɺ໨౰ͯͷཁૉ·ͰͨͲ͍ͬͯ͘

    View Slide

  15. ϚΫϩ͕׬੒
    w ͜͜·Ͱʹ঺հͨ͠ςΫχοΫΛۦ࢖͠ͳ͕Βʜ
    w ςετΛॻ͖
    w 4XJGU"45&YQMPSFSΛࢀߟʹϚΫϩग़ྗΛ૊Έཱͯ
    w EFCVH%FTDSJQUJPOΛࢀߟʹϚΫϩೖྗΛղੳͯ͠
    w ͱΓ͋͑ͣಈ͖ͦ͏ͳϚΫϩ͕׬੒͠·ͨ͠ʙʂ🎉
    w IUUQTHJUIVCDPNUPDIJ(FOFSBUF.PDL

    View Slide

  16. ࣮ࡍʹ࢖ͬͯΈͨ
    w "1*$MJFOUͱTXJGUEFQFOEFODJFTͰར༻͢ΔͨΊͷ͓·͡ͳ͍Λهड़
    @GenerateMock


    struct APIClient {


    var fetchUserName: @Sendable (_ userId: Int) async throws -> String


    var setUserFlag: @Sendable (_ userId: Int, _ flag: Bool) async throws -> Void


    }


    extension APIClient: DependencyKey {


    static let liveValue = Self(


    fetchUserName: { "Live user for \($0)" },


    setUserFlag: { _, _ in try await Task.sleep(nanoseconds: NSEC_PER_SEC) }


    )


    }


    extension DependencyValues {


    var apiClient: APIClient {


    get { self[APIClient.self] }


    set { self[APIClient.self] = newValue }


    }


    }

    View Slide

  17. ࣮ࡍʹ࢖ͬͯΈͨ
    w "1*$MJFOUΛར༻͢Δ7JFX.PEFMΛهड़
    @MainActor


    final class ViewModel: ObservableObject {


    @Published private(set) var text: String?


    @Published private(set) var isLoading = false


    @Dependency(\.apiClient) private var apiClient


    private let userId: Int


    init(userId: Int) { self.userId = userId }


    func buttonTapped() async {


    text = nil


    isLoading = true; defer { isLoading = false }


    do {


    text = try await apiClient.fetchUserName(userId)


    try await apiClient.setUserFlag(userId, true)


    } catch {


    text = "Error!"


    }


    }


    }


    View Slide

  18. ࣮ࡍʹ࢖ͬͯΈͨ
    w ςετͷTFU6QͰɺϚΫϩʹΑΓੜ੒͞Εͨ"1*$MJFOUͷϞοΫʹࠩ͠ସ͑
    @MainActor


    final class ViewModelTests: XCTestCase {


    var sut: ViewModel!


    var apiClientMock: APIClient.Mock!


    override func setUp() {


    super.setUp()


    apiClientMock = .init()


    sut = withDependencies {


    $0.apiClient = .mock(apiClientMock)


    } operation: {


    ViewModel(userId: 1234)


    }


    }


    ...


    }


    View Slide

  19. ࣮ࡍʹ࢖ͬͯΈͨ
    w ςετ͸͜Μͳײ͡Ͱ

    ॻ͚Δ
    func testButtonTapped_Success() async {


    apiClientMock.fetchUserNameHandler = { "Mock user for \($0)" }


    apiClientMock.setUserFlagHandler = { _, _ in }


    await sut.buttonTapped()


    XCTAssertEqual(sut.text, "Mock user for 1234")


    XCTAssertEqual(apiClientMock.fetchUserNameCallCount, 1)


    XCTAssertEqual(apiClientMock.fetchUserNameArgValues, [1234])


    XCTAssertEqual(apiClientMock.setUserFlagCallCount, 1)


    XCTAssertEqual(apiClientMock.setUserFlagArgValues.map(\.userId), [1234])


    XCTAssertEqual(apiClientMock.setUserFlagArgValues.map(\.flag), [true])


    }


    func testButtonTapped_Failure() async {


    apiClientMock.fetchUserNameHandler = { _ in


    struct SomeError: Error {}


    throw SomeError()


    }


    await sut.buttonTapped()


    XCTAssertEqual(sut.text, "Error!")


    XCTAssertEqual(apiClientMock.fetchUserNameCallCount, 1)


    XCTAssertEqual(apiClientMock.fetchUserNameArgValues, [1234])


    XCTAssertEqual(apiClientMock.setUserFlagCallCount, 0)


    }
    w GFUDIʹ੒ޭͨ͠৔߹͸

    ϑϥάઃఆ͕ݺ͹ΕΔ
    w GFUDIʹࣦഊͨ͠৔߹͸

    ϑϥάઃఆ͕ݺ͹Εͳ͍

    View Slide

  20. ࣮ࡍʹ࢖ͬͯΈͨ
    w Ұ౓໨͸ࣦഊͯ͠ɺϦτϥΠͨ͠Β

    ੒ޭ͢ΔΑ͏ͳςετ΋ॻ͚Δ
    func testButtonTapped_RetryWithLoading() async {


    await withMainSerialExecutor {


    apiClientMock.fetchUserNameHandler = { _ in


    await Task.yield()


    struct SomeError: Error {}


    throw SomeError()


    }


    let task1 = Task { await sut.buttonTapped() }


    await Task.yield()


    XCTAssertTrue(sut.isLoading)


    XCTAssertNil(sut.text)


    await task1.value


    XCTAssertFalse(sut.isLoading)


    XCTAssertEqual(sut.text, "Error!")


    apiClientMock.fetchUserNameHandler = {


    await Task.yield()


    return "Mock user for \($0)"


    }


    apiClientMock.setUserFlagHandler = { _, _ in }


    let task2 = Task { await sut.buttonTapped() }


    await Task.yield()


    XCTAssertTrue(sut.isLoading)


    XCTAssertNil(sut.text)


    await task2.value


    XCTAssertFalse(sut.isLoading)


    XCTAssertEqual(sut.text, "Mock user for 1234")


    }


    }
    w ͍ͭͰʹJT-PBEJOHͷ੾Γସ͑΋ςετ
    w 5BTLZJFME
    Λ࢖༻͢Δςετ͸ɺ

    XJUI.BJO4FSJBM&YFDVUPS\^Ͱ

    ғΘͳ͍ͱෆ҆ఆʹͳΔʢࣦഊʣ
    w ৄ͘͠͸1PJOU'SFFͷ

    υΩϡϝϯτΛ͝ཡ͍ͩ͘͞

    View Slide

  21. ·ͱΊ
    w ςετΛগͣͭ͠௥Ճ͠ͳ͕ΒɺϚΫϩͷ։ൃΛਐΊΔ
    w 4XJGU"45&YQMPSFSͱิ׬ʹཔΓͳ͕ΒɺϚΫϩग़ྗΛ૊Έཱ͍ͯͯ͘
    w EFCVH%FTDSJQUJPOΛࢀߟʹ͠ͳ͕ΒɺϚΫϩೖྗͷඞཁͳཁૉΛղੳ͢Δ
    w 9DPEF΁ͷҠߦ͕׬ྃͨ͠ΒɺͥͻϚΫϩΛ׆༻ͯ͠

    շదͳ4XJGUϓϩάϥϛϯάੜ׆ΛૹΓ·͠ΐ͏ʙ👍👍

    View Slide