Swift 5.0 日本語化計画 : Swift 5.0
Swift のローカルリファクタリング
2017年8月22日 Xi Ge
Xcode 9 には、全く新しいリファクタリングエンジンが含まれています。単一の Swift ソースファイル内でコードをローカル的に変換することも、複数のファイルや異なる言語ででも発生するメソッドやプロパティの名前を変更することなど、グローバルに変換することもできます。ローカルリファクタリングの背後にあるロジックは、コンパイラと SourceKit で完全に実装されており、すぐに Swift のリポジトリ のオープンソースにあります。したがって、Swift の愛好家はリファクタリングを言語に提供できます。この記事では、シンプルなリファクタリングを Xcode でどのように実装して表面化させることができるかについて説明します。
リファクタリングの種類
ローカルリファクタリング は、単一のファイルの範囲内で発生します。ローカルリファクタリングの例には、Extract メソッド と Extract Repeated Expression があります。グローバルリファクタリング は、複数のファイル (グローバルでの名前変更など) にまたがるコードを変更しますが、現在 Xcode による特別な調整が必要であり、現在は Swift コードベース内で独自に実装することはできません。この記事では、ローカルリファクタリングに焦点を当てていますが、これは彼ら自身の権利では非常に力強いでしょう。
リファクタリングアクションは、エディタでのユーザのカーソル選択によって開始されます。どのように初期化されるかに応じて、リファクタリングアクションをカーソルベースまたは範囲ベースのものとして分類します。カーソルベースのリファクタリング では、名前変更のリファクタリングなどの Swift ソースファイル内のカーソル位置によって十分に指定されたリファクタリングターゲットがあります。対照的に、範囲ベースのリファクタリング では、Extract メソッドリファクタリングなど、ターゲットを指定するための開始位置と終了位置が必要です。これらの2つのカテゴリの実装を容易にするために、Swift リポジトリでは ResolvedCursorInfo および ResolvedRangeInfo という事前分析結果を提供し、Swift ソースファイル内のカーソル位置または範囲に関する一般的な質問に答えることができます。
たとえば、ResolvedCursorInfo は、ソースファイル内の位置が式の開始点を指しているかどうかと、存在する場合は、その式の対応するコンパイラオブジェクトを提供します。また、カーソルが名前を指す場合、ResolvedCursorInfo はその名前に対応する宣言を提供します。同様に、ResolvedRangeInfo は、範囲に複数のエントリポイントまたは終了ポイントがあるかどうかなど、与えられたソース範囲に関する情報をカプセル化します。
Swift 用の新しいリファクタリングを実装するには、カーソルまたは範囲の位置の生の表現から始める必要はありません。代わりに、ResolvedCursorInfo と ResolvedRangeInfo から始めて、リファクタリング固有の解析を導き出すことができます。
カーソルベースのリファクタリング
カーソルベースのリファクタリングは、Swift のソースファイル内のカーソル位置によって開始されます。リファクタリングアクションは、リファクタリングエンジンが IDE で使用可能なアクションを表示し、変換を実行するために使用するメソッドを実装します。
具体的には、使用可能なアクションを表示するには:
- ユーザは Xcode のエディタから位置を選択します。
- Xcode は、sourcekitd に対して、その位置で利用可能なリファクタリングアクションが何かを確認するよう要求します。
- 実装された各リファクタリングアクションは ResolvedCursorInfo オブジェクトで照会され、アクションがその位置に適用可能かどうかを確認します。
- 適用可能なアクションのリストは、sourcekitd からの応答として返され、Xcode によってユーザに表示されます。
ユーザーが利用可能なアクションの 1 つを選択すると:
- Xcode は sourcekitd に対して、ソースの位置で選択されたアクションを実行するよう要求します。
- 特定のリファクタリングアクションは、同じ位置から派生した ResolvedCursorInfo オブジェクトで照会され、アクションが適用可能であることを検証します。
- リファクタリングアクションは、テキストソース編集で変換を実行するよう求められます。
- ソース編集は sourcekitd からの応答として返され、Xcode エディタによって適用されます。
String Localization (文字列ローカル化) のリファクタリングを実装するには、Refactoring Kinds.def ファイルでこのリファクタリングを以下のようなエントリで宣言する必要があります。
CURSOR_REFACTORING は、このリファクタリングがカーソル位置で初期化されるため、実装で ResolvedCursorInfo を使用するように指定します。最初のフィールド LocalizeString は、このリファクタリングの内部名を Swift コードベースで指定します。この例では、このリファクタリングに対応するクラスの名前は RefactoringActionLocalizeString です。文字列リテラル "Localize String" は、このリファクタリングが UI 内のユーザーに表示されるための表示名です。最後に、"localize.string" は、Swift ツールチェーンがソースエディタとの通信で使用するリファクタリングアクションを識別する安定したキーです。このエントリによってもまた、C++ コンパイラは String Localization リファクタリングとその呼び出し元のクラススタブを生成することもできます。したがって、必要な関数の実装に集中することができます。
このエントリを指定したら、Xcode に教えるために 2 つの関数を実装する必要があります:
- リファクタリングアクションを表示することが適切な時。
- ユーザーがこのリファクタリングアクションを呼び出すときに適用されるコード変更。
両方の宣言は、上記のエントリから自動的に生成されます。(1) を満たすには、RefactoringActionLocalizeString の isApplicable 関数を以下のように、Refactoring.cpp に実装する必要があります。
1 bool RefactoringActionLocalizeString::
2 isApplicable(ResolvedCursorInfo CursorInfo) {
3 if (CursorInfo.Kind == CursorInfoKind::ExprStart) {
4 if (auto *Literal = dyn_cast<StringLiteralExpr>(CursorInfo.TrailingExpr) {
5 return !Literal->hasInterpolation(); // Not real API.
6 }
7 }
8 }
ResolvedCursorInfo オブジェクトを入力として使用すると、使用可能なリファクタリングメニューに "ローカライズ文字列" をいつ設定するのかを確認する事は本当に些細な事です。この場合、カーソルが式の開始点 (3 行目) を指し、式が補間 (5 行目) のない文字列リテラル (4 行目) であることを確認するだけで十分です。
次に、リファクタリングアクションが適用されている場合、カーソルの下のコードをどのように変更するかを実装する必要があります。これを行うには、RefactoringActionLocalizeString の performChange メソッドを実装しなければなりません。performChange の実装では、受け取った isApplicable と同じ ResolvedCursorInfo オブジェクトにアクセスできます。
1 bool RefactoringActionLocalizeString:: 2 performChange() { 3 EditConsumer.insert(SM, Cursor.TrailingExpr->getStartLoc(), "NSLocalizedString("); 4 EditConsumer.insertAfter(SM, Cursor.TrailingExpr->getEndLoc(), ", comment: \"\")"); 5 return false; // Return true if code change aborted. 6 }
さらに String Localization を例として使用すると、performChange 関数は実装するのはかなり簡単です。関数本体では、EditConsumer を使用して、3 行目と 4 行目に示すように、適切な Foundation API 呼び出しでカーソルが指す式のまわりをテキストで編集できます。
レンジベースのリファクタリング
上の図が示すように、レンジベースのリファクタリングは、Swift ソースファイル内の連続するコード範囲を選択することによって開始されます。Extract Expression リファクタリングの実装を例として、RefactoringKinds.def で以下の項目を初めに宣言する必要があります。
このエントリは、Extract Expression リファクタリングが範囲選択によって開始され、ExtractExpr として内部的に名前が付けられ、表示名として "Extract Expression" を使用し、サービス通信の目的で "extract.expr" の安定キーが使用されることを宣言します。
このリファクタリングが利用可能なときを Xcode に教えるには、Refactoring.cpp でこのリファクタリングに isApplicable をも実装する必要があります。ただし、入力が ResolvedCursorInfo ではなく ResolvedRangeInfo であるというわずかな違いがあります。
1 bool RefactoringActionExtractExpr:: 2 isApplicable(ResolvedRangeInfo Info) { 3 if (Info.Kind != RangeKind::SingleExpression) 4 return false; 5 auto Ty = Info.getType(); 6 if (Ty.isNull() || Ty.hasError()) 7 return false; 8 ... 9 return true; 10 }
前述の String Localization リファクタリングのそれよりも少し複雑ですが、この実装も自己説明的です。3 行目から 4 行目では、与えられた範囲の種類がチェックされ、これは抽出を進めるための単一の式でなければなりません。5 行目から 7 行目では、抽出された式の形式が正しいことを確認します。今の例では、チェックする必要があるさらなる条件は省略されています。関心のある読者は、Refactoring.cpp を参照してください。コード変更部分では、同じ ResolvedRangeInfo インスタンスを使用してテキスト編集を行うことができます。
1 bool RefactoringActionExtractExprBase::performChange() { 2 llvm::SmallString<64> DeclBuffer; 3 llvm::raw_svector_ostream OS(DeclBuffer); 4 OS << tok::kw_let << " "; 5 OS << PreferredName; 6 OS << TyBuffer.str() << " = " << RangeInfo.ContentRange.str() << "\n"; 7 Expr *E = RangeInfo.ContainedNodes[0].get<Expr*>(); 8 EditConsumer.insert(SM, InsertLoc, DeclBuffer.str()); 9 EditConsumer.insert(SM, 10 Lexer::getCharSourceRangeFromSourceRange(SM, E->getSourceRange()), 11 PreferredName) 12 return false; // Return true if code change aborted. 13 }
2 行目から 6 行目は、抽出中の式、すなわち、letExpr = foo() の初期化された値を持つローカル変数の宣言を構成します。8 行目は宣言をローカルコンテキスト内の適切なソース位置に挿入し、9 行目は式のオリジナルの発生を新しく宣言された変数への参照で置き換えます。コード例で示したように、performChange の関数本体では、ユーザーが選択したオリジナルの ResolvedRangeInfo だけでなく、編集コンシューマーやソースマネージャーなどの重要なユーティリティーにもアクセスできるため、実装がより便利になります。
診断
さまざまな理由で自動コード変更中にリファクタリングアクションを中止する必要がある場合があります。これが起こると、リファクタリング実装は、診断を介して、そのような障害の原因をユーザに伝えることができます。リファクタリング診断は、コンパイラ自体と同じメカニズムを採用しています。名前変更リファクタリングを例にとると、指定された新しい名前が無効な Swift 識別子である場合、エラーメッセージを発行したいと考えるでしょう。これを行うには、最初に DiagnosticsRefactoring.def で以下の診断用のエントリを宣言する必要があります。
宣言後、isApplicable または performChange で診断を使用できます。Local Rename リファクタリングの場合 Refactoring.cpp で診断を発行すると、以下のようになります。
1 bool RefactoringActionLocalRename::performChange() {
...
2 if (!DeclNameViewer(PreferredName).isValid()) {
3 DiagEngine.diagnose(SourceLoc(), diag::invalid_name, PreferredName);
4 return true; // Return true if code change aborted.
5 }
...
6 }
テスト
新しいリファクタリングアクションを実装する 2 つのステップに対応して、以下のことをテストする必要があります。
- 文脈上利用可能なリファクタリングは適切に形成されます。
- 自動コード変更により、ユーザーのコードベースが正しく更新されます。
これらの 2 つの部分は、両方ともコンパイラの横にビルドされた swift-refactor コマンドラインユーティリティを使用してテストされます。
文脈上のリファクタリングテスト
1 func foo() { 2 print("Hello World!") 3 } 4 // RUN: %refactor -source-filename %s -pos=2:14 | %FileCheck %s -check-prefix=CHECK-LOCALIZE-STRING 5 // CHECK-LOCALIZE-STRING: Localize String
例として、String Localization をもう一度取りあげてみましょう。上記のコードスニペットは、文脈上のリファクタリングアクションのテストです。同様のテストは、test/refactoring/RefactoringKind/ にあります。
RUN 行をより詳しく見て、%refactor ユーティリティを使い始めましょう:
この行は、ユーザーがカーソルを文字列リテラル "Hello World!" に向けて、適用可能なすべてのリファクタリングの表示名をダンプします。%refactor は、テストが実行されたときに swift-refactor へのフルパスを与えるためにテストランナーに代入されるエイリアスです。-pos は、文脈上のリファクタリングアクションを引き出すカーソル位置を指定します。String Localization リファクタリングはカーソルベースなので、-pos だけで十分です。レンジベースのリファクタリングをテストするには、リファクタリングターゲットの終了位置も指定するために -end-pos を指定する必要があります。すべての位置は、line:column の形式です。
ツールの出力が期待どおりであることを確認するために、%FileCheck ユーティリティを使用します:
%FileCheck %s -check-prefix=CHECK-LOCALIZE-STRING
これは、CHECK-LOCALIZE-STRING を前に付けたすべての行に対して %refactor からの出力テキストをチェックします。この場合、使用可能なリファクタリングに Localized String が含まれているかどうかがチェックされます。正しいアクションを正しいカーソル位置に表示することをテストすることに加えて、補間を伴う文字列リテラルのような状況では、利用可能なリファクタリングが誤って埋め込まれていないかテストする必要もあります。
コード変換テスト
また、リファクタリングを適用する際に、自動コード変更が期待通りのものであることをテストする必要もあります。準備として、リファクタリングの種類のフラグを swift-refactor に教えて、テストするアクションを指定する必要があります。これを実現するには、swift-refactor.cpp に以下のエントリが追加されます。
clEnumValN(RefactoringKind::LocalizeString, "localize-string", "Perform String Localization refactoring"),
このようなエントリを使用すると、swift-refactor は String Localization のコード変換部分を具体的にテストできます。典型的なコード変換テストは、2 つの部分で構成されています。
- リファクタリング前のコードスニペット。
- 変換後の期待した出力。
テストは (1) で指定されたリファクタリングを実行し、結果を (2) と比較します。2 つが同一であれば合格し、そうでなければテストは失敗します。
1 func foo() { 2 print("Hello World!") 3 } 4 // RUN: rm -rf %t.result && mkdir -p %t.result 5 // RUN: %refactor -localize-string -source-filename %s -pos=2:14 > %t.result/localized.swift 6 // RUN: diff -u %S/Iutputs/localized.swift.expected %t.result/localized.swift 1 func foo() { 2 print(NSLocalizedString("Hello World!", comment: "")) 3 }
上記の 2 つのコードスニペットは、意味のあるコード変換テストを構成します。 4 行目は、リファクタリングの結果としてのコードの一時的なソースディレクトリを用意します。新しく追加された -localize-string を使用して、5 行目は "Hello World!" の開始位置でリファクタリングコードの変更を実行し、結果を一時ディレクトリにダンプします。最後に、6 行目は、結果を第 2 のコード例に示す期待された出力と比較します。
Xcode との統合
上記のすべてを Swift コードベースで実装した後は、ローカルにビルドしたオープンソースのツールチェーンと統合して、Xcode で新しく追加されたリファクタリングをテスト及び使用する準備が整いました。
- build-toolchain を実行して、オープンソースのツールチェーンをローカルにビルドします。
- toolchain を /Library/Developer/Toolchains/ に展開してコピーします。
- 以下の図に示すように、Xcode->Toolchains から Xcode を使用するためのローカルツールチェインを指定します。
潜在的なローカルリファクタリングの考え方
この記事は、新しいリファクタリングエンジンで実装できるようになったことの一部に触れるだけです。追加の変換を実装するためにリファクタリングエンジンを拡張することにあなたが興奮している場合、Swift の 問題データベース には、実装を待っている リファクタリング変換のアイデアがいくつか 含まれています。新しいリファクタリングのアイデアを提案したい場合は、 Swift の 問題データベース で Refactoring というラベルの付けて作業を行うだけで十分です。
リファクタリングの変換を実装する上でさらに役立つ情報は、マニュアル を参照するか、swift-dev メーリングリストで自由に質問してください。
Swift 4.0 がリリースされた!->