同じ名前で分割代入する場合はショートハンドで強制するESLintルールをTypeScriptで作った
こんにちは!CastingONEの大沼です。
始めに
弊社ではESLintルールのobject-shorthandを設定しており、ショートハンドで書けるものはショートハンドで強制されるようにしています。これによってコードがスッキリしたものになるのですが、一部ショートハンドにならないケースがありました。
const hoge = 'hogehoge';
const obj = {
hoge: hoge, // これはautofixしてくれる
fuga: 100,
};
const {
fuga: fuga, // これはそもそもlintエラーにならない
} = obj;
ルールの内容をよく見るとどうやらオブジェクトを作る際のショートハンドのみ見ているようで、分割代入の方は対象外ということが分かりました。代わりのルールを探してみたのですが、最も近そうなプラグインがeslint-plugin-prefer-destructuringで、ルールの内容を見るとショートハンドに関するものはありませんでした。
無いのであれば作れば良い!ということでカスタムルールを作ってみましたのでその内容についてまとめました。
作ったもの
先に今回作ったものをお見せしますと、以下のgifアニメのようなものになります。なお、エディタ上で赤くなっているのはTypeScriptのエラーでESLintのエラー内容では無いので注意してください。gifアニメのものはStackBlitzで作ったものになっており、StackBlitzではESLintのエラーをエディタ上に表示することはできません。
詳細のコードや動作を確認したい方は以下にStackBlitzで作ったものを埋め込んでおりますのでこちらをご参照ください。ESLintの実行はnpm run lint
で、autofixを試す場合はnpm run lint:fix
をターミナルで実行していただければ確認できます。
カスタムルールの作成
対象となるコードのAST構造を理解する
まず最初にESLintルールでマッチする条件を設定するために、分割代入している場所がASTだとどういう構造になっているか調べます。
ASTの解析は以下に貼ったAST Explorerを使うと非常に分かりやすいです。
このサイトでサンプルコードを書いて、分割代入されているところがどうなっているか見ます。
まず const { hoge, foo: foo } = obj
のようなコードですが、これを書いてAST Explorerで見るとスクショのようなASTツリーが表示され、 VariableDeclarator > ObjectPattern > Property
という構造になっていることが分かります。
Property
の詳細をショートハンドの時とそうで無い時を見ると、shorthand
というプロパティがtrue/falseとなっており、これがfalseの時、かつkey.name
とvalue.name
が一致しているときにESLintでエラーとして検知すると良さそうです。
分割代入のケースはアロー関数や通常の関数の場合もあり、こちらの構造も確認しました。 Property
の部分はどこも同じ構造だったので大枠のところのみ載せます。
アロー関数: VariableDeclarator > ArrowFunctionExpression > ObjectPattern > Property
通常の関数: FunctionDeclaration > ObjectPattern > Property
AST構造を元にカスタムルールを作成
エラーとして検知させる部分が分かったので実際にルールを作っていきます。今回はTypeScriptで作りますので、以下のパッケージをインストールします。
npm install -D eslint @types/eslint typescript
まずはESLintのカスタムルールの大枠を書きます。meta
にカスタムルールの情報を書きますが、今回はリファクタするものなのでtype: 'suggestion'
とし、autofixできるようにするためfixable: 'code'
を入れています。create
にルールの内容について定義するのですが、ESLintのカスタムルールはビジターパターンとなっており、特定のブロックに処理を差し込むように設定します。今回はVariableDeclarator
とFunctionDeclaration
にアクセスしたいので以下のように書きます。
import type { Rule } from 'eslint';
import type { AssignmentProperty, RestElement } from 'estree';
export const preferDestructuringShorthand: Rule.RuleModule = {
meta: {
type: 'suggestion', // リファクタ内容なのでsuggestion
fixable: 'code', // autofix可能なので設定
},
create(context) {
// チェックしたいASTブロックをオブジェクトの関数で定義
return {
VariableDeclarator: (node) => {
// 変数宣言ブロックの処理
},
FunctionDeclaration: (node) => {
// 関数宣言ブロックの処理
},
};
},
};
VariableDeclarator
とFunctionDeclaration
の第一引数であるnode
は前セクションで確認したAST構造がそのまま入っているので、それに従ってコードを書いていけば良いです。Property
部分はどれも同じものが入っていたのでそこの処理は共通化されるように書きます。
import type { Rule } from 'eslint';
import type { AssignmentProperty, RestElement } from 'estree';
export const preferDestructuringShorthand: Rule.RuleModule = {
meta: {
type: 'suggestion',
fixable: 'code',
},
create(context) {
/**
* Propertyブロックのチェック
* @param properties - Propertyブロックリスト
*/
const checkProperties = (
properties: Array<AssignmentProperty | RestElement>
) => {
// 詳細の実装は後述
};
return {
VariableDeclarator: (node) => {
if (node.init) {
// アロー関数のパターン
if ('params' in node.init) {
node.init.params.forEach((param) => {
if (param.type !== 'ObjectPattern') {
return;
}
checkProperties(param.properties);
});
}
}
// オブジェクトのパターン
if (node.id.type === 'ObjectPattern') {
checkProperties(node.id.properties);
}
},
// 通常の関数のパターン
FunctionDeclaration: (node) => {
node.params.forEach((param) => {
if (param.type !== 'ObjectPattern') {
return;
}
checkProperties(param.properties);
});
},
};
},
};
最後にProperty
ブロックの確認を実装します。AST Explorerで確認した内容を元に合致しないケースは早期リターンで除外していき、最終的に残った部分をcontext.report
でESLintに修正が可能な箇所だと通知します。
import type { Rule } from 'eslint';
import type { AssignmentProperty, RestElement } from 'estree';
export const preferDestructuringShorthand: Rule.RuleModule = {
// metaの設定は同じなので省略
create(context) {
/**
* Propertyブロックのチェック
* @param properties - Propertyブロックリスト
*/
const checkProperties = (
properties: Array<AssignmentProperty | RestElement>
) => {
properties.forEach((property) => {
if (property.type !== 'Property') {
return;
}
if (property.shorthand) {
return;
}
const { key, value } = property;
if (key.type !== 'Identifier' || value.type !== 'Identifier') {
return;
}
if (key.name !== value.name) {
return;
}
// 修正が可能な箇所と通知
context.report({
node: property, // 修正対象のASTブロック
message:
'同じ名前で分割代入する場合はショートハンドを使用してください',
// 修正コード
fix(fixer) {
return fixer.replaceText(property, key.name);
},
});
});
};
return {
// returnオブジェクトの内容は同じなので省略
};
},
};
カスタムルールを設定
カスタムルールが完成したので、これを実際にESLintルールに設定します。以下の記事のように最近はTypeScriptのままでも実行できるようになったのでその方法で試しました。
ESLintのセットアップ
まずは基本となるESLintを用意します。基本ルールのインストールと、今回はTypeScriptで実行するのでjiti
も合わせてインストールします。
npm install -D @eslint/js typescript-eslint jiti
eslint.config.mts
に以下のようなコードを書きます。
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
extends: [eslint.configs.recommended, ...tseslint.configs.recommended],
});
カスタムルールを追加
次にこのコンフィグにカスタムルールを追加します。カスタムルールを追加するにはプラグインにする必要があるので、まずはそれを作ります。rules
プロパティの中にルール名をキーとして先ほど作ったカスタムルールのコードを入れるだけでプラグインはできます。
import type { ESLint } from 'eslint';
import { preferDestructuringShorthand } from './prefer-destructuring-shorthand';
const plugin = {
rules: {
'prefer-destructuring-shorthand': preferDestructuringShorthand,
},
} satisfies ESLint.Plugin;
export default plugin;
これをimportしてプラグインとして登録することでカスタムルールを使うことができます。先頭の名前はplugins
のキー名と一致させる必要があるのでそこは注意してください。今回はlocal
というキー名でプラグインを登録しているのでルール名がlocal/
を先頭につける必要があります。
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
+import localPlugin from './rules';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
extends: [eslint.configs.recommended, ...tseslint.configs.recommended],
+ plugins: {
+ local: localPlugin,
+ },
+ rules: {
+ 'local/prefer-destructuring-shorthand': 'error',
+ },
});
カスタムルールの動作確認
早速ESLintを実行すると以下のようにカスタムルールのエラーが出てくるようになりました🎉
StackBlitz上ではコード上にはエラーメッセージは表示できませんが、弊社の環境でこのルールを入れた時にはしっかりとVSCode上でエラーが出ていました。
カスタムルールのテスト
最後に作成したカスタムルールのテストを書きたいと思います。今回はvitest
を使ってテストしたいのでこれをインストールします。
npm install -D vitest
テスト用の設定ファイルは通常のテストと別にできるようにファイル名をvitest.rules.config.ts
にして以下のようにrules
ディレクトリ配下のテストコードのみ実行されるようにしました。
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
root: './',
include: ['rules/**/*.test.ts'],
},
});
テストコードは以下のようにESLintのRuleTester
を使って、validとinvalidのケースを用意します。validはcode
のみでよく、invalidの場合はエラーの内容とautofixをする場合はfix後のコードを定義します。name
はテスト実行時に出てくる名前なので見やすくしたい場合は任意で設定してください。未指定の場合はcode
がそのまま表示されます。
import { RuleTester } from 'eslint';
import { preferDestructuringShorthand } from './prefer-destructuring-shorthand';
const ruleTester = new RuleTester();
const errorMessage = '同じ名前で分割代入する場合はショートハンドを使用してください';
ruleTester.run('prefer-destructuring-shorthand', preferDestructuringShorthand, {
valid: [{ name: '正常系', code: 'const { hoge, foo: test } = obj' }],
invalid: [
{
name: 'オブジェクトの分割代入でショートハンドが可能な時はショートハンドで修正される',
code: 'const { hoge: hoge } = obj',
output: 'const { hoge } = obj',
errors: [{ message: errorMessage }],
},
{
name: 'アロー関数の分割代入でショートハンドが可能なときはショートハンドで修正される',
code: 'const func = ({ hoge: hoge }) => hoge',
output: 'const func = ({ hoge }) => hoge',
errors: [{ message: errorMessage }],
},
{
name: 'functionの分割代入でショートハンドが可能なときはショートハンドで修正される',
code: 'function func({ hoge: hoge }) { return hoge }',
output: 'function func({ hoge }) { return hoge }',
errors: [{ message: errorMessage }],
},
],
});
vitest -c vitest.rules.config.ts run
をnpmのタスクとして定義して、そのタスクを実行すると以下のスクショのようにテスト実行ができました。
終わりに
以上が同じ名前で分割代入する場合はショートハンドで強制するESLintのカスタムルールをTypeScriptで作る内容でした。今までカスタムルールはeslint-plugin-local-rules
など所定の場所に配置して使う必要がありましたが、ESLintが9系になってから直接プラグインとして登録できるようになり、更に最近ではTypeScriptのまま実行できるようになってかなりカスタムルールの導入がやりやすくなったなと思いました。
カスタムルールの作成や分割代入でもショートハンドを強制したい人の参考になれば幸いです。
参考記事
Discussion
本体のルールに
no-useless-rename
というのがあります。https://eslint.org/docs/latest/rules/no-useless-renameコメントありがとうございます!ChatGPTに聞いてもないとハッキリ言われて止むを得ずカスタムルールを作りましたが、本体にあるのであればそれを使うのが良いですね。記事の冒頭にもその旨を追記したいと思います。
let の対応とかできないでしょうか……?
letを使ってそんな風に書いて代入することができるんですね!知りませんでした。。
他のケースと同じようにこのコードのASTの構造を解析すればできるので試しにやってみました。
AssignmentExpression > ObjectPattern > Property
という構造だったのでそれをルールに足したらできました!