皆さん、TypeScript使ってますか? TypeScriptはオープンソースで開発されているプログラミング言語であり、JavaScriptプログラミングに静的型による安全性をもたらしてくれます……という説明は、おそらくこの記事を開いたみなさんには今さらでしょう。
TypeScriptはOSSであり、我々ユーザーがPull Requestを送って開発に貢献することができます。筆者はこの前のゴールデンウィークに初めてのPRをTypeScriptに送って数時間くらいでマージされました。これは先日リリースされたTypeScript 3.5に含まれています。
このとき初めてTypeScriptコンパイラのソースコードをじっくり読んでおり、それが筆者が最近書いた別記事TypeScriptの型推論詳説の執筆にも役立っています。
皆さんにもぜひTypeScriptへの貢献をおすすめしたく、その手引としてTypeScriptコンパイラ入門を書くこととしました。第1回では、恐らく最もとっつきやすい部分であるエラーメッセージの改善について解説します。今回筆者が送ったPRもエラーメッセージの改善です。(2)以降は筆者が次のPRを送ったら書きます。
この記事では筆者がどのような改善を行ったのか紹介し、修正を完了するまでの思考ステップを全て解説します。なお、記事中ではTypeScriptコンパイラのソースコードを必要に応じて掲載しながら解説します。ソースコードの著作権表記は記事末尾を参照してください。
改善の内容
今回筆者は次のissueをクローズしました。
これは、async
関数の中以外の場所でawait
を使ったときのエラーメッセージに関するissueです。例として次のコードを見てください。このコードはasync
ではない普通の関数の中でawait
を使っています。
function foo() {
await null;
}
これに対する従来のエラーメッセージ(TypeScript 3.4以前)は以下のスクリーンショットのように「'await'式は、非同期関数内でのみ使用できます。」とだけ表示していました。
今回のPRによって、TypeScript 3.5では次のスクリーンショットのようにメッセージが追加されています。
すなわち、従来のメッセージに加えて「Did you mean to mark this function as 'async'?」というメッセージが追加されています(これは新しいメッセージなのでまだ翻訳されていないようです。いつ翻訳されるのかはよく分かりませんが)。さらに、このメッセージはtest.ts(1, 10)
という位置を示しています。この位置はfunction foo() {
のfoo
の部分に相当します。
つまるところ、await
を含んでいる関数がどこなのかを示すメッセージを従来のエラーメッセージに加えて表示したということです。関数が大きいときに便利かもしれませんね。
このissueはgood first issue
ラベルが付いているissueの中から目ぼしい物を選びました。ただ、TypeScriptへの貢献を狙っている人は意外と多く、そのようなissueには大抵未経験っぽい人が「やります😆」的なコメントを付けています。誰にも取られずに残っているgood first issue
は滅多にありません。
それにも関わらず残っているgood first issue
というのはいくつかのパターンに分かれます。だいたいは次の3つです。
- やりますと言った人がそのまま音沙汰が無くなって放置されている。
- やりますと言った人が「やり方教えて😉」と言って無視された状態で放置されている。
- ちゃんとPRが出たがPRが放置されている。
3はだめですが、1と2の場合は残念ながら彼らにはまだ早かったのでしょう。数カ月も放置されていれば奪って大丈夫です。筆者が取ったissueも過去2人の人が挑戦して撃沈していました。
開発環境
TypeScriptへの貢献に際しては開発環境を整える必要があります。詳しくはCONTRIBUTING.mdに書いてあるのでそちらを参照しましょう。
あとでまた説明しますが、とりあえずソースを変更したらgulp
を実行すれば(gulp
をグローバルインストールしていない場合はnpx gulp
でOK)ビルドされます。ビルドするとbuilt/local/tsc.js
という場所にソースからビルドされたtsc
ができるので、これを使って動作を確かめられます。
TypeScriptコンパイラはそれ自身がTypeScriptで書かれています。いわゆるセルフホスティングというやつですね。TypeScriptということで筆者はVSCodeを使ってコードを編集しようとしましたが、コードベースが大きいためメモリがたった8GBのPCでは重くて使い物にならず、結局vimを使いました。
では、いよいよ修正に入っていきましょう。
エラーメッセージはどこで定義されているのか
今回はエラーメッセージに関する変更ということで、そもそもTypeScriptがどのようにエラーメッセージを定義しているのかを理解しておく必要があります。筆者は事前にいくつかのPRを見てエラーメッセージがどこで定義されているのか理解しました。
答えを言ってしまうと、src/compiler/diagnosticMessages.json
に定義されています。ここに全てのエラーメッセージが定義されています。例えば上で例示したエラーメッセージは次のように書かれています。
"'await' expression is only allowed within an async function.": {
"category": "Error",
"code": 1308
},
このように、エラーメッセージをキーとしてcategory
とcode
の情報が書かれています。エラーコードについてはwikiで説明されています。このエラーメッセージはawait
がasync
関数の外では使えないという文法的な制約なので1000番台が割り当てられます。
今回のPRではDid you mean to mark this function as 'async'?
という新しいメッセージを追加するので、このファイルに新しいメッセージを追加しました。
"Did you mean to mark this function as 'async'?": {
"category": "Error",
"code": 1356
},
元のエラーメッセージが1000番台(構文に関するエラーメッセージ)なので、同じく1000番台の一番最後にこのメッセージを追加します。
これでPRはもうできたも同然ですね。あとはこのエラーメッセージを表示する処理を追加するコードを書けば完成です。
なお、diagnosticMessages.json
を変更したらgulp generate-diagnostics
を実行しておきましょう。実はdiagnosticMessages.json
からはTypeScriptの型定義などのコードが自動生成されており、これを実行しておかないとコンパイラのソースコード中から当該エラーメッセージを利用できません。
変更すべき箇所を探す
ではいよいよコンパイラ本体を変更していきます。しかし、どこを編集すればいいのでしょうか。
まず前提として、TypeScriptの型検査をはじめとするメインの処理(構文解析をして抽象構文木 (AST) 1ができた状態でそれに対してエラーチェックを行う部分)はsrc/compiler/checker.ts
に書かれています。よって、今回いじるのはこのファイルです。厳密にはawait
のチェックは型検査とかではなく構文上のエラーなのですが、構文解析の時点でこれをハンドルするのは面倒なので、構文解析の段階では一旦受け入れて抽象構文木を作り、その後でエラーを出すという戦略になっています。
型検査などを全部このファイル1つに詰め込んでいるため3万行くらいあってとてもでかいですが、全部を把握する必要はありません。今回はエラーメッセージを変えることになるため、まず当該のエラーメッセージを出している箇所を探しました。
ちょっと検索すると、以下のcheckAwaitExpression
関数でこのエラーメッセージが使われていることが分かります(ソースコードはPR反映前のコミットe8161ef
のもの)。
function checkAwaitExpression(node: AwaitExpression): Type {
// Grammar checking
if (produceDiagnostics) {
if (!(node.flags & NodeFlags.AwaitContext)) {
grammarErrorOnFirstToken(node, Diagnostics.await_expression_is_only_allowed_within_an_async_function);
}
if (isInParameterInitializerBeforeContainingFunction(node)) {
error(node, Diagnostics.await_expressions_cannot_be_used_in_a_parameter_initializer);
}
}
const operandType = checkExpression(node.expression);
return checkAwaitedType(operandType, node, Diagnostics.Type_of_await_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member);
}
checkAwaitExpression
という関数名から察するに、これはawait
式に対する型チェックなどを行う関数でしょう。返り値がType
ですから、await
式の型推論の結果が返されると思われます。前述のように、そのついでにawait
が使用可能な場所かどうかのチェックを行っています。
引数node
の型がAwaitExpression
となっていることから、これはawait expr
という式を表すASTのノードであることが分かります。最初の// Grammar checking
というコメントがついているif
文の中が、await
が正しい場所で使われているかどうかのチェックのようですね。
当該のエラーメッセージは次の文で使われています。
grammarErrorOnFirstToken(node, Diagnostics.await_expression_is_only_allowed_within_an_async_function);
引数のDiagnostics.await_expression_is_only_allowed_within_an_async_function
という定数っぽいものがエラーメッセージです。このDiagnostics
というオブジェクトは前述のdiagnosticMessages.json
から自動でコード生成されたものです。元々のDid you mean to mark this function as 'async'?
というメッセージ(プロパティ名)から空白を_
に変換したり記号を取り除いたりしたものがエラーメッセージを表すプロパティ名となります。
grammarErrorOnFirstToken
というのは、名前から察するにnode
の位置(FirstToken
とあるので最初のトークン、すなわちawait
の部分でしょう)で第2引数で指定したメッセージを発生させるという処理をする関数でしょう。これは先ほどお見せしたスクリーンショットと合致していますね。
ちなみに、エラーの発生条件である!(node.flags & NodeFlags.AwaitContext)
が「await
式がasync
関数内にない」という条件を表していることになります。node.flags
は多分構文解析器がASTを生成するときに付与する情報でしょう。構文解析の時点で「そのノードの文脈がawait
使用可能な文脈化どうか」という情報が付加されていることになります。まあ自然ですね。
なお、この部分を発見する方法ですが、await_expression_is
とかでソースコード内をすぐ検索するとすぐに見つかりました。'await' expression is only allowed within an async function.
というエラーメッセージがDiagnostics.await_expression_is_only_allowed_within_an_async_function
という名前でソースコード内に現れることを知っていれば見つけるのは簡単です。
もちろん、これを違うエラーに変更したりするとちゃんと反映されます。まずTypeScriptのソースコードをいじってちょっと挙動を壊してみるのも面白いですね。変更が反映済みのtsc
は、gulp
でビルドしたあとbuilt/local/tsc.js
に存在しています。これを使ってTypeScriptファイルをコンパイルすることで挙動を確かめましょう。
どう変更するか検討する
では、変更すべき箇所が分かったので次はどう変更すればいいのか検討しましょう。
今回の目的は「既存のエラーメッセージにサブメッセージとしてDid you mean to mark this function as 'async'?
を追加する」ということですから、エラー生成部分に手を入れる必要があります。まあ、さすがにサブメッセージ的な機構は既にあるはずです。そうでないとさすがにgood first issue
ではありません。
ということで、まずgrammarErrorOnFirstToken
の中身を見てみる必要があります。
function grammarErrorOnFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean {
const sourceFile = getSourceFileOfNode(node);
if (!hasParseDiagnostics(sourceFile)) {
const span = getSpanOfTokenAtPosition(sourceFile, node.pos);
diagnostics.add(createFileDiagnostic(sourceFile, span.start, span.length, message, arg0, arg1, arg2));
return true;
}
return false;
}
今回使われている2つの引数の他に3つほどオプショナルな引数がありますが、これはプレースホルダーを持つエラーメッセージ用であることはまあ分かりますね。例えばCannot find name '{0}'.
のようなエラーメッセージが該当します。
hasParseDiagnostics(sourceFile)
は、関数名から察するに「このファイルで既に構文エラーのメッセージが発生しているか」を判定していると思われます。すでに発生している場合は追加でエラーを発生させるのを避けているわけですね。一般に、パースエラーが1度発生した時点で正常にソースを読み込むのは不可能でありいくらでも構文エラーが発生しますから、複数の構文エラーメッセージを出す意味がそんなに無いわけです。
その中でやっていることは、createFileDiagnostic
関数でエラーメッセージを表すオブジェクトを作成し、それをdiagnostics.add
で発生したエラーメッセージの一覧に追加しているように見えます。span
というのはエラーの範囲(画像の赤線が引かれている部分)を表すオブジェクトです。今回はgetSpanOfTokenAtPosition
という関数名から察するに、一つのトークン(今回はawait
式の最初のトークンなのでawait
キーワードでしょう)に相当する範囲を表すオブジェクトとなります。
今回のミッションはcreateFileDiagnostic
で作られたエラーメッセージ(型を見るとDiagnosticWithLocation
型のオブジェクトのようです)に対して、付加的な情報、すなわち“サブメッセージ"のようなものを追加することです。ということで、このDiagnosticWithLocation
オブジェクトをいじる必要があります。どういじればいいのか調べるために、DiagnosticWithLocation
型の定義を見てみましょう。
export interface DiagnosticWithLocation extends Diagnostic {
file: SourceFile;
start: number;
length: number;
}
なるほど、これはDiagnostic
オブジェクトにファイル中の位置を指し示すプロパティを追加したオブジェクトですね。名前から察するに、このDiagnostic
がメインの型のようです。
export interface Diagnostic extends DiagnosticRelatedInformation {
/** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */
reportsUnnecessary?: {};
source?: string;
relatedInformation?: DiagnosticRelatedInformation[];
}
export interface DiagnosticRelatedInformation {
category: DiagnosticCategory;
code: number;
file: SourceFile | undefined;
start: number | undefined;
length: number | undefined;
messageText: string | DiagnosticMessageChain;
}
code
などのいかにもそれっぽい情報がありますが、目に付くのはrelatedInformation
プロパティです。これはDiagnosticRelatedInformation[]
型を持ち、あるDiagnostic
型に対して0個以上のDiagnosticRElatedInformation
オブジェクトを紐付けられるようになっています。これはまさに「エラーメッセージが子メッセージを持つ」という状況を表しており、今回の目的に激しく合致しています。
ということで、ここまでのコードリーディングでやるべきことがはっきりしました。Did you mean to mark this function as 'async'?
というメッセージを表すDiagnosticRelatedInformation
オブジェクトを作成し、それを既存のエラーメッセージのrelatedInformation
に追加してやればいいのです。
どう実装するか検討する
何をすればいいかは分かりましたので、それをどう実装するかを次に考えます。やりたいことの通りにコードを書けばそれでいいのですが、あまりに汚い、というか既存のコードと合致しないコードは書きたくありません。特に自分は、今回やりたいことをなるべく簡単にやってくれる既存の関数があるのかどうか気になりました。
ということで、まずやったことは「ソースコードをrelatedInformation
で検索する」ということです。relatedInformation
を扱う便利関数を探すわけですね。
そうすると次のaddRelatedInfo
関数が随所で呼ばれていることが分かります。まあ見れば分かる通り、与えられたDiagnostic
オブジェクトのrelatedInformation
にDiagnosticRelatedInformation
オブジェクトたちを追加してくれる便利関数ですね。
export function addRelatedInfo<T extends Diagnostic>(diagnostic: T, ...relatedInformation: DiagnosticRelatedInformation[]): T {
if (!diagnostic.relatedInformation) {
diagnostic.relatedInformation = [];
}
diagnostic.relatedInformation.push(...relatedInformation);
return diagnostic;
}
これがあちこちで呼ばれているということは、これをさらにラップしてくれるハイレベルな(grammarErrorOnFirstToken
のような)便利関数は無さそうということが分かります。
これにより、実装方針が固まりました。具体的には「新しいエラーメッセージを表すDiagnostic
オブジェクトを作り、addRelatedInfo
を使って既存の処理により作られたDiagnostic
オブジェクトにそれを追加し、最後にdiagnostics.add
する」という処理をcheckAwaitExpression
内にベタ書きすることにしました。
いざ実装
ここまで決めればあとは書くだけです。実装の中身を順番に見ていきましょう。
サブメッセージを作る
まず、今回追加したいサブメッセージを表すDiagnostic
オブジェクトを作る必要があります。これもエラーメッセージの一種なわけですが、ご存知のようにエラーメッセージは主に「エラーの場所」と「エラーメッセージ」から構成されます(前者は、DiagnosticRelatedInformation
の定義を見れば分かるように、無いこともあるようですが)。
今回のメッセージ、すなわちDid you mean to mark this function as 'async'?
がどこを指し示していればいいか考えてみましょう。これは「この関数をasync
関数にするんじゃないですか?」的な意味なので、「この関数」を指し示している必要があります。この関数とは、問題のawait
式を含む関数です。
ここで、「与えられたノード(今回はAwaitExpression
ノード)を含む関数を取得する」という処理が必要になりました。さすがにこの処理を行ってくれる関数はすでにどこかにあるだろうということでソースを検索します。関数名にcontain
あたりが入っているだろう、のようなアタリを付けて適当に検索していくとgetContaingFunction
というそれっぽい関数が見つかります。
export function getContainingFunction(node: Node): SignatureDeclaration | undefined {
return findAncestor(node.parent, isFunctionLike);
}
引数はNode
で返り値はSignatureDeclaration | undefined
ですね。undefined
の可能性があることは「node
を含む関数」が存在しないこともあるからでしょう。SignatureDeclaration
というのが関数を表すオブジェクトのようなので調べてみましょう。
export type SignatureDeclaration =
| CallSignatureDeclaration
| ConstructSignatureDeclaration
| MethodSignature
| IndexSignatureDeclaration
| FunctionTypeNode
| ConstructorTypeNode
| JSDocFunctionType
| FunctionDeclaration
| MethodDeclaration
| ConstructorDeclaration
| AccessorDeclaration
| FunctionExpression
| ArrowFunction;
関数の種類ごとに型が細分化されていますね。ただ、あまり細かく見ていかなくても大丈夫です。ちょっと見ていくと、これらの個別の型は全てSignatureDeclarationBase
の部分型であることが分かります。
export interface SignatureDeclarationBase extends NamedDeclaration, JSDocContainer {
kind: SignatureDeclaration["kind"];
name?: PropertyName;
typeParameters?: NodeArray<TypeParameterDeclaration>;
parameters: NodeArray<ParameterDeclaration>;
type?: TypeNode;
/* @internal */ typeArguments?: NodeArray<TypeNode>; // Used for quick info, replaces typeParameters for instantiated signatures
}
さらに継承関係を辿って行くと、これらは結局Node
であることが分かります。このことが今回重要です。
export interface Declaration extends Node {
_declarationBrand: any;
}
export interface NamedDeclaration extends Declaration {
name?: DeclarationName;
}
すなわち、getContainingFunction
の返り値は(undefined
でなければ)関数を表すNode
オブジェクトだったのです。
特定のノードに対してエラーを発生させるという処理はとてもよくありそうですから、Node
があればそれに対するエラーメッセージ(Diagnostic
)を生成してくれる関数はすでにありそうです。ということで適当にそれっぽい名前で再び検索します。
すると、createDiagnosticForNode
という関数が見つかります。とりあえずこの部分のコードを書いてみるとこんな感じになります。
const func = getContainingFunction(node);
if (func && func.kind !== SyntaxKind.Constructor) {
const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async);
}
node
がAwaitExpression
オブジェクトだったので、それを含む関数を取得し、それがあったらcreateDiagnosticForNode
でエラーメッセージを生成します。メッセージが指し示す対象はもちろん当該の関数ノード、すなわちfunc
です。
元々のエラーメッセージに追加する
こうしてできたサブメッセージをaddRelatedInfo
で元々のエラーメッセージに追加したいですね。元々のエラーメッセージを作る処理は、grammarErrorOnFirstToken
を呼び出す代わりにその中身をベタ書きすることにしましょう。そして、diagnostics.add
の前にサブメッセージを追加する処理を挟みます。するとこうなります。
const sourceFile = getSourceFileOfNode(node);
if (!hasParseDiagnostics(sourceFile)) {
const span = getSpanOfTokenAtPosition(sourceFile, node.pos);
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.await_expression_is_only_allowed_within_an_async_function);
const func = getContainingFunction(node);
if (func) {
const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async);
addRelatedInfo(diagnostic, relatedInfo);
}
diagnostics.add(diagnostic);
}
元々のエラーメッセージであるdiagnostic
を作るところまでをgrammarErrorOnFirstToken
から複製してきました。そして、addRelatedInfo
を用いてdiagnostic
にrelatedInfo
を追加していることが分かります。
以上で実装はほとんど終了です。
微調整
最初は以上のような実装にしていたのですが、ひとつ気づいた点がありました。それは、外の関数がコンストラクタだった場合はサブメッセージを付けてはいけないということです。このサブメッセージは関数をasync
にすることを提案するものですが、コンストラクタはasync
にできないからです。
というわけで、func
がコンストラクタの場合はサブメッセージを追加する処理を書くことにしました。そのためにはfunc
がコンストラクタかどうか判定できる必要がありますね。ここでfunc
はSignatureDeclaration
だったことを思い出します。
export type SignatureDeclaration =
| CallSignatureDeclaration
| ConstructSignatureDeclaration
| MethodSignature
| IndexSignatureDeclaration
| FunctionTypeNode
| ConstructorTypeNode
| JSDocFunctionType
| FunctionDeclaration
| MethodDeclaration
| ConstructorDeclaration
| AccessorDeclaration
| FunctionExpression
| ArrowFunction;
この中にConstructorDeclaration
というのがあり、これがいかにもそれっぽいですね。
export interface ConstructorDeclaration extends FunctionLikeDeclarationBase, ClassElement, JSDocContainer {
kind: SyntaxKind.Constructor;
parent: ClassLikeDeclaration;
body?: FunctionBody;
/* @internal */ returnFlowNode?: FlowNode;
}
他にConstructorTypeNode
というのもありますが、こちらは型を表すノードの話なので今回は関係ありません。
ConstructorDeclaration
はkind: SyntaxKind.Constructor;
とあり、SyntaxKind
というのはノードの種類を表すenum型です。つまり、これは筆者の既存記事でも解説しているタグ付きunionのパターンになっていることが分かります。よって、func
がコンストラクタかどうか判定するにはfunc.kind
がSyntaxKind.Constructor
かどうかを見ればいいことが分かります。
これにより、コードは以下のようになりました。これで今度こそOKですね。
if (func && func.kind !== SyntaxKind.Constructor) {
const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async);
addRelatedInfo(diagnostic, relatedInfo);
}
実際のPRでは親切にもアサーションを追加したりしています。
for-await-of
文も忘れずに対処する
実は、await
というのはawait expr
のようなawait
式の他に、for-await-of
文という構文でも登場します。こちらもやはりasync
関数の中でしか使えません。
ということで、こちらにも同様に変更を加えました。やっていることはほとんど同じなので実際のコードを見たい方はPRのdiffを参照してください。
似たことをしているので関数に纏めようかとも思いましたが、微妙にやっていることが違っていたのでそのままにしておきました。
既存のテストの修正
以上で実装は終わりましたが、これだけではPRを送ることはできません。テストをちゃんと整備しないといけないのです。
TypeScriptコンパイラのテストは基本的に、テスト用のTypeScriptソースコードに対して実際にコンパイラを走らせ、発生するエラーメッセージや推論される型などが正しいかどうかチェックするというものです。各ソースコードに対してそれに対して期待される結果が書かれたファイルが存在しており(これをbaselineといいます)、テストでは結果がこのファイルの中身と一致しているかどうかが試されます。いわゆるスナップショットテストというやつですね。
CONTRIBUTING.md
に書いてある通り、テストを走らせるには以下のコマンドを使います(全部のテストを走らせるとすごく時間がかかります)。
gulp runtests-parallel
実は、ここまでの変更を加えた段階ではテストが落ちるようになっています。エラーメッセージに変更を加えたので、そのエラーメッセージをチェックするテストが落ちてしまっているのです。
落ちているテストで何がどう変わったのかはgulp diff
を実行すると分かります。
今回の場合、テストの側を書き換える必要がありますね。エラーメッセージが変わったのは今回の改善の正しい結果だからです。その場合はbaselineを新しい結果に合わせて書き換えます。以下のコマンドを実行すればOKです。
gulp baseline-accept
テストを書く
また、今回の変更をより網羅的にテストするために、自分でも新しいテストを追加しなければいけません。
具体的には、tests/cases/compiler/awaitInNonAsyncFunction.ts
というファイルを作成し以下のようなコードを書きました。
// @target: esnext
// https://github.com/Microsoft/TypeScript/issues/26586
function normalFunc(p: Promise<number>) {
for await (const _ of []);
return await p;
}
export function exportedFunc(p: Promise<number>) {
for await (const _ of []);
return await p;
}
const functionExpression = function(p: Promise<number>) {
for await (const _ of []);
await p;
}
const arrowFunc = (p: Promise<number>) => {
for await (const _ of []);
return await p;
};
function* generatorFunc(p: Promise<number>) {
for await (const _ of []);
yield await p;
}
class clazz {
constructor(p: Promise<number>) {
for await (const _ of []);
await p;
}
method(p: Promise<number>) {
for await (const _ of []);
await p;
}
}
for await (const _ of []);
await null;
今回変更したエラーが発生するawait
式やfor-await-of
文をひたすら書いたソースコードですね。いろいろな関数タイプを試していたり、関数の中ではない場合もちゃんと試していたりします。
このファイルだけ追加してテストを走らせると、tests/baselines/reference/
以下に4つほどファイルが自動生成されます(中身はPRを参照)。これらが新しく追加されたテストに対するbaselineとなります。
ということで、生成されたbaselineファイルたちを目視で確認し、内容に問題が無ければ無事にテストが書けたということになります。
なお、テストを走らせるときは再び全テストを走らせるとものすごく時間がかかるので絞り込みを行う必要があります。ファイル名で絞り込めるのでこんな感じで適当に絞り込みましょう。
gulp runtests --tests=awaitInNonAsyncFunction
まとめ
というわけで、以上で今回筆者が作成したPRの中身と、そこに至るまでの思考の流れを解説しました。TypeScriptに対する初めての貢献でしたが、レビューもたいへんスムーズに進み(余計な改行が入っていたのを指摘されましたがそれだけ)、PRを出してから数時間でマージされました。good first issue
に対するPRがいくつも放置されている現状を見るに、コードに対して必要十分かつ的確な変更をすること(あとPRの説明文もいい感じに)が重要であると思われます。
繰り返しになりますが、エラーメッセージの変更は比較的やりやすい部類です。全く新しいエラー判定を追加するのがgood first issue
になることはあまり無いでしょうが、既存のエラーメッセージがわかりにくいので改善するというようなタスクは結構あります。
その際はただエラーメッセージの文面を変更するだけでなく、(今回外側の関数を探す処理を追加したよおうに)何らかの追加情報を集める処理を書かなければいけないことが多いでしょう。それがエラーメッセージ改善の肝であるといえます。そのような部分は場合に応じてアドリブで進めていく必要があります。この記事で事細かに述べた思考過程がその参考になれば幸いです。ソースコードを全部理解する必要はなく、必要な部分を調べれば大抵は大丈夫です。
とはいえ、ある程度コンパイラの全体像を把握していたほうがコードの理解・予測がしやすいのは間違いありません。例えば、記事中に出てきたcheckAwaitExpression
はベーシックな型検査アルゴリズム(抽象構文木に沿って再帰的に制約を作ったり型を生成したりする)のイメージが頭の中にあればその意味が自然に理解できます2。興味が出てきたらその方面を調べてみるのもよいかもしれません。
とにかく、皆さんもぜひTypeScriptに貢献しましょう。たまにgood first issue
が発生するので、いけそうだなと思ったら他の人に取られる前にissueにやりますと書きましょう(筆者が取ったissueはしばらく放置されていたのでいきなりPRを出しましたが)。
やり方をTypeScriptチームの人に聞くとたまに教えてもらえますが、返事が返ってこないことも多いです。まあ、いちいち教えてあげなくても(筆者のように😤)勝手にソースを読み解いてPRを送ってくる人もいるわけですから、手取り足取り教えてあげる時間が勿体無いのかもしれません。この記事を参考に自力でTypeScriptコンパイラのコードが読めるようになるといいですね。
この記事を参考に皆さんもぜひやってみてください。
著作権表示
記事中に掲載したTypeScriptコンパイラのソースコードはApache 2.0 ライセンスのもと公開されており、著作権表示は以下の通りです。
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
-
抽象構文木 (AST) とは、プログラムのソースコードを意味的に表す木構造です。木構造とかよく分からないという方はとりあえずプログラムのソースコードを表すオブジェクトだと思っておきましょう。プログラムの各単位(文とか式とか)に対して対応するオブジェクト(ノードと呼ばれる)が存在します。例えば
x + 1
という式全体に対応するノードは、自身が+
式であるという情報、左の式(子ノード)と右の式(子ノード)がそれぞれ何かという情報を持つオブジェクトです。この場合左の子ノードはx
という式に対応するノードであり、右の子ノードは1
という式に対応するノードです。 ↩ -
TypeScriptの型推論詳説で解説したように、TypeScriptの型推論はこの方式にそってワンパスで行なわれるものです。関数型言語などでは、木に沿って制約を生成したあと制約を解消するというものになったりして少し複雑です。 ↩