swift4
リファ

Swiftローカルリファクタリング(Swift Local Refactoring)

Original Link
by Xi Ge
translated Korean by pilgwon

Xcode 9は新しいリファクタリングエンジンが持っています。これは一つのSwiftソースファイルでローカルまたはグローバルにコードの変更が可能です。例えば、色んなファイルの名前のみに関わらず、他の言語のメソッドまたは属性の名前まで変更するなどの作業が可能です。ローカルリファクタリングのロジックはコンパイラートSourceKitですべて実現ができるようになっています。SourceKitは現在Swiftレポジトリでオープンソースになっています。もし興味があるならリファクタリングにcontribution(貢献)も可能です。この記事はリファクタリングがXcodeでいかに簡単に実現できるかを説明します。

リファクタリングの種類

ローカルリファクタリングは単一ファイルで行われます。ローカルリファクタリングサンプルは メソッド抽出(Extract Method)Extract Repeated Expression(繰り返し表現抽出)を含めます。数多くのファイルをアクロス横切ってコードの変更を行う グローバルリファクタリングみたいな方法は(例えばグローバル変数変更(Global Rename)) Xcodeの特別な調整が必要になり、現在のSwiftコードのみでは実現ができません。この記事ではそれらの(XCode) 範囲内で結構強いローカルリファクタリングを紹介します。

リファクタリングアクションはEditorでユーザーのにカーソルより初期化されます。初期化する方法により、我々はリファクタリングアクションをカーソルベース(cursor-based)와と範囲ベース(range-based)に分かれます。 カーソルベースリファクタリングは名前変更リファクタリングと同様のSwiftソースファイルのカーソルで指定されたリファクタリング対象が存在します。それに比べて範囲ベースリファクタリングはメソッド抽出リファクタリングみたいに
ターゲットの開始と終了が必要になります。この二つのカテゴリの実現を容易にするためにSwiftレポジトリはソースファイルでのカーソル位置と範囲に関連する様々の質問にすでに分析されている結果であるResolvedCursorInfoResolvedRangeInfoを提供します。

例えば、ResolvedCursorInfoはソースファイルの位置が表現式の開始を表すのかを知らせてくれて、もしそうだとすれば該当表現式を表すコンパイラーオブジエクトを提供します。またはカーソル位置が名前を表しているとResolvedCursorInfoはその名前に該当する宣言を提供します。似たように、 ResolvedRangeInfoは範囲内に様々な項目及び終了点があるのかと与えられたソースの範囲に関する情報をカプセル化します。

Swiftの新たなリファクタリングを実装する時、我々は一から始める必要はありません。ResolvedCursorInfoResolvedRangeInfoで始められ、それに起因したリファクタリングに特化された分析を出すことができます。

カーソルベースリファクタリング

Cursor.png

カーソルベースリファクタリングはSwiftソースファイルのカーソル位置により初期化されます。リファクタリングアクションはリファクタリングエンジンが利用するメソッドをIDEで利用可能なアクションを見せてくれるか変形を与えるために実装します。

具体的には、利用可能なアクションを取得するために:

  1. ユーザーがXcode Editorで位置を選択します。
  2. Xcodeがその位置である利用可能なリファクタリングアクションがあるか確認するためsourcekitdにリクエストを行います。
  3. 実現された各リファクタリングアクションはアクションがその位置で応用が可能かどうかを ResolvedCursorInfo オブジェクトに確認します。
  4. 応用可能なアクションのリストが sourcekitdのリスポンスとして返却されXcodeによりユーザーに見えます。

ユーザーが利用可能なアクションを選択すると:

  1. Xcodeは sourcekitdに選べられたアクションを行うためにリクエストします。
  2. 特定のリファクタリングアクションは同一の位置で派生された ResolvedCursorInfo オブジェクトに問われ、該当アクションが適用可能かを確認します。
  3. リファクタリングアクションはテキストソース編集で変換を行えるようリクエストされます。
  4. ソースの編集は sourcekitdのレスポンスで返却されXcode Editorにより適用されます。

