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

Xcodeの「Apply」ボタンの裏側

 Xcodeの「Apply」ボタンの裏側

2026/9/11 に iOSDC Japan 2026 で発表した登壇資料です。
https://iosdc.jp/2026/
株式会社ZOZO
ZOZOTOWN開発本部
ZOZOTOWN開発1部 iOSブロック
小松 悟
#iosdc

Avatar for ZOZO Developers

ZOZO Developers PRO

September 11, 2026

More Decks by ZOZO Developers

Other Decks in Technology

Transcript

  1. 例1: protocol 準拠のスタブ生成 protocol HogeProtocol { func hoge() } class

    Hoge: HogeProtocol { } 本資料のデモは全てXcode26.6 - Swift 6.3.3で行ったものになります。 © ZOZO, Inc. 3
  2. 例2: Optional のアンラップ func greet(name: String?) { let message: String

    = name print("Hello, \(message)") } © ZOZO, Inc. 6
  3. 「2択」のApply ボタン 2つの選択肢の文言の意味 1 Coalesce using '??' — name ??

    <#default value#> に書き換える nil だったときの代替値を用意して、処理を続行する修正 Force-unwrap using '!' — name! に書き換える 2 nil ではないと断言し、nil だったら実行時にクラッシュする修正 同じエラーに対して、意味の異なる直し方が2つ 提示されている © ZOZO, Inc. 7
  4. コンパイラはこれを「fix-it」と呼んでいる Apply ボタンの修正提案 = fix-it (コンパイラが生成するデータ) — Apply ボタンで出てくる修正提案は、Xcode の独自機能ではない

    — コンパイラが警告・エラーに添えて生成するデータで、fix-it と呼ばれる — Xcode はそれを受け取って表示・適用しているだけ © ZOZO, Inc. 9
  5. エディタは何を受け取っているのか key.diagnostics: [ { key.id: "unwrap_with_default_value", key.description: "coalesce using '??'

    to provide a default ...", key.fixits: [ { key.offset: 487, key.length: 0, key.sourcetext: " ?? <#default value#>" } ] }, { key.id: "unwrap_with_force_value", ... key.sourcetext: "!" } ] エディタに見えているもの ボタンのラベル = key.description その裏で届いている実データ ボタンの動作 = key.fixits fix-it 付き note が複数届くと、Apply ボタンも複数表示される © ZOZO, Inc. 10
  6. fix-it = source range + replacement text FixIt = {

    どこを: offset / length, 何に: sourcetext } Swift コンパイラでの実際の定義 /// Represents a fix-it, a replacement of one range of text with another. class FixIt { CharSourceRange Range; ← どこを (source range) std::string Text; ← 何に (replacement text) ... }; Apply ボタンが押されたとき Xcode がやるのは、この Range をこの Text で置き換えるだけ 参照: https://github.com/swiftlang/swift/blob/3fdf46f7ea815a7e7afda6dca4437a33bbca09f3/include/swift/AST/DiagnosticConsumer.h#L65 © ZOZO, Inc. 11
  7. コンパイルのフェーズ ① Parse ★ ソーステキスト → AST (構文木) ② Sema

    ★ 型チェック — fix-it の主産地 ③ SILGen 中間表現 (SIL) の生成 ④ SIL Optimizer ★ SIL の解析・最適化 コードの並びだけで直せる(例: 'hello' → "hello" のクォート修正) 型を見ないと決められない(例: Optional の ??) 実行経路を追わないと分からない(例: return 忘れ) © ZOZO, Inc. ⑤ IRGen LLVM IR の生成 ⑥ LLVM 機械語生成 12
  8. 具体例: Sema が fix-it を生成するまで let message: String = name

    1 名前解決 — name は引数 (String?) だと判明 2 制約生成 — 「String? は String に変換可能」という制約を作る 3 制約充足 — 解けない (T? → T の変換は存在しない) 4 再挑戦 — 「修理 (fix)」を許可してもう一度解く → アンラップすれば解けると発見 5 修理内容がそのまま fix-it になる © ZOZO, Inc. 13
  9. Xcode に SourceKit が常駐している エディタに入力する ↓ 編集差分だけを送信 ────────▶ Xcode ◀────────

    SourceKit └─ Parse → Sema → SILGen → SILOptimizerの診断部分 を実行 診断 + fix-it を返却 ↓ エディタに赤線と Fix ボタンが出る — SourceKit が独自の解析エンジンを持っているのではなく、コンパイラ本体と同じコードで解析している © ZOZO, Inc. 15
  10. 実際の会話ログ $ SOURCEKIT_LOGGING=3 /Applications/Xcode.app/Contents/MacOS/Xcode リクエスト (Xcode → SourceKit) 18:10:03 {

    ← ファイル全文を渡して解析開始 レスポンス (SourceKit → Xcode) { key.request: source.request.editor.open, key.diagnostics: [ { key.name: ".../Sample.swift", key.description: "value of optional type 'String?' must be ...", key.sourcetext: "//\n// key.diagnostics: [ { Sample.swift\n..." } key.description: "coalesce using '??' ...", key.fixits: [ { 18:10:15 { ← 変更箇所を送る key.offset: 487, key.request: source.request.editor.replacetext, ... key.length: 0, } key.sourcetext: " ?? <#default value#>" } ] 18:10:17 { ← 診断結果をリクエスト }, ... ] key.request: source.request.diagnostics, ... } ← Fix ボタンの中身 } ] } — シンタックスハイライトや補完等の表示も、全て SourceKit から出力される — fix-it も例外ではなく、diagnostics レスポンスに含まれるフィールドの1つ © ZOZO, Inc. 16
  11. Swift 公式ガイドライン — Diagnostics.md「fix-it」全ルール — fix-it は複数行にまたがれるが、すべての環境で表示されるとは限らない — fix-it は、添付先の診断と同じファイル内になければならない

    — エラー / 警告本体に付ける fix-it は「唯一・明白・ほぼ確実に正しい」修 — note に fix-it を付けるなら、文言は「 fix-it が行う操作と、その理由 」を 正だけ 説明する — fix-it 付き note が複数あるときは選択肢として扱う。 最初の選択肢は最も — 本体に fix-it が付いているなら、その note には fix-it を付けない 安全なもの にする — 警告を黙らせる手段として「括弧を追加」より良い方法を探す — 必要ならプレースホルダ <#...#> を使う。 fix-it を出さないより、プレ ースホルダ入りで出す方が良い — 綴りは「fix-it」が正 (camelCase では FixIt / fixIt) 診断の種類(同ドキュメント「Errors vs. Warnings」より) — error: 不正なコード。ビルドが止まる — warning: 意図は明確だが直すべきコード。ビルドは通る — note: error/warning に付属する補足。原因の場所や fix-it の選択肢を示す 参照: https://github.com/swiftlang/swift/blob/main/docs/Diagnostics.md © ZOZO, Inc. 17
  12. 曖昧さゆえに出せない例 — 修正候補が一意に決まらない let totalPrice: Double = 59.5 let itemCount:

    Int = 12 let average = totalPrice / itemCount error: binary operator '/' cannot be applied to operands of type 'Double' and 'Int' note: overloads for '/' exist with these partially matching parameter lists: (Double, Double), (Int, Int) ← 候補は2つに絞れているのに fix-it なし — Double(itemCount) なら小数の割り算、Int(totalPrice) なら整数の割り算 → 結果の値が変わる — average の型はこれから推論される ものなので期待型が存在せず、どちらに変換すべきかを決定できない → fix-it は出ない © ZOZO, Inc. 18
  13. 型を書くと fix-it が出る — 修正の方向が一意に決まる average に型を書いて、修正の方向が一意に決まる文脈にすればいい: let average: Double

    = totalPrice / itemCount error: cannot convert value of type 'Int' to expected argument type 'Double' let average: Double = totalPrice / itemCount ^ Double( ) ← 型を書いただけで fix-it が出る — average: Double と書いたことで期待型が固定され、itemCount を Double に変換する方向しかなくなる 同じ式でも「修正が一意に決まるか」で、fix-it を出す / 出さないが分かれている © ZOZO, Inc. 19
  14. なぜ誤った修正案を出してくるのか Apply を押すと—— protocol P { func f() struct S:

    P { } func f() { code struct S: P { func f() throws {} } // throws が余計で準拠を満たせない } func f() throws {} // error: invalid redeclaration of 'f()' error: type 'S' does not conform to protocol 'P' } note: candidate throws, but protocol does not allow it ← 原因は正確に言い当てている note: add stubs for conformance ← でも出せる fix-it はこれだけ — コンパイラは「throws が原因」と分かっているのに、欲しい修正 (throws を消す) の fix-it が存在しない — 代わりに出るスタブ追加を適用すると、throws の有無だけ違う f() が並び、確実にコンパイルエラーになる © ZOZO, Inc. 20
  15. ここまでのまとめ — fix-it = source range + replacement text。Apply は

    range を text で置き換えるだけ — fix-it は Parse, Sema, SILOptimizer のフェーズで生まれる — エラー / 警告本体に付くのは「唯一・明白・ほぼ確実に正しい」修正だけ — 一方で、原因が分かっているのに fix-it が出ないケースが実際にある © ZOZO, Inc. 21
  16. 実際に直した話 ① — 「Replace 'nonisolated' with 'nonisolated'」 computed property やメソッドに付けた

    nonisolated(unsafe) が 無意味なとき、コンパイラの提案がこれ: Replace 'nonisolated' with 'nonisolated' …同じものに置き換えろ?? — 何を直してほしいのか読み取れない。押しても何も変わらない fix-it — 意図: nonisolated(unsafe) → nonisolated に置き換えたか った 参照: https://github.com/swiftlang/swift/pull/88086 © ZOZO, Inc. 23
  17. ①の原因 — range の取り方を1箇所間違えていた // lib/Sema/TypeCheckAttr.cpp — visitNonisolatedAttr() 内 diag.fixItReplace(attr->getStartLoc(),

    "nonisolated") ← nonisolated キーワードだけを指す range diag.fixItReplace(attr->getRange(), "nonisolated") ← nonisolated(unsafe) 全体を指す range(修正後) — range の取り方を1つ間違っていたため「X を X に置き換えろ」という状態になっていた — 修正は1行。 Before: Replace 'nonisolated' with 'nonisolated' After: © ZOZO, Inc. Replace 'nonisolated(unsafe)' with 'nonisolated' 24
  18. 実際に直した話 ② — Apply すると finalclass が生まれる class MyClass: Sendable

    {} // warning: non-final class 'MyClass' // cannot conform to 'Sendable' ↓ Apply finalclass MyClass: Sendable {} // コンパイルエラー! — fix-it を押した結果、壊れたコードが生まれる — エディタは意味を理解せずテキストを貼るだけ — だからコンパイラ側 の text が1文字欠けるとそのまま壊れる 修正前: 実際に出ていた「Insert 'final'」 参照: https://github.com/swiftlang/swift/pull/88180 © ZOZO, Inc. 25
  19. ②の原因 — スペースが不足 // lib/Sema/TypeCheckConcurrency.cpp .fixItInsert(classDecl->getStartLoc(), "final") ← 末尾にスペースがない .fixItInsert(classDecl->getStartLoc(),

    ← 末尾にスペースを追加 (修正後) "final ") — 既存テストは診断メッセージしか検証していなかったので、壊れた fix-it がすり抜けていた © ZOZO, Inc. 26
  20. 直そうとしたら指摘が入った話 スライド20の「本当に欲しい修正は throws を消すこと」 throws を書くと準拠エラーになる (Xcode 実画面) note「Candidate throws…」に自作

    fix-it が Apply として出る — TypeCheckProtocol.cpp の ThrowsConflict 診断に fixItRemove を添付 — protocolに準拠できるようにthrowsを削除するfix-itを追加 参照: https://github.com/swiftlang/swift/pull/88268 © ZOZO, Inc. 27
  21. 直した後の検証方法 — 3段構え 方法 確認できること 1回の確認にかかる時間 1. CLI で見る 診断が出ること・文言

    (fix-it の中身までは見えない) 差分ビルド数分 2. テストを書く fix-it の中身 (range と置換テキスト) が正しいこと 差分ビルド数分 3. Xcode に差し替え Xcode 上で動作できる 数時間 (毎回フルビルド必須) © ZOZO, Inc. 29
  22. 方法 1. CLI で見る — ビルドしたコンパイラを直接叩く $ ~/build/.../swift-macosx-arm64/bin/swiftc -typecheck test.swift

    — 修正入りの swift-frontend で -typecheck → 診断が出ること・文言を数秒で確認できる — ただし fix-it の中身 (range と置換テキスト) までは表示されない。実際の見え方: error: value of optional type 'String?' must be unwrapped to a value of type 'String' let message: String = name |- note: coalesce using '??' to provide a default | when the optional value contains 'nil' `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil' — 文言 (note) は見えるが、?? <#default value#> という挿入テキストや位置はどこにも出ない — fix-it の適用も CLI ではできない(-fixit-allはあるがobsoleteオプション) © ZOZO, Inc. 30
  23. 方法 2. テストを書く — -verify モードと fix-it 期待値 // expected-warning@+1

    {{'nonisolated(unsafe)' has no effect on ...}} {{3-22=nonisolated}} nonisolated(unsafe) func nonisolatedUnsafe(otherActor: MyActor) -> Int { } ↑ 3文字目 ↑ 22文字目 — {{開始桁-終了桁=置換テキスト}} が fix-it の期待値。位置とテキストを桁単位で検証する — 一致すれば成功、不一致なら失敗 — 実際の Swift コンパイラのテストも、この方式で書かれている © ZOZO, Inc. 31
  24. 方法 2 の実例 — 修正がテストの数字にそのまま現れる PR #88086 のテスト差分 (range を直した)

    - {{3-14=nonisolated }} ← 3〜14桁 (11文字) = "nonisolated" (11文字) だけを置換していた + {{3-22=nonisolated }} ← 3〜22桁 (19文字) = "nonisolated(unsafe)" (19文字) 全体を置換 PR #88180 のテスト差分 (期待値を新設した) - // expected-warning @+1 {{non-final class 'Klass' cannot conform ... }} + // expected-warning @+1 {{non-final class 'Klass' cannot conform ... }} {{1-1=final }} — #88180 では、既存テストは診断の文言しか検証していなかった → fix-it の中身が壊れていても通っていた © ZOZO, Inc. 32
  25. 方法 3. Xcode にコンパイラを差し替える — ツールチェーンを作る — ツールチェーン (.xctoolchain) =

    コンパイラ・標準ライブラリ・SourceKit など、 ビルドに使う道具一式のパッケージ — Xcode は使うツールチェーンをメニューから切り替えられる → 自分でビルドしたコンパイラを差し込める $ ./swift/utils/build-toolchain com.example # .xctoolchain 一式を作る (数時間) $ tar -xzf swift-LOCAL-*-osx.tar.gz -C ~/ # → ~/Library/Developer/Toolchains/ へ展開 Toolchains メニューで自分のツールチェーンに切り替え © ZOZO, Inc. 33
  26. 方法 3-2. 実際に適用して動作確認 自分の書いた fix-it が Apply ボタンとして出る (直した2件) Xcode26.6

    - Swift 6.3.3 Xcode26.6 - Swift 6.3.3 自作ツールチェーン 自作ツールチェーン 提案が「Replace 'nonisolated(unsafe)' with ‘nonisolated'」に。 挿入テキストがスペース込みの "final " に。 Apply しても finalclass にならない range が (unsafe) まで含む正しい置換になった © ZOZO, Inc. 34
  27. まとめ Apply ボタンの正体 Swift に Contribute しよう! — fix-it =

    コンパイラが診断に添える「 どこを (range) + 何に — fix-it は対応しやすい (text) 」という情報 — fix-it は唯一・明白・ほぼ確実に正しい修正の場合にしか — テストも書きやすい — 不具合を実際に Xcode で確認できる 出さない — fix-it は Xcode に常駐する SourceKit がエディタへ届ける。 だからビルドしなくても、タイプした瞬間に表示される © ZOZO, Inc. 35