Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
SwiftSyntaxMacrosに入門してみた
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
Toshiya Kobayashi (とち)
September 26, 2023
Programming
420
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
SwiftSyntaxMacrosに入門してみた
Toshiya Kobayashi (とち)
September 26, 2023
More Decks by Toshiya Kobayashi (とち)
See All by Toshiya Kobayashi (とち)
ユーザー数10万人規模のアプリで挑んだトップ画面のUI刷新
tochi86
0
2.2k
Swift Zoomin' #9 報告会
tochi86
1
580
Other Decks in Programming
See All in Programming
SLOをサービス品質の共通言語にするために 取り組んできたこと
wakana0222
0
500
琵琶湖の水は止められてもNet--HTTPのリトライは止められない / You might be able to stop the water flow of Lake Biwa but you can't stop Net::HTTP retries
luccafort
PRO
0
380
アルゴリズムは何を圧縮しているのか ─ Haskell から育った「圧縮代数」というメンタルモデル
naoya
16
3.5k
そのテスト、説明できますか?~LWテスト戦略FW~のご紹介
nakahara
0
210
エンジニアにデザインハーネスを 〜デザインプロセスを規定するためのハーネス〜 / Design harness from an engineer's perspective
rkaga
2
1.5k
これからAgentCoreを触る方へトレンドはGatewayです
har1101
6
490
yield再入門 #phpcon
o0h
PRO
0
380
PHP に部分適用が来るぞ!……ところで何それ?おいしいの? #phpcon / phpcon-2026
shogogg
0
200
ルールを書いて終わらせないハーネスエンジニアリング
yug1224
2
1.5k
Laravelで学ぶ Webアプリケーションチューニング入門/web_application_tuning_101
hanhan1978
4
560
Laravel Boostに学ぶ、AIにPHPを書かせる技術 〜OSSの実装から蒸留するエージェント制御の王道〜
kentaroutakeda
3
440
Claude Opus 4.6以後の受託開発エンジニアの変化(Claude Code開発ノウハウ大公開スペシャルbyクラスメソッド)
iidatakuma
1
760
Featured
See All Featured
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
72
40k
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
200
Art, The Web, and Tiny UX
lynnandtonic
304
22k
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
150
Music & Morning Musume
bryan
47
7.3k
Are puppies a ranking factor?
jonoalderson
1
3.7k
A Tale of Four Properties
chriscoyier
163
24k
Site-Speed That Sticks
csswizardry
13
1.3k
How to Talk to Developers About Accessibility
jct
2
380
So, you think you're a good person
axbom
PRO
2
2.1k
The Hidden Cost of Media on the Web [PixelPalooza 2025]
tammyeverts
2
350
Optimizing for Happiness
mojombo
378
71k
Transcript
4XJGU4ZOUBY.BDSPTʹ ೖͯ͠Έͨ 5PTIJZB,PCBZBTIJ ͱͪ!UPDIJ@
ϚΫϩʹೖͨ͠Ϟνϕʔγϣϯ 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ελΠϧͷΠϯλʔϑΣʔεͰར༻ͨ͘͠ͳͬͨ
ࠓճ࡞͢Δ!(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
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 Λઃఆ
ςετΛ༻ҙ͢Δ 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Λͦͷ··ग़ྗ͢Ε ςετ͕௨Δ
ςετΛॻ͖͑Δ 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 )
4XJGU"45&YQMPSFSͰߏจΛௐΔ w IUUQTTXJGUBTUFYQMPSFSDPN ͳΔ΄Ͳʜɻ ·ͣ'VODUJPO%FDM͔Β ࢝ΊΕྑͦ͞͏ͩͳ🧐
'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ͱ͍͏ͷΛٻΊ͖ͯͨ
QBSBNFUFS$MBVTFΛຒΊΔ w ίʔυิʹཔΓͳ͕Βɺ4XJGU"45&YQMPSFSͰௐͨ༰ΛຒΊ͍ͯ͘ parameterClause: FunctionParameterClauseSyntax( parameters: FunctionParameterListSyntax([ FunctionParameterSyntax(stringLiteral: "_ mock:
Mock") ]) ) ࣍ઌ಄ʹTUBUJDΛ͚ͯΈΑ͏
TUBUJDΛ͚Δ w 'VODUJPO%FDM4ZOUBYʹNPEJ fi FSTΛՃ͢Δ ࣍4FMGΛฦͯ͠ΈΑ͏ FunctionDeclSyntax( modifiers: DeclModifierListSyntax([ DeclModifierSyntax(name:
"static") ]), name: TokenSyntax(stringLiteral: "mock"), signature: FunctionSignatureSyntax(…) )
4FMGΛฦ͢ w 'VODUJPO4JHOBUVSF4ZOUBYʹSFUVSO$MBVTFΛՃ͢Δ ͋ͱؔʹίʔυϒϩοΫΛՃ͢Δ͚ͩʂ signature: FunctionSignatureSyntax( parameterClause: FunctionParameterClauseSyntax(…), returnClause: ReturnClauseSyntax(
type: IdentifierTypeSyntax(name: "Self") ) )
͜Μͳײ͡ͷϊϦͰΈཱ͍ͯͯ͘ΜͰ͕͢ʜ struct APIClient { var fetch: @Sendable (Int) async throws
-> String static func mock(_ mock: Mock) -> Self { Self (fetch: mock.fetch) } } w ϚΫϩͷೖྗΛղੳ͢Δඞཁ͕͋Δ w ͜ΕΒͷ໊લʢGFUDIʣ ͔͜͜Βऔ͖͍ͬͯͨ
ϚΫϩೖྗΛղੳ͢Δ 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 ϚΫϩʹೖྗ͞Εͨίʔυͷߏจ͕ ΞεΩʔΞʔτܗࣜͰग़ྗ͞ΕΔ
Ϋϩʔδϟͷม໊Λऔಘ͢Δ 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 దٓΩϟετΛར༻͠ͳ͕Βɺͯͷཁૉ·ͰͨͲ͍ͬͯ͘
ϚΫϩ͕ w ͜͜·Ͱʹհͨ͠ςΫχοΫΛۦ͠ͳ͕Βʜ w ςετΛॻ͖ w 4XJGU"45&YQMPSFSΛࢀߟʹϚΫϩग़ྗΛΈཱͯ w EFCVH%FTDSJQUJPOΛࢀߟʹϚΫϩೖྗΛղੳͯ͠ w
ͱΓ͋͑ͣಈ͖ͦ͏ͳϚΫϩ͕͠·ͨ͠ʙʂ🎉 w IUUQTHJUIVCDPNUPDIJ(FOFSBUF.PDL
࣮ࡍʹͬͯΈͨ 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 } } }
࣮ࡍʹͬͯΈͨ 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!" } } }
࣮ࡍʹͬͯΈͨ 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) } } ... }
࣮ࡍʹͬͯΈͨ 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ʹࣦഊͨ͠߹ ϑϥάઃఆ͕ݺΕͳ͍
࣮ࡍʹͬͯΈͨ 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ͷ υΩϡϝϯτΛ͝ཡ͍ͩ͘͞
·ͱΊ w ςετΛগͣͭ͠Ճ͠ͳ͕ΒɺϚΫϩͷ։ൃΛਐΊΔ w 4XJGU"45&YQMPSFSͱิʹཔΓͳ͕ΒɺϚΫϩग़ྗΛΈཱ͍ͯͯ͘ w EFCVH%FTDSJQUJPOΛࢀߟʹ͠ͳ͕ΒɺϚΫϩೖྗͷඞཁͳཁૉΛղੳ͢Δ w 9DPEFͷҠߦ͕ྃͨ͠ΒɺͥͻϚΫϩΛ׆༻ͯ͠
շదͳ4XJGUϓϩάϥϛϯάੜ׆ΛૹΓ·͠ΐ͏ʙ👍👍