TypeScript Compiler APIによるリネーム実装メモ
TypeScript上のリネーム処理は正規表現でゴリ押しするよりはTypeScript Compiler APIを用いた方が正確にリネームできそう。
TypeScript Compiler API参考資料
symbol.getDeclarations()でリネームに必要なSymbolの情報が手に入るみたい[1]。 NTypeScriptというTypeScriptのLanguage Serviceを簡易的に実装したライブラリも参考になりそう。 TypeScriptのLanguage Service(services.ts)も同様の処理が書いてある
RenameInfoの型はこのように定義されている[2] code:ts
export type RenameInfo = RenameInfoSuccess | RenameInfoFailure;
export interface RenameInfoSuccess {
canRename: true;
/**
* File or directory to rename.
* If set, getEditsForFileRename should be called instead of findRenameLocations.
*/
fileToRename?: string;
displayName: string;
fullDisplayName: string;
kind: ScriptElementKind;
kindModifiers: string;
triggerSpan: TextSpan;
}
export interface RenameInfoFailure {
canRename: false;
localizedErrorMessage: string;
}
RenameInfoでは対象がリネーム可能かどうかの情報とリネーム対象をもつファイル名、TextSpanというリネーム対象の開始位置と文字列の長さを持つ情報を得ることができる。
もう一つRenameLocationという型もある。RenameLocationは対象のファイル名と位置情報を持ち、findRenameLocationsメソッドにより複数のリネーム対象を探してくることができる。
RenameLocationの型定義 -> [3] こちらはリネーム対象を参照している他のファイルの変更に使うことができそう。
findRenameLocationsの実装を見るとFindAllReferencesを参照しているのでそこから参照箇所の位置とファイル名を取得できそう
services/findAllReferences.ts -> getReferencedSymbolsForSymbol(764行目)
一方でschematicsでは書き換え時の変更はReplaceChangeに格納され、ReplaceChangeのapplyメソッドにより変更が適用される。 code:ts
/**
* Will replace text from the source code.
*/
export class ReplaceChange implements Change {
order: number;
description: string;
constructor(public path: string, private pos: number, private oldText: string,
private newText: string) {
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = Replaced ${oldText} into position ${pos} of ${path} with ${newText};
this.order = pos;
}
apply(host: Host): Promise<void> {
return host.read(this.path).then(content => {
const prefix = content.substring(0, this.pos);
const suffix = content.substring(this.pos + this.oldText.length);
const text = content.substring(this.pos, this.pos + this.oldText.length);
if (text !== this.oldText) {
return Promise.reject(new Error(Invalid replace: "${text}" != "${this.oldText}".));
}
// TODO: throw error if oldText doesn't match removed string.
return host.write(this.path, ${prefix}${this.newText}${suffix});
});
}
}
ReplaceChangeには書き換え先のpathと書き換え元のpos、書き換え元のテキストoldTextと書き換え後のテキストnewTextが必要。
実際の変更はapplyメソッドで行われる。書き換えは単純にファイル内容の文字列のpos~oldText.lengthの部分文字列(要するにoldTextに該当する文字列)をnewTextに置き換えるように変更している。
リネームを実装するためにはリネーム対象を参照する箇所のファイル名とファイル上の位置を知る必要がある(その情報があればschematicsのようにテキストを直接書き換えてリネームを実行することができる)。
FindAllReferencesのコードはライブラリとして提供されていないので、tsserverのプロセスを立ち上げて通信して利用するか、同様のロジックを自ら実装する必要がある。
ただし、FindAllReferencesのロジック全てが常に必要になるとは限らない(例えばクラス名のリネームだけを考えたいのであれば、いくつかのユーティリティ関数は簡略化できる)。
必要なロジックとそうでないものを分けるにはFindAllReferences内のロジックを全て正確に説明できなければならない。