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
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
『コードを書く以外の』エンジニアリング〜課金基盤移行プロジェクト推進のためのTips4選
yuriko1211
0
510
5分で問診!Composer セキュリティ健康診断
codmoninc
0
330
LaravelLive Japan の裏方のすべて — 第188回 PHP勉強会@東京 (2026-06-24)
suguruooki
2
150
トークンをケチるな、設計しろ:GitHub Copilotを賢く使うコンテキスト戦略
ochtum
0
320
Claude Opus 4.6以後の受託開発エンジニアの変化(Claude Code開発ノウハウ大公開スペシャルbyクラスメソッド)
iidatakuma
1
780
ソフトウェア設計に溶けるインフラ ― AWS CDK のインフラ認識論
konokenj
2
550
JAWS-UG横浜 #102 AWSサ終供養LT会 成仏できない AWS サービスたち 〜本日、三体供養します〜
maroon1st
0
210
Go言語とトイモデルで学ぶTransformerの気持ち / fukuokago23-transformer
monochromegane
0
110
琵琶湖の水は止められても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
390
ビデオ通話が繋がる0.2秒で何が起きているのか
supurazako
2
150
フィードバックで育てるAI開発
kotaminato
1
120
【やさしく解説 設計編 #0】DDDのコード、読めるのに分からない人へ
panda728
PRO
2
270
Featured
See All Featured
Ruling the World: When Life Gets Gamed
codingconduct
0
280
The Illustrated Guide to Node.js - THAT Conference 2024
reverentgeek
1
410
Side Projects
sachag
455
43k
The Pragmatic Product Professional
lauravandoore
37
7.4k
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
1
2.8k
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
118
120k
Game over? The fight for quality and originality in the time of robots
wayneb77
1
220
My Coaching Mixtape
mlcsv
0
170
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
34
2.8k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
28
3.6k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
2k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
55
3.4k
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ϓϩάϥϛϯάੜ׆ΛૹΓ·͠ΐ͏ʙ👍👍