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
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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
はてなアカウント基盤 State of the Union
cockscomb
1
1.3k
型も通る、synthも通る、それでも危ない 〜AIのCDKの権限とコストを機械で検証する〜 / It Passes Type Checks, It Passes Synth Checks, but It’s Still Risky — Automatically Verifying Permissions and Costs in AI’s CDK —
seike460
PRO
1
350
Welcome to the "Parametricity" 🏙️ − Generic だけど Specific な世界 −
guvalif
PRO
1
170
Haskell/Servantを通してWebミドルウェアを捉え直す
pizzacat83
1
580
分散システム、なんですぐ死んでしまうん?耐障害性を高めたいあなたのためのレジリエンスパターン入門
mshibuya
7
6.3k
どこまでゆるくて許されるのか
tk3fftk
0
500
技術記事、 専門家としてのプログラマ、 言語化
mizchi
14
7.5k
5分で問診!Composer セキュリティ健康診断
codmoninc
0
330
気圧・高度・GPSを記録&可視化するアプリ「Koudo」を作った話
hjmkth
1
360
광주소프트웨어마이스터고등학교 DevFest 특강 - 바이브 코딩 시대에서 주니어 개발자로 살아남는 방법
utilforever
1
140
Claude Opus 4.6以後の受託開発エンジニアの変化(Claude Code開発ノウハウ大公開スペシャルbyクラスメソッド)
iidatakuma
1
780
AI 輔助遺留系統現代化的經驗分享
jame2408
1
1.2k
Featured
See All Featured
WCS-LA-2024
lcolladotor
0
720
Kristin Tynski - Automating Marketing Tasks With AI
techseoconnect
PRO
0
290
Git: the NoSQL Database
bkeepers
PRO
432
67k
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
420
16th Malabo Montpellier Forum Presentation
akademiya2063
PRO
0
250
Designing for Performance
lara
611
70k
Paper Plane
katiecoart
PRO
2
52k
Faster Mobile Websites
deanohume
310
32k
Side Projects
sachag
455
43k
Designing for Timeless Needs
cassininazir
1
370
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
220
How GitHub (no longer) Works
holman
316
150k
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ϓϩάϥϛϯάੜ׆ΛૹΓ·͠ΐ͏ʙ👍👍