文字列のローカライズ(String Localization) リファクタリングを実現するためにはこのリファクタリングを RefactoringKinds.def ファイルの導入部分に以下の通りに宣言する必要があります:

CURSOR_REFACTORING(LocalizeString, "Localize String", localize.string)

CURSOR_REFACTORINGはこのリファクタリングがカーソル位置によって初期化されて、実現する時にResolvedCursorInfoを利用することを表します。最初のフィルドである LocalizeStringはSwiftコードベースのこのリファクタリング内部で使う名前を表します。このサンプルでこのリファクタリングに対応するクラスの名前は RefactoringActionLocalizeStringになります。 "Localize String"という文字列はこのリファクタリングのUIでユーザーに見える名前になります。最後に、“localize.string”はSwiftツールチェーンにソースエディターとの通信に利用されるリファクタリングアクションを職別できる安定するキーであります。また、この項目を利用することで C++コンパイラー内部で String Localization及び該当呼び出し元に関するクラススタブを生成することができます。それによって我々は必要な機能の実現に集中することができます。

その後、Xcodeに認識させるため二つの関数を実装する必要があります:

  1. いつがリファクタリングアクションを見せる適切な時期なのか。
  2. ユーザーがリファクタリングアクションを取る時にどのコードの変更を行うべきか。

上記二つ宣言は前に言った項目により自動で生成されます。1を行うためには Refactoring.cppにあるRefactoringActionLocalizeStringの関数であるisApplicableを以下の通りに実装する必要があります。:

