Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SwiftSyntaxMacrosに入門してみた

とち🐹
September 26, 2023

 SwiftSyntaxMacrosに入門してみた

とち🐹

September 26, 2023
Tweet

More Decks by とち🐹

Other Decks in Programming

Transcript

  1. ϚΫϩʹೖ໳ͨ͠Ϟνϕʔγϣϯ 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ελΠϧͷΠϯλʔϑΣʔεͰ΋ར༻ͨ͘͠ͳͬͨ
  2. ࠓճ࡞੒͢Δ!(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
  3. 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 Λઃఆ
  4. ςετΛ༻ҙ͢Δ 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Λͦͷ··ग़ྗ͢Ε͹ ςετ͕௨Δ
  5. ςετΛॻ͖׵͑Δ 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 )
  6. '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ͱ͍͏΋ͷΛٻΊ͖ͯͨ
  7. ͜Μͳײ͡ͷϊϦͰ૊Έཱ͍ͯͯ͘ΜͰ͕͢ʜ struct APIClient { var fetch: @Sendable (Int) async throws

    -> String static func mock(_ mock: Mock) -> Self { Self (fetch: mock.fetch) } } w ϚΫϩͷೖྗΛղੳ͢Δඞཁ͕͋Δ w ͜ΕΒͷ໊લʢGFUDIʣ͸ ͔͜͜Βऔ͖͍ͬͯͨ
  8. ϚΫϩೖྗΛղੳ͢Δ 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 ϚΫϩʹೖྗ͞Εͨίʔυͷߏจ໦͕ 
 ΞεΩʔΞʔτܗࣜͰग़ྗ͞ΕΔ
  9. Ϋϩʔδϟͷม਺໊Λऔಘ͢Δ 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 దٓΩϟετΛར༻͠ͳ͕Βɺ໨౰ͯͷཁૉ·ͰͨͲ͍ͬͯ͘
  10. ࣮ࡍʹ࢖ͬͯΈͨ 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 } } }
  11. ࣮ࡍʹ࢖ͬͯΈͨ 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!" } } }
  12. ࣮ࡍʹ࢖ͬͯΈͨ 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) } } ... }
  13. ࣮ࡍʹ࢖ͬͯΈͨ 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ʹࣦഊͨ͠৔߹͸ 
 ϑϥάઃఆ͕ݺ͹Εͳ͍
  14. ࣮ࡍʹ࢖ͬͯΈͨ 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ͷ 
 υΩϡϝϯτΛ͝ཡ͍ͩ͘͞