Swift Digest
SE-0486 | Swift Evolution

Migration tooling for Swift features

Proposal
SE-0486
Authors
Anthony Latsis, Pavel Yaskevich
Review Manager
Franz Busch
Status
Implemented (Swift 6.2)

01 何が問題だったのか

SE-0362 で導入された upcoming feature flag によって、ソース互換性を壊しうる言語変更を機能単位で前倒し有効化できるようになりました。実際、Swift 5.8 以降に追加された多くの upcoming feature には、コンパイラが「ここをこう書き換えれば、この機能を有効にしても挙動が保たれる」と判断できる 機械的な移行パス が存在します。たとえば次のようなものです。

  • ExistentialAnySE-0335):Pany P
  • ConciseMagicFileSE-0274):#file#filePath
  • InternalImportsByDefaultSE-0409):import Xpublic import X
  • DisableOutwardActorInferenceSE-0401):これまで暗黙的に推論されていたグローバルアクター isolation を明示する

ところが、こうした書き換えを「コンパイラに自動でやってほしい」と伝える手段がありませんでした。upcoming feature flag を有効化すると、コンパイラは「これからは新しいルールでビルドする」と解釈してエラーや挙動変更を起こすだけで、「ユーザーは移行作業中である」という意図を区別できません。

特に厄介なのは、機能を有効化しても エラーは出ないが挙動だけ変わる タイプの upcoming feature です。たとえば DisableOutwardActorInference は、property wrapper を介して伝播していたアクター isolation の推論を止めるため、有効化前後で型のアクター isolation が静かに変わる可能性があります。これに対して fix-it で「現状の isolation を明示して挙動を保つ書き換え」を提案する手段がなく、SE-0401 のレビュー時点でも「これは別途、移行ツール側で扱うべき問題だ」とされていました。

加えて、こうした移行作業は通常パッケージ全体に対して走らせたいものですが、各ターゲットに対してフラグを立て、ビルドし、fix-it を適用し、Package.swift を更新する、という一連の流れを手作業で繰り返すのは現実的ではありませんでした。

02 どのように解決されるのか

個別の upcoming / experimental feature に対して、新しく migration mode を導入します。これは「機能を有効化する」のではなく「機能を有効化したときに挙動が保たれるよう、ソースコードを移行する意図がある」ことをコンパイラに伝えるためのモードです。migration mode をサポートしている機能では、現在のコードをコンパイルしつつ、その機能を有効化した際に挙動が変わってしまう箇所を警告として報告し、互換性を保つ fix-it を提示します。

コンパイラ側のインターフェース

-enable-upcoming-feature-enable-experimental-feature に、機能名の後ろへ :migrate を付けてモードを指定できるようになります。

-enable-upcoming-feature <feature>[:<mode>]
-enable-experimental-feature <feature>[:<mode>]

<mode> := migrate

たとえば InternalImportsByDefault を移行モードで有効化するには次のように指定します。

-enable-upcoming-feature InternalImportsByDefault:migrate

migration mode で機能を有効化しても、新しいコンパイルエラーや挙動変更は発生しません。代わりに、その機能を本番有効化したときに互換性を保つために必要な書き換えが、fix-it 付きの警告として報告されます。fix-it は 互換性を保つことを最優先 にしており、ソース・バイナリ・挙動のいずれの観点でも壊さない書き換えだけが提案されます。

機能側に機械的な移行パスがない(= migration mode をサポートしていない)場合、:migrate 指定は無視され、その旨の警告が出ます。これは「指定したのに何も起きないので、コードは新機能と互換である」と誤解されるのを防ぐためです。同様に、不正なモード名を渡した場合も警告とともに無視されます。これらの警告はいずれも StrictLanguageFeatures という診断グループに属します。

ある機能が言語モードによって暗黙的に有効化されている場合、その機能に対する :migrate 指定は意味を持たないため無視されます。

診断のグループ化

migration mode 由来の警告は、機能名に対応する診断グループに属するようになります。-print-diagnostic-groups オプションを併用することで、診断メッセージと機能の対応関係を辿れます。複数の機能を同時に migration mode に入れたときに、どのメッセージがどの機能由来かを区別するための土台になります。

swift package migrate コマンド

SwiftPM 側にも、この移行モードをパッケージ単位で扱うためのコマンドが追加されます。

USAGE: swift package migrate [<options>] --to-feature <to-feature> ...

OPTIONS:
  --target <targets>      The targets to migrate to specified set of features or a new language mode.
  --to-feature <to-feature>
                          The Swift language upcoming/experimental feature to migrate to.
  -h, --help              Show help information.

--to-feature には移行したい機能を1つ以上指定し、--target で対象ターゲットを絞り込めます。--target を省略するとパッケージ内のすべてのターゲットが対象になります。

たとえば次のように呼び出します。

swift package migrate --target MyTarget,MyTest --to-feature ExistentialAny

このコマンドは内部で次のことをまとめて行います。

  1. 指定ターゲットを ExistentialAny:migrate フラグ付きでビルドする
  2. コンパイラが提示した fix-it を該当ファイルに適用する
  3. 上記が成功したら、Package.swift の各ターゲットに .enableUpcomingFeature("ExistentialAny") を追記して機能を本番有効化する
.target(
  name: "MyTarget",
  ...
  swiftSettings: [
    // ... existing settings,
    .enableUpcomingFeature("ExistentialAny")
  ]
)
...
.testTarget(
  name: "MyTest",
  ...
  swiftSettings: [
    // ... existing settings,
    .enableUpcomingFeature("ExistentialAny")
  ]
)

Package.swift への追記は、既存の swift package add-setting コマンドと同じ仕組みで行われます。何らかの事情で自動追記できなかった場合は、どのターゲットに何を追加すべきかを示す診断メッセージが表示されるので、それに従って手動で追加します。

このコマンドは新規追加であり、既存のコマンドの挙動には影響しません。サブコマンド名がトップレベルではなく swift package の下に置かれているのは、対象が常に「現在のパッケージ」であり、最近追加された swift package add-target などのリファクタリング系コマンドと一貫性を持たせるためです。

採用にあたっての注意

migration mode の出入り自体は、生成されるコードの挙動に影響を与え得るため、ソース互換性を壊し得る操作として扱われます。実際の運用としては、swift package migrate の各ステップ(migration mode でのビルド、fix-it 適用、Package.swift 更新)が一括で走るので、利用者が中間状態を意識する必要はあまりありません。

Future Directions(今後の見通し)

提案では、今回のスコープ外として次のような拡張方針が示されています。いずれも将来的な検討事項であり、実現を約束するものではありません。

  • 挙動を変えてでも望ましい書き換えを提示する fix-it:たとえば any Psome P に置き換えるような、ソース互換性は崩れるが利用上望ましい変更を、オプションで提示できるようにする方向。
  • 追加機能(additive feature)への適用:typed throwsSE-0413)や opaque parameter types(SE-0341)のように、強制的に有効化される追加機能についても、新機能の活用を促す「採用ヒント」を提示する用途に migration mode を拡張する方向。
  • 診断メタデータの拡充:診断シリアライズ形式に診断グループ名や「この fix-it が挙動を保つかどうか」といった情報を含め、ツール側で互換性を保つ fix-it を優先・自動適用しやすくする方向。

今後の Proposal で機械的な移行パスを伴う upcoming feature を提案する場合は、その移行パスとあわせて migration mode の挙動を Source Compatibility セクションに記述することが期待されます。