bool RefactoringActionLocalizeString::
  isApplicable(ResolvedCursorInfo CursorInfo) {
    if (CursorInfo.Kind == CursorInfoKind::ExprStart) {
      if (auto *Literal = dyn_cast<StringLiteralExpr>(CursorInfo.TrailingExpr) {
        return !Literal->hasInterpolation(); // Not real API.
      }
    }
  }

ResolvedCursorInfo オブジェクトを入力として利用することで、利用可能なリファクタリングメニューに “localize string”をいつ貼り付けるかをチェックすることができるようになります。この場合、カーソルポイントが表現式の開始点を表すのか(Line 3)と表現式が Interpolation(Line 5)がないリテラル文字列か(Line 4)をチェックします。

その次、カーソルの下にあるコードがリファクタリングアクションが適用された時にどう変わっていくべきか実装する必要があります。そのためにはRefactoringActionLocalizeStringのメソッドであるperformChangeを実装する必要があります。performChange の実装中、 isApplicableを取得した ResolvedCursorInfoオブジエクトをコールすることができます。

bool RefactoringActionLocalizeString::
  performChange() {
    EditConsumer.insert(SM, Cursor.TrailingExpr->getStartLoc(), "NSLocalizedString(");
    EditConsumer.insertAfter(SM, Cursor.TrailingExpr->getEndLoc(), ", comment: \"\")");
    return false; // Return true if code change aborted.
  }

String Localizationをそのままサンプルに利用して、performChange関数を実装することは簡単です。関数の本文に Line 3と Line 4に出ている通りEditConsumerを利用して適切なFoundation APIのコールでカーソルが表す表現式をテキストで編集することができます。

範囲ベースリファクタリング

Range.png

上のイメージの通り、範囲ベースリファクタリングはSwiftソースファイルの連続で選択された範囲により初期化されます。抽出表現式(Extract Expression)の実装を例えとすると先にRefactoringKinds.defに以下のように定義する必要があります。

RANGE_REFACTORING(ExtractExpr, "Extract Expression", extract.expr)

この項目は Extract Expression リファクタリングが選択範囲により初期化され、基本的に ExtractExpr 名前が設定されていて、"抽出表現式"を見せる名前に利用され、サービスコミュニケーション目的で “extract.expr” キーを利用することを意味する定義です。

このリファクタリングが利用可能になったことを Xcodeに知らせるためには Refactoring.cppのこのリファクタリングのために isApplicableを実装する必要があり、ちょっと違うとこがあるとしたら ResolvedCursorInfo の代わりに ResolvedRangeInfoを入力として使うことです。

bool RefactoringActionExtractExpr::
  isApplicable(ResolvedRangeInfo Info) {
    if (Info.Kind != RangeKind::SingleExpression)
      return false;
    auto Ty = Info.getType();
    if (Ty.isNull() || Ty.hasError())
      return false;
    ...
    return true;
  }

例えば前に記述された String Localization リファクタリングよりちょっと複雑ですがこの実装方法は説明がなくても明白は実装方法にもなります。ライン3から4に与えられた範囲の種類が抽出を進めるための単一表現式かを確認します。ライン5から7は抽出された表現式がよく使われるタイプかを確認します。加えて確認する必要がある条件はこのサンプルでは省略しています。興味がある方は Refactoring.cppをみていただくともっと詳しい情報があります。コード編集でテキスト編集をする代わりに同様の ResolvedRangeInfo インスタンスを利用できます:

bool RefactoringActionExtractExprBase::performChange() {
    llvm::SmallString<64> DeclBuffer;
    llvm::raw_svector_ostream OS(DeclBuffer);
    OS << tok::kw_let << " ";
    OS << PreferredName;
    OS << TyBuffer.str() <<  " = " << RangeInfo.ContentRange.str() << "\n";
    Expr *E = RangeInfo.ContainedNodes[0].get<Expr*>();
    EditConsumer.insert(SM, InsertLoc, DeclBuffer.str());
    EditConsumer.insert(SM,
                       Lexer::getCharSourceRangeFromSourceRange(SM, E->getSourceRange()),
                       PreferredName)
  return false; // コードの変更がキャンセルされたら trueを返却します。
}

ライン2から6は抽出中の表現式の初期化された値を利用して地域変数を構成します。 (예시: let extractedExpr = foo())。ライン8はローカルコンテキストの適切な位置に宣言を加えてライン 9は表現式の元の模様を新たに宣言した変数への参照にて変更します。コードサンプルで見たようにperformChange関数内でユーザーの選択のため元々のResolvedRangeInfoに関わらずedit consumerと source managerみたいな重要なユーティリティにもアクセスできることで実装がもっと楽になります。

診断

リファクタリングアクションは様々な理由で自動でコード変更されるうちにキャンセルになる時があります。この時にはリファクタリングの実装はユーザーに失敗理由を診断させることができます。リファクタリング診断はコンパイラー自身と同様のメカニズムを利用します。名前変更リファクタリングを例にすると、与えられた名前の変更が不可能なSwift職別者としたらエラーメッセージが返却されます。そのためには先に診断のための項目をDiagnosticsRefactoring.defに宣言する必要があります。

ERROR(invalid_name, none, "'%0' is not a valid name", (StringRef))

宣言後の診断をisApplicableperformChangeで利用可能です。ローカル名前変更リファクタリングの場合、 Refactoring.cppの診断は以下となります:

bool RefactoringActionLocalRename::performChange() {
  ...
    if (!DeclNameViewer(PreferredName).isValid()) {
      DiagEngine.diagnose(SourceLoc(), diag::invalid_name, PreferredName);
      return true; // コードの変更がキャンセルされたら trueを返却します。
    }
  ...
  }

テスティング

新たなリファクタリングアクションを実装するため、以下のような内容をテストします:

  1. 文脈上、利用可能なリファクタリングが適切に行われる必要があります。
  2. 自動コード変更はユーザーのコードを正しくアップデートする必要があります。

上記2箇所はコンパイラーによりビルドされた swift-refactor コマンドラインユーティリティーを利用してテストが行われます。

文脈リファクタリングテスト

  func foo() {
    print("Hello World!")
  }
  // RUN: %refactor -source-filename %s -pos=2:14 | %FileCheck %s -check-prefix=CHECK-LOCALIZE-STRING
  // CHECK-LOCALIZE-STRING: Localize String

もう一度文字列ローカライズを例と上げてみます。 上記のコードスニペットは文脈リファクタリングアクションのためのテストです。似たようなテストを test/refactoring/RefactoringKind/でご確認できます。

もう **RUN** ラインを詳しく見てみましょう。 %refactor ユーティリティーの使い方から見てみます:

%refactor -source-filename %s -pos=2:14 | %FileCheck %s -check-prefix=CHECK-LOCALIZE-STRING

このラインはユーザーが文字列リテラルである “Hello World!”にカーソルを置いた時に利用可能なすべてのリファクタリングの名前を重ねておきます。%refactorはテストが実行される時 swift-refactorに全体パスを与えるためのテスト実行者により入れ替わるエイリアス(alias)です。 -posは文脈リファクタリングアクションをどこから持ってくるかカーソルの位置を提供します。String Localization リファクタリングはカーソルベースのため、 -posを一つだけ設定することで十分です。範囲ベースリファクタリングをテストするために、リファクタリングターゲントの最後の位置を表すための -end-posを明示する必要があります。また、すべての位置は line:columnのフォーマットを持つ必要があります。

結果値が予想通りのものかどうかを確認するため、 %FileCheck ユーティリティーを利用します:

%FileCheck %s -check-prefix=CHECK-LOCALIZE-STRING

これはprefixCHECK-LOCALIZE-STRINGを持った全ての次の行について %refactorの出力テストをチェックします。この場合、利用可能なリファクタリングが Localize Stringを含んでいるかをチェックします。正しいカーソル位置に正しいアクションが現れているかテストするため文字列リテラルと同様な状況に利用可能なリファクタリングが間違って表示されてはいないかテストする必要があります。

コード変形テスト

リファクタリングを適用する時にも自動コード変更が予想と一致するのかをテストする必要があります。そのためにswift-refactorにテスト中のアクションがどのようなものかをわかるためのリファクタリング種類フラグを教える必要があります。そのためには以下の項目が swift-refactor.cppに追加されます:

clEnumValN(RefactoringKind::LocalizeString, "localize-string", "Perform String Localization refactoring"),

次と同じ項目で swift-refactorは特に文字列のローカライズのようなコード変形のテストが可能です。代表的なコード変形テストは2箇所に分けられています:

  1. リファクタリング前のコードスニペット
  2. 変形後予想されるアウトプット

テストは指定したリファクタリングを (1)に適用し、その結果を (2)と比較します。この二つが同一であればテスト通過であり、出ないと失敗です。

func foo() {
  print("Hello World!")
}
// RUN: rm -rf %t.result && mkdir -p %t.result
// RUN: %refactor -localize-string -source-filename %s -pos=2:14 > %t.result/localized.swift
// RUN: diff -u %S/Iutputs/localized.swift.expected %t.result/localized.swift
func foo() {
  print(NSLocalizedString("Hello World!", comment: ""))
}

上記2個のコードスニペットは意味あるコード変形テストで構成されています。ライン4はリファクタリング結果を臨時的に保存する空間を用意します。ライン5では新たに追加された -localize-stringを利用して "Hello World!"のスタート部分からコード変更を行い、結果の値を臨時に保存します。最後に6は2個目コードサンプルで書かれた予想出力と結果を比較します。

Xcodeに貼り付ける

上記すべての部分のSwiftコードでの実装が終わった時点で、Xcodeにローカルビルドされたオープンソースツールチェーンにより新たに追加されたリファクタリングをテストするか利用する準備が終わりました。

  1. オープンソースツールチェーンをローカルでビルドするため build-toolchainを実行します。
  2. ツールチェーンの圧縮を溶けて Library/Developer/Toolchainsにコピーします。
  3. 以下のイメージのようにXcode -> Toolchainsを通してローカルツールチェーンを指定します。

Toolchain.png

 潜在的なローカルリファクタリングのIDEA

この記事は新たなリファクタリングエンジンで実現できるいくつかをちょっと触ってみただけです。もしリファクタリングエンジンを追加的に変形のため拡張することに興味があるのであればSwiftの Issue Databaseで実現して欲しい リファクタリング変形に関するいくつのIDEAがあります。もし新たなリファクタリングIDEAの提案があるのであればSwiftの Issue DatabaseにコミットしRefactoringとラベルを付けるだけです。

リファクタリング変形を実現することに詳しい情報が必要な方ドキュメントを見ていただき swift-dev メーリングリストに質問してください。