焼売飯店

GoとかTS、JSとかRubyとか

tsconfig.jsonはJSONじゃないと言う話

気になったので調べてみました。

tsconfig.jsonと普通のJSONの大きな違い

tsconfig.jsonには、コメントが書けます。

tsc --init した時に生成されるtsconfig.jsonに、大量にコメントが付けられているので、すぐに気付くことと思います。

例)

{
  "compilerOptions": {
    "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,

    /* Strict Type-Checking Options */
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */
  }
}

これについて深く考えたことも無く、どこかでこれは JSON5 だと言う噂を見ていた*1のでそうだと思っていました。

ところが、試してみたところJSON5なら使えるはずの豊富な文法が全然使えなかったり、JSONとJSON5にかけていたformatOnSaveの設定が効かなかったので、違うということに気付きました。

tsconfig.jsonの正体は一体何なのか

結論から入りますが、どうやらこれは JSON with Comments (JSONC) と言うフォーマットのようです。

少なくとも、VS Codeはtsconfig.jsonJSON with Commentsとして解釈しています。

右下に注目ください。

f:id:f_syumai:20200331015852p:plain
VS Codeでtsconfig.jsonを開いた時の様子

自動的にJSON with Commentsの編集モードになっていることが確認出来ると思います。

JSON with Commentsとは

VS Codeのドキュメントでは、JSON with Commentsについて下記のように説明されています。

JSON with Comments

In addition to the default JSON mode following the JSON specification, VS Code also has a JSON with Comments (jsonc) mode. This mode is used for the VS Code configuration files such as settings.json, tasks.json, or launch.json. When in the JSON with Comments mode, you can use single line (//) as well as block comments (/* */) as used in JavaScript.

https://code.visualstudio.com/docs/languages/json#_json-with-comments

どうやら、VS Codeの設定ファイルを記述するために使われているフォーマットのようです。

VS Codeは、内部で node-jsonc-parser と言うpackageを利用してJSON with Commentsを解析しているようでした。

github.com

node-jsonc-parserの使い方

node-jsonc-parserは、 parse 関数を提供しており、JSON with Commentsを下記のように簡単にObjectとして読み取ることが出来ます。

import { parse } from "jsonc-parser";
import { promises as fsPromises } from "fs";

(async () => {
  const buf = await fsPromises.readFile("./data.jsonc");
  const data = parse(buf.toString());
  console.log(data); // => 結果のObjectが出力される
})();

parse関数は、ParseOptionsを提供しており、ここに下記のように disallowComments が含まれています。どうやら、VS Codeは通常のJSONもこのparserのOptionを使い分け解析しているようでした。*2

/**
 * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result.
 * Therefore, always check the errors list to find out if the input was valid.
 */
export declare const parse: (text: string, errors?: ParseError[], options?: ParseOptions) => any;

https://github.com/microsoft/node-jsonc-parser/blob/e38baa7f22ee391e6dc0581d70750bdb746d855d/src/main.ts#L100

export interface ParseOptions {
    disallowComments?: boolean;
    allowTrailingComma?: boolean;
    allowEmptyContent?: boolean;
}

https://github.com/microsoft/node-jsonc-parser/blob/e38baa7f22ee391e6dc0581d70750bdb746d855d/src/main.ts#L227

ここに allowTrailingComma が含まれていることからおわかりいただけると思いますが、 なんと JSONC ではtrailing commaが使えます。 これもJSONとの大きな違いですね。

allowEmptyContent の使い方については調べられていません。

TypeScript本体はどのようにしてtsconfig.jsonをパースしているのか

では、TypeScript本体もこの node-jsonc-parser を利用しているのかと言うと、リポジトリ内にこのpackageへの依存が見付からず、そうではないようでした。

見つかったのは下記の parseJsonText と言う関数で、これを使ってJSONを解析しているようでした。

createSourceFile に渡しているオプションを見る限り、TypeScriptはJSONをES2015のJavaScriptとして解析しているように見えました。

export function parseJsonText(fileName: string, sourceText: string, languageVersion: ScriptTarget = ScriptTarget.ES2015, syntaxCursor?: IncrementalParser.SyntaxCursor, setParentNodes?: boolean): JsonSourceFile {
    initializeState(sourceText, languageVersion, syntaxCursor, ScriptKind.JSON);
    // Set source file so that errors will be reported with this file name
    sourceFile = createSourceFile(fileName, ScriptTarget.ES2015, ScriptKind.JSON, /*isDeclaration*/ false);
    sourceFile.flags = contextFlags;

https://github.com/microsoft/TypeScript/blob/96f01227d4f300ad85e5e470595b5c7bd96d9304/src/compiler/parser.ts#L811

ここに続くのは、TypeScriptがJSONとして受け付けられるトークンを分岐するswitch文で、受け付けていたのは 配列、boolean、null、Number (正負)、文字列 で、それらのどれにも当てはまらなかったらObjectとして解析する。と言う風に見受けられました。

switch (token()) {
  case SyntaxKind.OpenBracketToken:
    statement.expression = parseArrayLiteralExpression();
    break;
  case SyntaxKind.TrueKeyword:
  case SyntaxKind.FalseKeyword:
  case SyntaxKind.NullKeyword:
    statement.expression = parseTokenNode<BooleanLiteral | NullLiteral>();
    break;
  case SyntaxKind.MinusToken:
    if (lookAhead(() => nextToken() === SyntaxKind.NumericLiteral && nextToken() !== SyntaxKind.ColonToken)) {
        statement.expression = parsePrefixUnaryExpression() as JsonMinusNumericLiteral;
    }
    else {
        statement.expression = parseObjectLiteralExpression();
    }
    break;
  case SyntaxKind.NumericLiteral:
  case SyntaxKind.StringLiteral:
    if (lookAhead(() => nextToken() !== SyntaxKind.ColonToken)) {
        statement.expression = parseLiteralNode() as StringLiteral | NumericLiteral;
        break;
    }
    // falls through
  default:
    statement.expression = parseObjectLiteralExpression();
    break;
}

ここから想像したのは、 TypeScriptはES2015のObjectの一部文法を制限してJSONCを表現している というものでしたが、やはりコメントとtrailing comma以外の要素は含めることが出来ず、追いかけきれなかったので断念しました。

誰か知ってたら教えてください。

調べた感じ、tsconfig.jsonと紐付ける形でJSON with Commentsに触れている情報ソースはかなり少なく、TypeScript本体も特に言及していないので情報収集がとても困難でした。

JSON with Commentsと言う規格について

この規格は、どうも標準化されていない気がします。

JSONCで検索して見つかるのは、Goで実装された野良Parserのようなものばかりでした。

node-jsonc-parserが代表的な実装なのかなと思っています。

見つかったParserとか

下記の記事を見る限り、Windows Terminalの設定ファイルとかでも使われているようです。

qiita.com

tsconfig.jsonや、VS Codeのsettings.jsonなど、ここまで広く使われているもののフォーマットがこうフワッとしているように感じられるのは、何だか面白いなあと思った話でした。