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.3k
Swift Zoomin' #9 報告会
tochi86
1
580
Other Decks in Programming
See All in Programming
為什麼你並不需要ViewModel / No, you don't need a ViewModel
lovee
1
480
仕様駆動開発へのトライを機に チームに適合する手法を模索し続けている話
freee
PRO
0
450
php-fpmのプロセスが枯渇した日-調査・対処・そして本当にやるべきだったこと-
shibuchaaaan
0
240
ドリフトを絶対に許さない(?)CDK運用 / CDK Ops with Zero Tolerance for Drifts (?)
akihisaikeda
1
180
Go言語とトイモデルで学ぶTransformerの気持ち / fukuokago23-transformer
monochromegane
0
160
jsmini JavaScript Engine を作ってみた話
yosuke_furukawa
PRO
0
300
琵琶湖の水は止められても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
620
Laravelで学ぶ Webアプリケーションチューニング入門/web_application_tuning_101
hanhan1978
4
1.7k
全PRの83%がAIレビューだけでマージできるようになった開発組織はその後どうなったか
athug
1
1.5k
言葉の格闘技のススメ~紙とペンと言葉から始める、キャリアの描き方~
progresscicada
2
140
言語を使う側から、作る側へ。 自作 Lisp で得た新たな気づき。
andpad
0
160
PHP初心者セッション2026 〜生成AIでは見えない裏側を知る:今だからLAMPを通して仕組みを学ぶ〜
kashioka
0
870
Featured
See All Featured
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
360
First, design no harm
axbom
PRO
2
1.2k
Deep Space Network (abreviated)
tonyrice
0
250
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
2
450
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
540
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Data-driven link building: lessons from a $708K investment (BrightonSEO talk)
szymonslowik
1
1.2k
The Spectacular Lies of Maps
axbom
PRO
1
890
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
650
Speed Design
sergeychernyshev
33
2k
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
640
Done Done
chrislema
186
16k
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ϓϩάϥϛϯάੜ׆ΛૹΓ·͠ΐ͏ʙ👍👍