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

[Original Link](https://swift.org/blog/swift-local-refactoring/)
by [Xi Ge](https://twitter.com/xge_apple/)
translated Korean by [pilgwon](https://pilgwon.github.io)

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レポジトリはソースファイルでのカーソル位置と範囲に関連する様々の質問にすでに分析されている結果である[ResolvedCursorInfo](https://github.com/apple/swift/blob/7f29b362d68eb990a592257850aabadb24de61df/include/swift/IDE/Utils.h#L158)と[ResolvedRangeInfo](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/include/swift/IDE/Utils.h#L344)を提供します。

例えば、[ResolvedCursorInfo](https://github.com/apple/swift/blob/7f29b362d68eb990a592257850aabadb24de61df/include/swift/IDE/Utils.h#L158)はソースファイルの位置が表現式の開始を表すのかを知らせてくれて、もしそうだとすれば該当表現式を表すコンパイラーオブジエクトを提供します。またはカーソル位置が名前を表していると[ResolvedCursorInfo](https://github.com/apple/swift/blob/7f29b362d68eb990a592257850aabadb24de61df/include/swift/IDE/Utils.h#L158)はその名前に該当する宣言を提供します。似たように、 [ResolvedRangeInfo](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/include/swift/IDE/Utils.h#L344)は範囲内に様々な項目及び終了点があるのかと与えられたソースの範囲に関する情報をカプセル化します。

Swiftの新たなリファクタリングを実装する時、我々は一から始める必要はありません。[ResolvedCursorInfo](https://github.com/apple/swift/blob/7f29b362d68eb990a592257850aabadb24de61df/include/swift/IDE/Utils.h#L158)と [ResolvedRangeInfo](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/include/swift/IDE/Utils.h#L344)で始められ、それに起因したリファクタリングに特化された分析を出すことができます。

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

<img width="300" alt="Cursor.png" src="https://qiita-image-store.s3.amazonaws.com/0/100829/0a561539-fe59-ae8e-3c86-2c8e357d4b24.png">

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

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

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

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

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

*文字列のローカライズ(String Localization)* リファクタリングを実現するためにはこのリファクタリングを [RefactoringKinds.def](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/include/swift/IDE/RefactoringKinds.def) ファイルの導入部分に以下の通りに宣言する必要があります:

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

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

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

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

上記二つ宣言は前に言った項目により自動で生成されます。1を行うためには [Refactoring.cpp](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp)にある```RefactoringActionLocalizeString```の関数である[isApplicable](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp#L646)を以下の通りに実装する必要があります。:

```cpp
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](https://github.com/apple/swift/blob/7f29b362d68eb990a592257850aabadb24de61df/include/swift/IDE/Utils.h#L158) オブジェクトを入力として利用することで、利用可能なリファクタリングメニューに “localize string”をいつ貼り付けるかをチェックすることができるようになります。この場合、カーソルポイントが表現式の開始点を表すのか(Line 3)と表現式が Interpolation(Line 5)がないリテラル文字列か(Line 4)をチェックします。

その次、カーソルの下にあるコードがリファクタリングアクションが適用された時にどう変わっていくべきか実装する必要があります。そのためには```RefactoringActionLocalizeString```のメソッドである[performChange](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp#L599)を実装する必要があります。```performChange``` の実装中、 [isApplicable](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp#L646)を取得した ```ResolvedCursorInfo```オブジエクトをコールすることができます。

```cpp
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](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp#L599)関数を実装することは簡単です。関数の本文に Line 3と Line 4に出ている通り[EditConsumer](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/include/swift/IDE/Utils.h#L506)を利用して適切なFoundation APIのコールでカーソルが表す表現式をテキストで編集することができます。

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

<img width="300" alt="Range.png" src="https://qiita-image-store.s3.amazonaws.com/0/100829/fbddf8c1-3654-c92d-3bd8-55b316b472be.png">

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

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

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

このリファクタリングが利用可能になったことを Xcodeに知らせるためには [Refactoring.cpp](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp)のこのリファクタリングのために [isApplicable](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp#L646)を実装する必要があり、ちょっと違うとこがあるとしたら [ResolvedCursorInfo](https://github.com/apple/swift/blob/7f29b362d68eb990a592257850aabadb24de61df/include/swift/IDE/Utils.h#L158) の代わりに [ResolvedRangeInfo](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/include/swift/IDE/Utils.h#L344)を入力として使うことです。

```cpp
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](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp)をみていただくともっと詳しい情報があります。コード編集でテキスト編集をする代わりに同様の [ResolvedRangeInfo](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/include/swift/IDE/Utils.h#L344) インスタンスを利用できます:

```cpp
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](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp#L599)関数内でユーザーの選択のため元々の[ResolvedRangeInfo](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/include/swift/IDE/Utils.h#L344)に関わらずedit consumerと source managerみたいな重要なユーティリティにもアクセスできることで実装がもっと楽になります。

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

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

宣言後の診断を[isApplicable](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp#L646)と[performChange](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp#L599)で利用可能です。ローカル名前変更リファクタリングの場合、 [Refactoring.cpp](https://github.com/apple/swift/blob/60a91bb7360dde5ce9531889e0ed10a2edbc961a/lib/IDE/Refactoring.cpp)の診断は以下となります:

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

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

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

上記２箇所はコンパイラーによりビルドされた [swift-refactor](https://github.com/apple/swift/tree/60a91bb7360dde5ce9531889e0ed10a2edbc961a/tools/swift-refactor) コマンドラインユーティリティーを利用してテストが行われます。


##文脈リファクタリングテスト
```cpp
  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/](https://github.com/apple/swift/tree/master/test/refactoring/RefactoringKind)でご確認できます。

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

```cpp
%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``` ユーティリティーを利用します:

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

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

##コード変形テスト

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

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

次と同じ項目で [swift-refactor](https://github.com/apple/swift/tree/60a91bb7360dde5ce9531889e0ed10a2edbc961a/tools/swift-refactor)は特に文字列のローカライズのようなコード変形のテストが可能です。代表的なコード変形テストは２箇所に分けられています:

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

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

```cpp

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
```

```cpp
func foo() {
  print(NSLocalizedString("Hello World!", comment: ""))
}
```

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

##Xcodeに貼り付ける

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

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

<img width="300" alt="Toolchain.png" src="https://qiita-image-store.s3.amazonaws.com/0/100829/6d3d2273-405b-21ed-440b-0e29ecd1aa59.png">

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

この記事は新たなリファクタリングエンジンで実現できるいくつかをちょっと触ってみただけです。もしリファクタリングエンジンを追加的に変形のため拡張することに興味があるのであればSwiftの [Issue Database](https://bugs.swift.org/)で実現して欲しい [リファクタリング変形に関するいくつのIDEA](https://bugs.swift.org/issues/?jql=labels%3DStarterProposal%20AND%20labels%3DRefactoring%20AND%20resolution%3DUnresolved)があります。もし新たなリファクタリングIDEAの提案があるのであればSwiftの [Issue Database](https://bugs.swift.org/)にコミットし```Refactoring```とラベルを付けるだけです。

リファクタリング変形を実現することに詳しい情報が必要な方[ドキュメント](https://github.com/apple/swift/blob/master/docs/refactoring/SwiftLocalRefactoring.md)を見ていただき [swift-dev](https://lists.swift.org/mailman/listinfo/swift-dev) メーリングリストに質問してください。