TypeScript Compiler APIでASTからコード生成しFormatter CLIを作る

はじめに
こんにちは。サイバーエージェントのソフトウェアエンジニアの平井柊太(@did0es)です。
前回は、TypeScriptで書かれたソースコードのASTの解析と、これを応用したLinterの開発を行いました。
今回は、ASTからTypeScriptのソースコードを生成します。この生成の仕組みを利用し、前回開発したLinterをFormatterとして使えるようにします。
プロジェクトをセットアップする
前回と同様に、プロジェクトをセットアップします。Node.jsは「v23.10.0」を使います。build-your-own-ast-tools-by-typescriptディレクトリの中に、以下で新たな作業用ディレクトリ(004-generate-codeディレクトリ)を作成します。
mkdir -p build-your-own-ast-tools-by-typescript/004-generate-code cd build-your-own-ast-tools-by-typescript/004-generate-code
今回は手順を簡単にするため、GitHubリポジトリにある basic-project ディレクトリにあるファイル一式を、作業用ディレクトリにコピーして使用します。以下の構造になります。
004-generate-code ├── console.ts ├── node.config.json ├── package-lock.json ├── package.json └── tsconfig.json
環境構築の詳細は、前回の「セットアップ」の項を参照ください。
ASTからTypeScriptのソースコードを生成してみる
今まではTypeScriptのソースコードからASTを生成し、内容を解析してきました。今回はその逆で、ASTからソースコードを生成するような仕組みを実装します。
TypeScript Compiler APIを用いてASTからソースコードを生成する
TypeScript Compiler APIには、createPrinterというASTからソースコードの文字列を生成する機能があります。この機能を用いて、簡単なソースコードを生成してみましょう。
作業用ディレクトリにtsPrinterSample.tsファイルを作成し、以下を記述します。
import * as ts from 'typescript';
// ts.factory にある関数を使用して AST を作成します
const f = ts.factory;
// ASTの種類に対応したcreate関数を呼び出します
const ast = f.createVariableStatement(
undefined,
f.createVariableDeclarationList(
[
f.createVariableDeclaration(
'greeting',
undefined,
f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
f.createStringLiteral('Hello, World!')
),
],
ts.NodeFlags.Const
)
);
// ASTをコンソールに出力するためのプリンターを作成します
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const source = ts.createSourceFile('sample.ts', '', ts.ScriptTarget.Latest);
const code = printer.printNode(ts.EmitHint.Unspecified, ast, source);
console.log(code);
ターミナルを開き、npm run dev -- ./tsPrinterSample.tsを実行します。
$ npm run dev -- ./tsPrinterSample.ts > basic-project@1.0.0 dev > node --experimental-default-config-file ./tsPrinterSample.ts const greeting: string = "Hello, World!";
create関数で構築したASTを元にconst greeting: string = "Hello, World!";が出力されます。
ASTからソースコードを生成するCLIを実装する
前項の応用として、入力されたデータからASTを構築し、ソースコードを出力するCLIを実装します。
作業用ディレクトリの中に__mocks__ディレクトリを作成し、その中にinput.jsonファイルを作成します。このファイルをCLIの入力として使います。
004-generate-code
└── __mocks__
└── input.json
__mocks__ディレクトリの内容はnpmに公開する際に含めないように、tsconfig.jsonを開いてexcludesフィールドに__mocks__ディレクトリを追記します。
{
// ~~~
"exclude": ["node_modules", "dist", "__mocks__"] // node_modules、出力先のdistフォルダ、__mocks__フォルダは対象外にする
}
__mocks__ディレクトリに追加したinput.jsonファイルを開き、以下を記述します。
{
"kind": "SourceFile",
"children": [
{
"kind": "FirstStatement",
"children": [
{
"kind": "VariableDeclarationList",
"children": [
{
"kind": "VariableDeclaration",
"children": [
{
"kind": "Identifier",
"text": "greeting",
"children": []
},
{
"kind": "StringKeyword",
"text": "string",
"children": []
},
{
"kind": "StringLiteral",
"text": "\"Hello, World!\"",
"children": []
}
]
}
]
}
]
},
{
"kind": "EndOfFileToken",
"children": []
}
]
}
このJSONはconst greeting: string = "Hello, World!";のASTの構造をJSON形式で再現したものです。
作業用ディレクトリの中にgenerateCodeCli.tsファイルを作成します。このファイルでは、JSON形式のASTを入力としてTypeScriptのソースコードを出力するCLIを実装します。
作成したgenerateCodeCli.tsファイルを開き、以下を記述します。
#!/usr/bin/env node
// Node.jsのモジュールをインポート
import fs from 'node:fs/promises';
import path from 'node:path';
// カスタムコンソールモジュールをインポート
import { debug, error, info } from './console.ts';
import { createSourceFromAst } from './createSourceFromAst.ts';
console.log('Welcome to the Generate Code CLI!');
const helpMessage =
'Usage: basic-cli <file-path> [--out <output-path>] [--debug]';
// コマンドの引数を取得(0番目はnodeの実行パス、1番目はスクリプトのパスなので省く)
const args = process.argv.slice(2);
// 引数が不足している場合はヘルプメッセージを表示
if (args.length === 0) {
info(helpMessage);
process.exit(1);
}
let [inputFilePath, ...restArgs] = args;
// コマンドの1つ目の引数に、入力ファイルパスが指定されていない場合はエラーにする
if (!inputFilePath || inputFilePath.startsWith('--')) {
error('Input file path is required');
info(helpMessage);
process.exit(1);
}
let outputFilePath = '';
for (let i = 0; i < restArgs.length; i++) {
const arg = restArgs[i];
if (arg === '--out') {
// --outオプションが指定された場合、次の引数を出力ファイルパスとして取得する
// 次の引数がない、または次の引数がオプション形式(--で始まる)ならエラーにする
if (i + 1 >= args.length || restArgs[i + 1].startsWith('--')) {
error('--out requires a file path');
process.exit(1);
}
outputFilePath = restArgs[++i];
} else if (arg === '--debug') {
// --debugオプションが指定された場合、デバッグモードを有効にする
process.env.DEBUG = 'true';
} else {
error(`Unknown argument: ${arg}`);
process.exit(1);
}
}
// 入力ファイルを読み込む
try {
const absoluteInputPath = path.resolve(inputFilePath);
const data = (await fs.readFile(absoluteInputPath)).toString();
debug(`Input file path: ${absoluteInputPath}`);
debug(`Input file content:\n${data}`);
const ast = JSON.parse(data);
const source = createSourceFromAst(ast);
if (outputFilePath) {
// 出力ファイルパスが指定されている場合、内容を逆順にしてファイルに書き込む
const absoluteOutputPath = path.resolve(outputFilePath);
await fs.writeFile(absoluteOutputPath, source);
debug(`Output written to: ${absoluteOutputPath}`);
} else {
// 出力ファイルパスが指定されていない場合、コンソールに出力する
info(`Source:\n${source}`);
}
} catch (err) {
if (err instanceof Error) {
error(err.message);
} else {
error('An unknown error occurred');
}
process.exit(1);
}
第2回で実装したBasic CLIをベースに受け取ったJSONをcreateSourceFromAst関数に渡して実行し、結果を出力するCLIになっています。
次に、createSourceFromAst関数を実装します。作業用ディレクトリの中にcreateSourceFromAst.tsファイルを作成し、以下を記述します。
import * as ts from 'typescript';
import { debug } from './console.ts';
export type AstNode = {
kind: string;
text: string;
children: AstNode[];
};
const f = ts.factory;
export function createSourceFromAst(astNode: AstNode): string {
const tsNode = buildTsNodeFromAst(astNode);
if (tsNode === null) {
return '';
}
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
if (ts.isSourceFile(tsNode)) {
return printer.printFile(tsNode);
}
const source = ts.createSourceFile(
'generated.ts',
'',
ts.ScriptTarget.Latest
);
return printer.printNode(ts.EmitHint.Unspecified, tsNode, source);
}
function buildTsNodeFromAst(astNode: AstNode, depth = 0): ts.Node | null {
const indent = ' '.repeat(depth);
debug(`${indent}Kind: ${astNode.kind}, Text: ${astNode.text}`);
switch (astNode.kind) {
// SourceFileノードを生成します
case 'SourceFile': {
const statements: ts.Statement[] = [];
for (const child of astNode.children) {
if (child.kind === 'FirstStatement') {
// FirstStatement が複数ある場合、すべての子ノードを処理します
const inner = child.children
.map((c) => buildTsNodeFromAst(c, depth + 2))
.filter((n): n is ts.Statement => n !== null);
statements.push(...inner);
} else {
// それ以外の子ノードは個別に処理します
const node = buildTsNodeFromAst(child, depth + 1);
if (node && ts.isStatement(node)) {
statements.push(node);
}
}
}
return f.createSourceFile(
statements,
f.createToken(ts.SyntaxKind.EndOfFileToken),
ts.NodeFlags.None
);
}
// 変数宣言のリストからVariableStatementノードを生成します
case 'VariableDeclarationList': {
const letHead = /^(\s*)let\b/;
const useLet = letHead.test(astNode.text);
const varDeclaration = astNode.children
.map((child) => buildTsNodeFromAst(child, depth + 1))
.filter((node): node is ts.VariableDeclaration => node !== null);
const varDeclarationList = f.createVariableDeclarationList(
varDeclaration,
useLet ? ts.NodeFlags.Let : ts.NodeFlags.Const
);
return f.createVariableStatement(undefined, varDeclarationList);
}
// 変数宣言の場合、VariableDeclarationノードを生成します
case 'VariableDeclaration': {
const [identifierNode, typeNode, initializerNode] = astNode.children;
return f.createVariableDeclaration(
identifierNode.text,
undefined,
toTsTypeNode(typeNode),
toTsPrimitiveNode(initializerNode)
);
}
// 識別子の場合、Identifierノードを生成します
case 'Identifier':
return f.createIdentifier(astNode.text);
// 型の場合、TypeNodeノードを生成します
case 'StringKeyword':
return toTsTypeNode(astNode);
//Primitiveな値の場合、Literalノードを生成します
case 'StringLiteral':
return toTsPrimitiveNode(astNode);
// ファイルの末尾の改行またはサポートされていないノードの場合、nullを返します
case 'EndOfFileToken':
default:
return null;
}
}
function toTsTypeNode(node: AstNode): ts.TypeNode {
switch (node.kind) {
case 'StringKeyword':
return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
default:
throw new Error(`Unsupported type: ${node.kind}`);
}
}
function toTsPrimitiveNode(node: AstNode): ts.Expression {
switch (node.kind) {
case 'StringLiteral':
const isSingleQuote = node.text.startsWith("'") && node.text.endsWith("'");
return f.createStringLiteral(node.text.slice(1, -1), isSingleQuote);
default:
throw new Error(`Unsupported literal: ${node.kind}`);
}
}
このファイルではinput.jsonに存在するASTのkindごとに、対応するTypeScriptのノードを生成するbuildTsNodeFromAst関数を実装しています。また、これを呼び出してソースコードを生成する関数として、createSourceFromAst関数を実装しています。
では、generateCodeCli.tsファイルを実行してみましょう。ターミナルを開き、以下のコマンドを実行します。
$ npm run dev -- ./generateCodeCli.ts ./__mocks__/input.json > basic-project@1.0.0 dev > node --experimental-default-config-file ./generateCodeCli.ts ./__mocks__/input.json Welcome to the Generate Code CLI! [INFO] Source: const greeting: string = "Hello, World!";
JSON形式のASTを入力として、ソースコードを出力するCLIが完成しました。
ソースコードをFormatするCLIを実装する
前回実装したLinter CLIと、今回実装したASTからソースコードを生成するCLIを応用して、Formatter CLIを実装しましょう。Formatter CLIでは、以下のフローで処理を行います。
入力されたソースコードをASTに変換し、Linterと同様に検査します。ルールに違反している箇所のうち自動修正が可能なものは修正し、ASTからソースコードに変換します。
以上の処理を実装に落とし込んでみましょう。作業用ディレクトリの中にconfig.jsonファイル、formatterCli.tsファイル、createAst.ts ファイルを作成して以下を記述します。前回のLinter CLIの実装を一部改変しています。
config.jsonファイル:
{
"use-let-never-reassigned": "error",
"double-quotes": "error"
}
formatterCli.tsファイル:
#!/usr/bin/env node
// Node.jsのモジュールをインポートします
import fs from 'node:fs/promises';
import path from 'node:path';
// カスタムコンソールモジュールをインポートします
import { debug, error, info } from './console.ts';
import { createAst } from './createAst.ts';
import { createSourceFromAst } from './createSourceFromAst.ts';
import { fixAst } from './fixAst.ts';
// configの型を定義します
export type Config = {
'use-let-never-reassigned': 'error' | 'warn' | 'off';
'double-quotes': 'error' | 'warn' | 'off';
};
console.log('Welcome to the Formatter CLI!');
const helpMessage =
'Usage: formatter-cli <file-path> [--out <output-path>] [--debug]';
// コマンドの引数を取得(0番目はnodeの実行パス、1番目はスクリプトのパスなので省きます)
const args = process.argv.slice(2);
// 引数が不足している場合はヘルプメッセージを表示します
if (args.length === 0) {
info(helpMessage);
process.exit(1);
}
let [inputFilePath, ...restArgs] = args;
// コマンドの1つ目の引数に、入力ファイルパスが指定されていない場合はエラーにします
if (!inputFilePath || inputFilePath.startsWith('--')) {
error('Input file path is required');
info(helpMessage);
process.exit(1);
}
let outputFilePath = '';
for (let i = 0; i < restArgs.length; i++) {
const arg = restArgs[i];
if (arg === '--out') {
// --outオプションが指定された場合、次の引数を出力ファイルパスとして取得します
// 次の引数がない、または次の引数がオプション形式(--で始まる)ならエラーにします
if (i + 1 >= args.length || restArgs[i + 1].startsWith('--')) {
error('--out requires a file path');
process.exit(1);
}
outputFilePath = restArgs[++i];
} else if (arg === '--debug') {
// --debugオプションが指定された場合、デバッグモードを有効にします
process.env.DEBUG = 'true';
} else {
error(`Unknown argument: ${arg}`);
process.exit(1);
}
}
// コマンドが実行されたディレクトリにconfigファイルがない場合はエラーにします
const configFilePath = path.resolve('config.json');
const isConfigFileExists = await fs
.access(configFilePath)
.then(() => true)
.catch(() => false);
if (!isConfigFileExists) {
error('Config file not found: config.json');
process.exit(1);
}
// 入力ファイルを読み込む
try {
const absoluteInputPath = path.resolve(inputFilePath);
const data = (await fs.readFile(absoluteInputPath)).toString();
debug(`Input file path: ${absoluteInputPath}`);
debug(`Input file content:\n${data}`);
// configファイルを読み込みます
const config: Config = JSON.parse(
(await fs.readFile(configFilePath)).toString()
);
debug(`Config file content:\n${JSON.stringify(config, null, 2)}`);
const ast = await createAst(absoluteInputPath);
// AST を走査してルールを元に修正します
const fixedAst = fixAst(ast, 0, config, ast);
// AST をソースコードに変換し、ファイルに書き込みます
const source = createSourceFromAst(fixedAst);
if (outputFilePath) {
const absoluteOutputPath = path.resolve(outputFilePath);
await fs.writeFile(absoluteOutputPath, source);
debug(`Output written to: ${absoluteOutputPath}`);
} else {
// 出力ファイルパスが指定されていない場合、コンソールに出力します
info(`Formatted source:\n${source}`);
}
} catch (err) {
if (err instanceof Error) {
error(err.message);
} else {
error('An unknown error occurred');
}
process.exit(1);
}
createAst.ts ファイル:
import { readFile } from 'fs/promises';
import ts from 'typescript';
export type AstNode = {
kind: string;
text: string;
children: AstNode[];
};
function toAstNode(node: ts.Node) {
const result: AstNode = {
kind: ts.SyntaxKind[node.kind],
text: node.getText(),
children: [],
};
ts.forEachChild(node, (child) => {
result.children.push(toAstNode(child));
});
return result;
}
export async function createAst(fileName: string) {
const sourceCode = await readFile(fileName, 'utf8');
const sourceFile = ts.createSourceFile(
fileName,
sourceCode,
ts.ScriptTarget.Latest,
true
);
return toAstNode(sourceFile);
}
作業用ディレクトリ内の__mocks__フォルダにinvalidInput.tsファイル、validInput1.tsファイル、validInput2.tsファイルを追加し、それぞれ以下を記述します。
invalidInput.tsファイル:
let message: string = "This method has fixable errors."; console.log(message);
validInput1.tsファイル:
const message: string = 'This method has fixable errors.'; console.log(message);
validInput2.tsファイル:
let message: string = 'This method has fixable errors.'; message = 'Reassigned'; console.log(message);
前項で作成したcreateSourceFromAst.tsファイルを開き、以下のように改変します。この改変で、先ほど追加した__mocks__以下の3つの入力向けTypeScriptファイルのASTをソースコードに変換できます。
import * as ts from 'typescript';
import { debug } from './console.ts';
export type AstNode = {
kind: string;
text: string;
children: AstNode[];
};
const f = ts.factory;
export function createSourceFromAst(astNode: AstNode): string {
const tsNode = buildTsNodeFromAst(astNode);
if (tsNode === null) {
return '';
}
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
if (ts.isSourceFile(tsNode)) {
return printer.printFile(tsNode);
}
const source = ts.createSourceFile(
'generated.ts',
'',
ts.ScriptTarget.Latest
);
return printer.printNode(ts.EmitHint.Unspecified, tsNode, source);
}
function buildTsNodeFromAst(astNode: AstNode, depth = 0): ts.Node | null {
const indent = ' '.repeat(depth);
debug(`${indent}Kind: ${astNode.kind}, Text: ${astNode.text}`);
switch (astNode.kind) {
// SourceFileノードを生成します
case 'SourceFile': {
const statements: ts.Statement[] = [];
for (const child of astNode.children) {
if (child.kind === 'FirstStatement') {
// FirstStatement が複数ある場合、すべての子ノードを処理します
const inner = child.children
.map((c) => buildTsNodeFromAst(c, depth + 2))
.filter((n): n is ts.Statement => n !== null);
statements.push(...inner);
} else {
// それ以外の子ノードは個別に処理します
const node = buildTsNodeFromAst(child, depth + 1);
if (node && ts.isStatement(node)) {
statements.push(node);
}
}
}
return f.createSourceFile(
statements,
f.createToken(ts.SyntaxKind.EndOfFileToken),
ts.NodeFlags.None
);
}
// 変数宣言のリストからVariableStatementノードを生成します
case 'VariableDeclarationList': {
const letHead = /^(\s*)let\b/;
const useLet = letHead.test(astNode.text);
const varDeclaration = astNode.children
.map((child) => buildTsNodeFromAst(child, depth + 1))
.filter((node): node is ts.VariableDeclaration => node !== null);
const varDeclarationList = f.createVariableDeclarationList(
varDeclaration,
useLet ? ts.NodeFlags.Let : ts.NodeFlags.Const
);
return f.createVariableStatement(undefined, varDeclarationList);
}
// 変数宣言の場合、VariableDeclarationノードを生成します
case 'VariableDeclaration': {
const [identifierNode, typeNode, initializerNode] = astNode.children;
return f.createVariableDeclaration(
identifierNode.text,
undefined,
toTsTypeNode(typeNode),
toTsPrimitiveNode(initializerNode)
);
}
// 識別子の場合、Identifierノードを生成します
case 'Identifier':
return f.createIdentifier(astNode.text);
// 型の場合、TypeNodeノードを生成します
case 'StringKeyword':
return toTsTypeNode(astNode);
//Primitiveな値の場合、Literalノードを生成します
case 'StringLiteral':
return toTsPrimitiveNode(astNode);
// 式文の場合、ExpressionStatementノードを生成します
case 'ExpressionStatement': {
const expr = buildTsNodeFromAst(astNode.children[0], depth + 1);
return expr ? f.createExpressionStatement(expr as ts.Expression) : null;
}
// 代入式の場合、BinaryExpressionノードを生成します
case 'BinaryExpression': {
const [leftNode, _, rightNode] = astNode.children;
const left = buildTsNodeFromAst(leftNode, depth + 1) as ts.Expression;
const right = buildTsNodeFromAst(rightNode, depth + 1) as ts.Expression;
return f.createBinaryExpression(
left,
f.createToken(ts.SyntaxKind.EqualsToken),
right
);
}
// 関数呼び出しの場合、CallExpressionノードを生成します
case 'CallExpression': {
const [calleeNode, argNode] = astNode.children;
const callee = buildTsNodeFromAst(calleeNode, depth + 1) as ts.Expression;
const arg = buildTsNodeFromAst(argNode, depth + 1) as ts.Expression;
return f.createCallExpression(callee, undefined, [arg]);
}
// プロパティへのアクセスの場合、PropertyAccessExpressionノードを生成します
case 'PropertyAccessExpression': {
const [objNode, propNode] = astNode.children;
const obj = buildTsNodeFromAst(objNode, depth + 1) as ts.Expression;
const prop = buildTsNodeFromAst(propNode, depth + 1) as ts.Identifier;
return f.createPropertyAccessExpression(obj, prop);
}
// BinaryExpressionで生成するため、FirstAssignmentは無視します
case 'FirstAssignment':
// ファイルの末尾の改行またはサポートされていないノードの場合、nullを返します
case 'EndOfFileToken':
default:
return null;
}
}
function toTsTypeNode(node: AstNode): ts.TypeNode {
switch (node.kind) {
case 'StringKeyword':
return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
default:
throw new Error(`Unsupported type: ${node.kind}`);
}
}
function toTsPrimitiveNode(node: AstNode): ts.Expression {
switch (node.kind) {
case 'StringLiteral':
const isSingleQuote =
node.text.startsWith("'") && node.text.endsWith("'");
return f.createStringLiteral(node.text.slice(1, -1), isSingleQuote);
default:
throw new Error(`Unsupported literal: ${node.kind}`);
}
}
作業用ディレクトリにfixAst.tsファイルを作成し、以下を記述します。Formatter CLIのコアの実装です。
import type { AstNode } from './createAst.ts';
import type { Config } from './formatterCli.ts';
import { debug, info } from './console.ts';
export function fixAst(astNode: AstNode, depth = 0, config: Config, root: AstNode): AstNode {
const indent = ' '.repeat(depth);
debug(
`${indent}Kind: ${astNode.kind}, Text: ${astNode.text.replaceAll('\n', ' ')}`
);
// configのuse-let-never-reassignedが有効な場合、ルールを適用してASTを修正します
if (config['use-let-never-reassigned'] !== 'off') {
const letHead = /^(\s*)let\b/;
if (
astNode.kind === 'VariableDeclarationList' &&
letHead.test(astNode.text)
) {
// 宣言に含まれる識別子名を抽出します
const identifiers = astNode.children
.filter((c) => c.kind === 'VariableDeclaration')
.map(
(v) =>
v.children.find((n) => n.kind === 'Identifier')?.text || '__dummy__'
);
// 後続で再代入が無ければ const に置換します
if (isNeverReassigned(root, astNode, identifiers)) {
astNode.text = astNode.text.replace(letHead, '$1const');
info('[use-let-never-reassigned] letをconstに置換しました');
}
}
}
// configのdouble-quotesが有効な場合、ルールを適用してASTを修正します
if (config['double-quotes'] !== 'off') {
if (astNode.kind === 'StringLiteral') {
const text = astNode.text.trim();
if (text.startsWith('"') && text.endsWith('"')) {
// 本体を取り出し、シングルクォートをエスケープします
const inner = text.slice(1, -1).replace(/'/g, "\\'");
astNode.text = `'${inner}'`;
info('[double-quotes] ダブルクオートをシングルクオートに修正しました');
}
}
}
for (const child of astNode.children) {
// 子ノードを再帰的に修正します
fixAst(child, depth + 1, config, root);
}
return astNode;
}
/**
* let → const に変換して良いか判定するユーティリティ関数です
*/
function isNeverReassigned(
root: AstNode,
declNode: AstNode,
identifiers: string[]
): boolean {
/** 変数宣言ノード自身は検索対象から除外したいので引数で受け取り比較します */
const walk = (node: AstNode): boolean => {
if (node === declNode) return false; // 宣言部分は無視します
// `Identifier = ...` という BinaryExpression があれば再代入とみなします
if (
node.kind === 'BinaryExpression' &&
node.children.length >= 1 &&
node.children[0].kind === 'Identifier' &&
identifiers.includes(node.children[0].text)
) {
return true;
}
return node.children.some(walk);
};
return !walk(root);
}
ターミナルを開き、__mocks__ディレクトリのinvalidInput.tsファイルがFormatされるか確かめましょう。
$ npm run dev -- ./formatterCli.ts ./__mocks__/invalidInput.ts > basic-project@1.0.0 dev > node --experimental-default-config-file ./formatterCli.ts ./__mocks__/invalidInput.ts Welcome to the Formatter CLI! [INFO] [use-let-never-reassigned] letをconstに置換しました [INFO] [double-quotes] ダブルクオートをシングルクオートに修正しました [INFO] Formatted source: const message: string = 'This method has fixable errors.'; console.log(message);
ダブルクオートがシングルクオートに、再代入されていないletによる変数宣言がconstに書き換わっています。
では、引き続きターミナルで__mocks__ディレクトリのvalidInput1.tsファイルとvalidInput2.tsファイルがFormatされないか確かめましょう。
validInput1.tsファイルを入力した結果:
$ npm run dev -- ./formatterCli.ts ./__mocks__/validInput1.ts > basic-project@1.0.0 dev > node --experimental-default-config-file ./formatterCli.ts ./__mocks__/validInput1.ts Welcome to the Formatter CLI! [INFO] Formatted source: const message: string = 'This method has fixable errors.'; console.log(message);
validInput2.tsファイルを入力した結果:
$ npm run dev -- ./formatterCli.ts ./__mocks__/validInput2.ts > basic-project@1.0.0 dev > node --experimental-default-config-file ./formatterCli.ts ./__mocks__/validInput2.ts Welcome to the Formatter CLI! [INFO] Formatted source: let message: string = 'This method has fixable errors.'; message = 'Reassigned'; console.log(message);
どちらの入力ファイルもルールに違反していないので、Formatは行われません。
これでFormatter CLIが完成しました。CLIを公開する場合は、第2回のnpmへの公開手順を参照ください。
以上のコードはGitHubのリポジトリに掲載しています。併せてご覧ください。
次回は、最終回として今まで実装してきた内容の振り返りと、現場で広く用いられているLinterやFormatterのソースコードの一部をリーディングし、どういった処理になっているのかを概観します。
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- ASTの解析とCLIの実装によるTypeScript製Lintツール開発
- TypeScript×Node.jsでCLIを開発してnpmで公開する基本手順を学ぶ
- ASTの基礎知識とTypeScriptの環境構築から始めるCLIツール開発
- 「Ace」を使って「TAURI」で「テキストエディタ」アプリを作ろう
- 「K8sGPT」の未来と生成AIを用いたKubernetes運用の最前線
- ECMAScript
- ES2015で導入された、より洗練された構文 Part 1
- ES2015が備えるモダンな非同期処理
- 「WebGPU」でシェーダーを使って三角形を1つだけ描画してみよう
- 「Flutter」のプロジェクト構造と状態管理でアプリ開発を標準化する


