FindAllReferencesコードリーディング
1. getReferencedSymolsForNode関数
このCoreネームスペース及びこの関数のコメントを読むと、この関数はFindAllReferencesの主要なロジックを扱っており、さらに内部の詳細なコアロジックはgetReferencedSymbolsForSymbol関数に委譲され、この関数の責務は委譲する前の特別なケースにおける前処理を扱うものとされている。
まずはこの関数の引数と戻り値について見ていく。
code:ts
function getReferencedSymbolsForNode(
position: number,
node: Node,
program: Program,
sourceFiles: readonly SourceFile[],
cancellationToken: CancellationToken,
options: Options = {},
sourceFilesSet: ReadonlyMap<true> = arrayToSet(sourceFiles, f => f.fileName)
): readonly SymbolAndEntries[] | undefined {
まずは引数から
position: number: これはファイル上のカーソル位置を表す数値で0からファイルの終端までの値が入りうる。
program: Program: ProgramはTypeScript Compiler APIにおいて1つのコンパイル単位を表すもの。複数のソースファイルとTypeScriptのコンパイルオプションを持つ。program.getTypeChecker()によりTypeScriptの型にまつわる情報を得るために使われるType Checkerを取得することができる。その他便利な情報を提供することができる。Compiler APIではts.createProgram(fileNames, options)でProgramインスタンスを生成することができる。
options: Options = {}: Optionsの定義は以下の通り(コメントは省略しているのでコメントを見たい場合はリンク先参照) code:ts
export interface Options {
readonly findInStrings?: boolean;
readonly findInComments?: boolean;
readonly use?: FindReferencesUse;
readonly isForRename?: boolean;
readonly implementations?: boolean;
readonly providePrefixAndSuffixTextForRename?: boolean;
}
findInStringsやfindInCommentsは文字列リテラル内やコメント内も探索対象に含めるかどうかを示す場合に設定する。
isForRenameはリネームの際、リネーム対象の参照先を探すfindRenameLocations関数から呼ばれる際にtrueになる。
異なる名前で参照されている場合(具体的にはどんな場合?)、オリジナルの名前による参照だけ見るのでより少ない探索で済むみたい↓
If so, we will find fewer references -- if it is referenced by several different names, we still only find references for the original name.
providePrefixAndSuffixTextForRenameはUserPreferencesでprovidePrefixAndSuffixTextForRenameオプションを有効にした(例えばVSCodeならsettings.jsonで"typescript.preferences.providePrefixAndSuffixTextForRename": trueにする)場合に有効になるオプション。 このオプションはshorthand property ※1またはimport/export specifier ※2で参照している変数名をリネームする時に関わる。 例えばこのオプションが有効なとき、shorthand propertyのリネームでは以下のコードでTestのfooをfoo2にリネームしたとき
code: ts
interface Test {
foo: string
}
function bar(): Test {
const foo = 'foo'
return { foo }
}
return { foo }はreturn { foo2 }にならずにreturn { foo2: foo }になる。
import/export specifierもexport { x };はexport { y };とならずにexport { y as x };となる。
詳しくは該当Issueを参照
sourceFilesSet: ReadonlyMap<true> = arrayToSet(sourceFiles, f => f.fileName): sourceFilesetはsourceFilesをSetに変換したもの。ReadonlyMap<true>という型とarrayToSetという関数があるがこれはTypeScript内部で定義されて使われているもの。見慣れたMapの型Map<K, V>やReadonlyMap<K, V>と違い、このMapのジェネリクスは1つの型しか受け取らない。内部実装を見るとこのMapはMap<string, T>のように使われている。arrayToSetでReadonlyMap<true>に変換されるのはSetの実装もこのMapの実装をベースにしているため(というかSetという型を用意していない)。ReadonlyMap<true>はReadonlyMap<string, true>に等しいので、Mapのkeyのみを使ったSet的な表現と言える。つまりReadonlyMap<true>はTypeScriptから提供されているReadonlySet<T>のReadonlySet<string>と同じような使い方ができるということ。ちなみになぜTypeScript内部では独自のMapを定義して使っているかというとコメントを読む限りパフォーマンス上の理由がありそう。 戻り値
SymbolAndEntries[]] | undefined: SymbolAndEntriesは定義元の情報(definition: Definition | undefined)と参照先の情報(references: readonly Entry[])から構成される。 Definitionの型は以下のように定義される。
code:ts
export const enum DefinitionKind { Symbol, Label, Keyword, This, String }
export type Definition =
| { readonly type: DefinitionKind.Symbol; readonly symbol: Symbol }
| { readonly type: DefinitionKind.Label; readonly node: Identifier }
| { readonly type: DefinitionKind.Keyword; readonly node: Node }
| { readonly type: DefinitionKind.This; readonly node: Node }
| { readonly type: DefinitionKind.String; readonly node: StringLiteral };
Definitionは大きく分けてSymbol ※3かNode(もしくはNodeの派生型)に分類される。クラス名や変数名などSymbolを持つような条件ではSymbolが入る。その他例外的なパターンとして、LABEL: for(/*...*/) /*...*/ break LABEL;のようにbreakやcontinueで指定するラベルの場合や、anyやbooleanなどtypeKeywordsに定義されているプリミティブ型を含む型キーワードの場合、thisキーワードの場合、{ 'prop': /*...*/ }のようなプロパティとして扱える文字列リテラルの場合もDefinitionとして扱われる。 参照先の情報であるEntryの型は以下のように定義される。
code:ts
export type Entry = NodeEntry | SpanEntry;
export interface NodeEntry {
readonly kind: NodeEntryKind;
readonly node: Node;
readonly context?: ContextNode;
}
export interface SpanEntry {
readonly kind: EntryKind.Span;
readonly fileName: string;
readonly textSpan: TextSpan;
}
このEntryはNodeを持つ場合とそうでない場合(その場合はファイル名とファイル内の位置を情報に持つ)の参照先に分かれる。
ここからはgetReferencedSymbolsForNode関数の内部実装について順に見ていく。
code:ts
if (isSourceFile(node)) {
const reference = GoToDefinition.getReferenceAtPosition(node, position, program);
const moduleSymbol = reference && program.getTypeChecker().getMergedSymbol(reference.file.symbol);
return moduleSymbol && getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet);
}
この条件に当てはまるのはソースファイル全体を表すNode(つまりASTにおける最上位階層のNode)が渡されたとき。isSourceFileはSyntaxKindがSourceFileのときにnodeをSourceFileとして扱うことのできるType Guard関数。 GoTodefinition.getReferenceAtPosition関数に関してはまとめたこの記事を参照。
ifブロック内2行目ではTypeCheckerからgetMergedSymbolを呼び出している。mergedSymbolはTypeScriptのDeclaration Mergingに関連する概念。モジュールもnamespaceやinterfaceと同様、同じ名前のものが複数宣言されている場合に1つのものとして扱われる。そのため、GoToDefinition.getReferenceAtPositionで取得したsourceFile(モジュール)のsymbolは断片的なものである可能性がある※4ため、mergedSymobolが存在している場合にはそのsymbolを用いるようにする必要がある。 1.1. getReferencedSymbolsForModule関数
この関数の定義は以下の通り
code:ts
function getReferencedSymbolsForModule(
program: Program,
symbol: Symbol,/* moduleSymbol */
excludeImportTypeOfExportEquals: boolean, /* false */
sourceFiles: readonly SourceFile[],
sourceFilesSet: ReadonlyMap<true>
): SymbolAndEntries[] {
symbolには先程取得したmoduleSymbolが入り、excludeImportTypeOfExportEqualsというフラグにはfalseが指定されている。
最初のブロックではmapDefined関数によってイテレートされている。
code:ts
const references = mapDefined<ModuleReference, Entry>(
findModuleReferences(program, sourceFiles, symbol),
reference => { /* ... */ }
);
このmapDefinedはModuleReference[]型のリストをEntry[]に置換している。置換結果にundefinedが返る場合はリストから除外されるのでこのmapDefined関数は以下の実装に近い
code:ts
let toArray = [];
if (fromArray) {
toArray = fromArray.map(fromItem => { /* ... */ }).filter(fromItem => fromItem !== undefined);
}
mapDefinedの第一引数ではfindModuleReferences関数の戻り値を受け取っている。
1.1.1. findModuleReferences関数
引数と戻り値は以下の通り
code:ts
findModuleReferences(
program: Program,
sourceFiles: readonly SourceFile[],
searchModuleSymbol: Symbol
): ModuleReference[]
searchModuleSymbolは探索対象のモジュールのSymbolを受け取る。ModuleReferenceは
code:ts
type ModuleReference =
/** "import" also includes require() calls. */
| { kind: "import", literal: StringLiteralLike }
/** <reference path> or <reference types> */
| { kind: "reference", referencingFile: SourceFile, ref: FileReference };
のように定義されていて、ESModuleのimportの場合とTriple-Slash Directivesの場合でペアとなる値が異なる。
findModuleReferencesの全体は以下のようになっている
code:ts
export function findModuleReferences(program: Program, sourceFiles: readonly SourceFile[], searchModuleSymbol: Symbol): ModuleReference[] {
const refs: ModuleReference[] = [];
const checker = program.getTypeChecker();
for (const referencingFile of sourceFiles) {
const searchSourceFile = searchModuleSymbol.valueDeclaration;
/* 1. Triple-Slash Directivesから参照ファイルを探索 */
if (searchSourceFile.kind === SyntaxKind.SourceFile) {
/* 1.1. <reference path>の探索 */
for (const ref of referencingFile.referencedFiles) {
if (program.getSourceFileFromReference(referencingFile, ref) === searchSourceFile) {
refs.push({ kind: "reference", referencingFile, ref });
}
}
/* 1.2. <reference types>の探索 */
for (const ref of referencingFile.typeReferenceDirectives) {
const referenced = program.getResolvedTypeReferenceDirectives().get(ref.fileName);
if (referenced !== undefined && referenced.resolvedFileName === (searchSourceFile as SourceFile).fileName) {
refs.push({ kind: "reference", referencingFile, ref });
}
}
}
/* 2. import文から参照ファイルを探索 */
forEachImport(referencingFile, (_importDecl, moduleSpecifier) => {
const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier);
if (moduleSymbol === searchModuleSymbol) {
refs.push({ kind: "import", literal: moduleSpecifier });
}
});
}
return refs;
}
大まかにはsourceFilesを探索して参照しているファイルをModuleReferenceの配列に入れている。
code:ts
const refs: ModuleReference[] = [];
const checker = program.getTypeChecker();
for (const referencingFile of sourceFiles) {
/* 1. Triple-Slash Directivesから参照ファイルを探索 */
/* 2. import文から参照ファイルを探索 */
}
return refs;
最初のTriple-Slash Directivesからの探索は<reference path>の探索と<reference types>の探索に分かれている。
code:ts
const searchSourceFile = searchModuleSymbol.valueDeclaration;
if (searchSourceFile.kind === SyntaxKind.SourceFile) {
/* 1.1. <reference path>の探索 */
/* 1.2. <reference types>の探索 */
}
上記1行目でsymbolの持つDeclarationオブジェクトを取得している。DeclarationはNodeの派生型でありDeclarationの派生型にはSourceFileが含まれる。
SourceFile型はTriple-Slash Directivesの参照ファイルを保持するプロパティを持っているのでsymbolから取得したDeclarationがSourceFileのNodeであるかをチェックする必要がある。
SourceFileはreferencedFilesに<reference path="..." />の参照を、typeReferenceDirectivesに<reference type="..." />の参照を持っている。以下のforループでは対象モジュール(searchSourceFile)のファイルがTriple-Slash Directivesで指定されたファイルに等しいかを見ている。
code:ts
// for (const referencingFile of sourceFiles) {
/* 1.1. <reference path>の探索 */
for (const ref of referencingFile.referencedFiles) {
if (program.getSourceFileFromReference(referencingFile, ref) === searchSourceFile) {
refs.push({ kind: "reference", referencingFile, ref });
}
}
/* 1.2. <reference types>の探索 */
for (const ref of referencingFile.typeReferenceDirectives) {
const referenced = program.getResolvedTypeReferenceDirectives().get(ref.fileName);
if (
referenced !== undefined &&
referenced.resolvedFileName === (searchSourceFile as SourceFile).fileName
) {
refs.push({ kind: "reference", referencingFile, ref });
}
}
最初の<reference path="..." />の探索のreferencedFilesはreadonly FileReference[]型であり(つまり
refはFileReference型、対象のsearchSourceFileはSouceFile型であり、型が異なるのでそのままでは比較できない。
そのため、ProgramのgetSourceFileFromReference関数でrefに相当するSourceFileオブジェクトの取得をしてからsearchSourceFileと比較をしている。
もし、対象のSourceFileと等しければkindを"reference"として、参照元(referencingFile)と参照先(ref)をセットでrefsに追加している。
<reference types="..." />の探索も基本的には同じ。こちらはSourceFileの取得ではなくファイル名での比較を行って等しいか確認している。
Triple-Slash Directivesの探索の後はimport文による参照に含まれているかどうかを確認している。
code:ts
// for (const referencingFile of sourceFiles) {
/* 2. import文から参照ファイルを探索 */
forEachImport(referencingFile, (_importDecl, moduleSpecifier) => {
const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier);
if (moduleSymbol === searchModuleSymbol) {
refs.push({ kind: "import", literal: moduleSpecifier });
}
});
forEachImportは名前の通り第一引数のSourceFileのもつimport文それぞれのforループとなっている。コールバック関数の第2引数はmoduleSpecifier(import { hoge } from '...'の'...'の部分を表すNode)となっているのでそのsymbolをTypeCheckerを用いて取得して探索対象のモジュールシンボルと等しいかを調べている。
もし等しいならkindを"import"としmoduleSpecifierをrefsリストに加えている。
1.1. getReferencedSymbolsForModule関数に戻る。
code:ts
const references = mapDefined<ModuleReference, Entry>(
findModuleReferences(program, sourceFiles, symbol),
reference => { /* ... */ }
);
つまり、この部分ではfindModuleReferences関数で対象のモジュールのsymbolを参照しているファイルの一覧をModuleReference型のリストで取得し、それをEntry型のリストに置換してる。 /icons/hr.icon
※1: shorthand propertyはオブジェクトリテラルにおけるvalueの変数名がkeyのプロパティ名と等しいときに省略して書ける記法のプロパティ名。
code:ts
const prop = 0;
// これは
const obj = { prop: prop };
// こう書ける。このpropがshorthand propertyとして扱われる。
const obj = { prop };
※2: import specifierはimport { a, b } from 'module'のように{}内で指定された各要素(この例の場合aとbそれぞれがimport specifier)。export specifierも同様。import/export specifierはasを用いて別名で扱うことができる。
※3: AST上のNodeは以下のように例え同じ変数を表していてもそれを同一のものと解釈できる情報は用意されていない。
そのため、Symbolは異なるNodeで同一のものを識別するために用いらられる。このSymbolを使うことで型推論などが可能になる。