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

SwiftSyntaxMacrosに入門してみた

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

 SwiftSyntaxMacrosに入門してみた

Avatar for Toshiya Kobayashi (とち)

Toshiya Kobayashi (とち)

September 26, 2023
Tweet

More Decks by Toshiya Kobayashi (とち)

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ͷ 
 υΩϡϝϯτΛ͝ཡ͍ͩ͘͞