GoToDefinition.getReferenceAtPositionを読む
TypeScriptリポジトリのsrc/services/goToDefinition.tsにgetReferenceAtPositionという関数がある。このあたり。 この関数はTypeScriptのLanguage ServiceにおけるGo To Definition内で使われているだけでなく、Find All Referenceのコアロジックでも使われている(ので読んでみた)。
この関数はexportされているがGoToDefiniton namespaceに@internalのJSDocアノテーションが付いているため、この関数自体へはTypeScriptのLanguage Service内部でしか参照できない(つまり、import * as ts from 'typescript'して参照できない。Language Service全体で言えることだが...)。
Triple-Slash DirectivesとはTS/JSのモジュール(import/export)の代わりに依存ファイルをコンパイラに指定できる特殊な記述のこと。宣言ファイル(*.d.ts)でこの記述をたまに見かける。
例えば/// <reference path="./file.ts" />とファイルの先頭に書けばローカルの別ファイルをコンパイラに依存ファイルとして指定できる(そして依存関係を解決してコンパイルできる)。
このgetReferenceAtPosition関数ではpositionがTriple-Slash Directivesの依存ファイルを指定している範囲(上記の例だと"./file.ts")内にあるとき、その参照先のファイルの情報を返す。
以下はこのgetReferenceAtPosition関数の引数と戻り値
code:ts
export function getReferenceAtPosition(
sourceFile: SourceFile,
position: number,
program: Program
): { fileName: string, file: SourceFile } | undefined
{
/* ... */
}
引数には開いているファイルのオブジェクト(SourceFileという型のオブジェクト)とそのファイル上の位置を示すposition、そしてTypeScript Compilerにおいて1つのコンパイル単位を表すprogramが渡される。
sourceFileのpositionに該当する参照が見つかった場合、fileNameとそのファイルのオブジェクトを返し、該当する参照が見つからなかった場合にはundefinedを返す。
この関数のロジックは大きく分けて3つに分かれる。
positionが/// <reference path="..." />のファイル指定部上にあればその参照先のファイルを返す
positionが/// <reference types="..." />の宣言ファイルの指定部上にあればその参照先のindex.d.tsのファイルを返す
positionが/// <reference lib="..." />のビルトインライブラリの指定部上にあれば該当する*.d.tsファイルを返す
上記いずれかにも当てはまらない場合undefinedを返す。
最初の/// <reference path="..." />の参照を取得する部分は以下の様に書かれている(関数内1-5行目)
code:ts
const referencePath = findReferenceInPosition(sourceFile.referencedFiles, position);
if (referencePath) {
const file = program.getSourceFileFromReference(sourceFile, referencePath);
return file && { fileName: referencePath.fileName, file };
}
findReferenceInPositionを細かく読んでみる。
code:ts
// goToDefinition.ts 109行目
const referencePath = findReferenceInPosition(sourceFile.referencedFiles, position);
// goToDefinition.ts 305行目
export function findReferenceInPosition(refs: readonly FileReference[], pos: number): FileReference | undefined {
/* ... */
}
findReferenceInPositionの第一引数に渡しているのはsourceFile.referencedFiles。referencedFilesはこのsourceFile内に記載されている/// <reference path="..." />形式のTriple-Slash Directivesで参照しているファイルのリストが入っている。
それぞれの参照しているファイルはFileReferenceという型を持ち、FileReferenceは単にファイル名と開始位置(pos)と終了位置(end)をもつオブジェクト。
code:ts
export function findReferenceInPosition(refs: readonly FileReference[], pos: number): FileReference | undefined {
return find(refs, ref => textRangeContainsPositionInclusive(ref, pos));
}
find関数はTypeScript Compiler内で使っているユーティリティ関数でArray.prototype.findと同じ動きをする。
code:ts
// goToDefinition.ts 305行目
export function findReferenceInPosition(refs: readonly FileReference[], pos: number): FileReference | undefined {
return find(refs, ref => textRangeContainsPositionInclusive(ref, pos));
}
// src/compiler/utilities.ts 4915行目
export function textRangeContainsPositionInclusive(span: TextRange, position: number): boolean {
return position >= span.pos && position <= span.end;
}
textRangeContainsPositionInclusiveは単にpositionがspanのposとendの範囲内にあるかを見ているだけ。spanの型のTextRangeは以下のような継承関係を持っているのでFileReferenceを渡すことができている
code:ts
export interface FileReference extends TextRange {
fileName: string;
}
export interface TextRange {
pos: number;
end: number;
}
この一連の処理をひとまとめにして書くと以下のように置き換えることができる。
code:ts
const referencePath: FileReference | undefined
= find(refs, ref => (ref.pos <= position && position <= ref.end));
code:ts
const referencePath = findReferenceInPosition(sourceFile.referencedFiles, position);
if (referencePath) {
const file = program.getSourceFileFromReference(sourceFile, referencePath);
return file && { fileName: referencePath.fileName, file };
}
そしてreferencePathがあればprogramのgetSourceFileFromReference関数にsourceFile(開いているファイル)とreferencePathを渡す。referencePathのファイルをprogramが持ってない場合もあるのでその場合はundefinedが返される。存在する場合はsourceFileと同じ型(SourceFile)のfileにreferencePathに相当するファイルのオブジェクトが入る。
programのgetSourceFileFromReferenceに関してはまた今度詳しく読んでいく。
/// <reference types="..." />のTriple-Slash Directivesは*.d.tsの宣言ファイルを生成するときにDefinitly Typedに定義されているパッケージを参照しているときに自動的に付け加えられる。 このTriple-Slash Directivesのtypesにはnode_modules/の@typesディレクトリの中から宣言ファイル名を指定することができる。例えばnode_modules/@types/node/があれば/// <reference types="node" />のように。
code:ts
const typeReferenceDirective = findReferenceInPosition(sourceFile.typeReferenceDirectives, position);
if (typeReferenceDirective) {
const reference = program.getResolvedTypeReferenceDirectives().get(typeReferenceDirective.fileName);
const file = reference && program.getSourceFile(reference.resolvedFileName!); // TODO:GH#18217
return file && { fileName: typeReferenceDirective.fileName, file };
}
該当箇所のコードはこの通り(getReferenceAtPosition関数内7-12行目)。基本的には/// <reference path="..." />のときと構造は同じ。souceFileのreferencedFilesだったものがtypeReferenceDirectives(型は同じreadonly FileReference[])に置き換わり、見つけたtypeReferenceDirectiveからSourceFileのオブジェクトを取ってきているのが、
code:ts
const reference = program.getResolvedTypeReferenceDirectives().get(typeReferenceDirective.fileName);
const file = reference && program.getSourceFile(reference.resolvedFileName!);
の部分。typeReferenceDirectiveのfileNameには単に"node"のように完全なファイル名は入っていないので、program.getResolvedTypeReferenceDirectives()でnode_modules/内にある@typesの詳細情報のMapを取得し、そのMapから該当するオブジェクトを取得している。
例えばreference.resolvedFileNameには"/path/to/node_modules/@types/node/index.d.ts"のような完全なファイル名が入る。
そしてprogram.getSourceFile(reference.resolvedFileName!)で該当するSourceFileオブジェクトを取得してファイル名とともに返している。
program.getResolvedTypeReferenceDirectivesについてはまたいつかじっくり読む。
/// <reference lib="..." />のTriple-Slash Directivesではビルトインライブラリの宣言ファイルを指定できる。例えば"es2017"や"es2017.string"のように。--libで指定するのと同じ。
宣言ファイルを作るときにビルトインライブラリの型定義を使いたい場合はこのTriple-Slash Directivesで明示的に指定することが推奨されている。
code:ts
const libReferenceDirective = findReferenceInPosition(sourceFile.libReferenceDirectives, position);
if (libReferenceDirective) {
const file = program.getLibFileFromReference(libReferenceDirective);
return file && { fileName: libReferenceDirective.fileName, file };
}
三度目になればもう慣れてくるがこの(関数内14-18行目)部分も先程とどうようの構造を持っている。
program.getLibFileFromReferenceについては気が向いたら詳しく読みすすめる。