この記事は「イエソド アウトプット筋 トレーニング Advent Calendar 2020」21日目の記事です。
Custom (local) ESLint rules for TypeScript
前回の投稿でJavaScript向けのeslintの独自ruleの設定の方法について書きました。
今回はTypeScriptプロジェクトでも同様のことができることを示していこうと思います。
環境
MacOS Catalina 10.15.7
node 14.15.3
TypeScriptプロジェクト
せっかく前回の投稿で用意したリポジトリがあるので、それをベースにしていきます。
$ git clone git@github.com:swallowtail62/eslint-local-rule.git
$ npm i -D typescript @tsconfig/node14 // tsconfig.jsonのベースに使用
$ npx tsc --init
npx tsc --init
で生成されたtsconfig.json
を修正
{
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
},
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["node_modules"]
}
今回はNode14.15.3を使用するので、Node14系を対象としたtsconfigのベースを基底にします。
Convert js -> ts
以前用意していあったファイルとeslintの設定は、現状こんな感じです。
module.exports = {
... // 省略
rules: {
'rulesdir/ban-variable-name': ['error', { words: [ 'hoge', 'fuga' ] }]
}
};
$ npx eslint ./src
/Users/path-to-dir/eslint-local-rule/src/index.js
3:7 error The keyword hoge is banned to be used as a variable name rulesdir/ban-variable-name
9:7 error The keyword fuga is banned to be used as a variable name rulesdir/ban-variable-name
9:7 error 'fuga' is assigned a value but never used no-unused-vars
custom ruleが効いているのが分かりますね。(spellチェックの警告はスコープ外のため無視)
では、index.js
の拡張子をts
に変更します。
すると、VSCode上でのeslintの警告はなくなりました。
tscのエラーはありますが、本題ではないので割愛。
{
"compilerOptions": {
"lib": ["DOM"],
}
}
を追加すればOKです。ただ、今回は"@tsconfig/node14/tsconfig.json"をベースとしていて、そちらでlib
を定義しており、tsconfig.json
では同一プロパティの宣言はmergeではなくoverride扱いとなるため、
{
"compilerOptions": {
+ "lib": ["ES2020", "DOM"],
}
}
とする必要があります。
さて、cliから今まで通り、eslintを実行してみると、
$ npx eslint ./src
Oops! Something went wrong! :(
ESLint: 7.15.0
No files matching the pattern "./src" were found.
Please check for typing mistakes in the pattern.
lint対象がないと言われました。eslintでは拡張子の指定がない限り、対象は*.js
に指定されるからです。
https://eslint.org/docs/user-guide/configuring#specifying-target-files-to-lint
なので、コマンドラインオプションで拡張子を指定して改めて実行してみると
$ npx eslint ./src --ext .js,.ts
/Users/path-to-dir/eslint-local-rule/src/index.ts
3:7 error The keyword hoge is banned to be used as a variable name rulesdir/ban-variable-name
9:7 error The keyword fuga is banned to be used as a variable name rulesdir/ban-variable-name
9:7 error 'fuga' is assigned a value but never used no-unused-vars
✖ 3 problems (3 errors, 0 warnings)
js
の頃同様、lint errorが出ました
ファイル拡張子的にはts
でもTypeScirpt特有のシンタックスが出てこない限りは、parserがJavaScript用のものでも問題ないんですね。
ただ、eslint実行の度に--ext .js,.ts
を指定するのは面倒なので、package.json
にタスクを作ってしまいます。
{
"scripts": {
+ "lint": "eslint ./src --ext .js,.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
}
parser, parserOptions
const str: string = 'some string';
上記の型アノテーション付きのような、TypeScirpt特有のシンタックスをASTに変換するには、TypeScirptのために用意されたparserを使用する必要があります。
(eslintの設定ファイルで特に何も指定していない場合、デフォルトのespreeが使用されます。)
https://eslint.org/docs/user-guide/configuring#specifying-parser
TypeScript用のparserとして、@typescript-eslint/parserが用意されているので、ここではこちらを使用します。
(他にもbabel-parserなんかもTypeScriptのparserとして使用できます。)
$ npm i -D @typescript-eslint/parser
https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#parseroptionsproject に従い、eslint設定ファイルにて、parser
及びparserOptions.project
を指定します。
module.exports = {
root: true,
env: {
node: true,
},
+ parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
+ project: './tsconfig.json',
}
}
こうすることでeslintは、@typescript-eslint/parser
を用いて、./tsconfig.json
で指定したinclude
プロパティを対象にlintをかけてくれます。
"include": ["src/**/*.ts", "src/**/*.js"],
ただ、include
に含まれないファイル、ここでは
.
├── eslint
│ └── rules
│ └── ban-variable-name.js
└── .eslintrc.js
上記の2ファイルをVSCodeで表示している時、
のようにproject(lint対象)に含めるよう警告が出ます。この対応としては、
lint対象としたい場合は
"include": ["src/**/*.ts", "src/**/*.js", "eslint/**/*.js", ".eslintrc.js"],
とすればいいし、lint対象から除外したい場合は、(.eslintrc.js
なんて意味ないですし)
.eslintrc.js
eslint
のようなファイルをプロジェクトルートに配置しておけば、eslintがそれを読み込んでよしなに解釈してくれます。(記法は.gitignore
等と同じ感じです)
custom rule 拡張
せっかくなので型を使って何かしら制約をかけようと思いましたが、なかなかいいのが思い浮かばないのと、前回の拡張でいいかなという思いで、
string型の特定の変数名を禁止する
という使い道のないクソlintを作りたいと思います。
まずは、index.ts
を以下のように修正します。
const fuga: number = 100;
すると、まだ禁止する型をstring
に限定していないため、eslintのerrorが発生します。
ruleをTypeScript化
せっかくなので、前回作成したrule ban-variable-name
をTypeScript化します。
@typescript-eslint
がASTの型を提供してくれているので、それを利用します。また、module
を使用するために@types/node
もインストールします。
$ npm i -D @typescript-eslint/types @types/node
https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/types/src/ts-estree.ts
を見てみると各Nodeの型情報が定義されているので、セレクタで指定していたVariableDeclarator
のNodeの型定義を使用し、TypeScript風にリファクタすると以下のようになります。
import { TSESTree } from "@typescript-eslint/types";
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Ban some keywords from being used as a variable name",
category: "Variables",
},
schema: [
{
type: "object",
properties: {
words: {
type: "array",
items: {
type: "string",
},
},
},
additionalProperties: false,
},
],
},
create(context) {
const [option] = context.options;
const bannedWords = option?.words ?? [];
return {
VariableDeclarator: (node: TSESTree.VariableDeclarator) => {
const { id } = node as { id: TSESTree.Identifier };
if (bannedWords.includes(id.name)) {
context.report({
node,
message: `The keyword ${id.name} is banned to be used as a variable name`,
});
}
},
};
},
};
変数Nodeの型情報(TypeAnnotation)はここに定義されています。また、型情報はこのように定義されています。
なので、string型であるか判定するためのロジックは以下のようになります。
+ import { TSESTree, AST_NODE_TYPES } from "@typescript-eslint/types";
module.exports = {
...
create(context) {
const [option] = context.options;
const bannedWords = option?.words ?? [];
return {
VariableDeclarator: (node: TSESTree.VariableDeclarator) => {
const { id } = node as { id: TSESTree.Identifier };
+ if (
+ bannedWords.includes(id.name) &&
+ id.typeAnnotation?.typeAnnotation.type ===
+ AST_NODE_TYPES.TSStringKeyword
+ ) {
context.report({
node,
message: `The keyword ${id.name} is banned to be used as a variable name`,
});
}
},
};
},
こうすることでstring型の特定の変数名のみ禁止というようなruleに変更することができました。あとはこれをトランスパイルして
$ npx tsc eslint/rules/ban-variable-name.ts
$ tree eslint
eslint
└── rules
├── ban-variable-name.js
└── ban-variable-name.ts
トランスパイル後のjsファイルの中身
"use strict";
exports.__esModule = true;
var types_1 = require("@typescript-eslint/types");
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Ban some keywords from being used as a variable name",
category: "Variables"
},
schema: [
{
type: "object",
properties: {
words: {
type: "array",
items: {
type: "string"
}
}
},
additionalProperties: false
},
]
},
create: function (context) {
var _a;
var option = context.options[0];
var bannedWords = (_a = option === null || option === void 0 ? void 0 : option.words) !== null && _a !== void 0 ? _a : [];
return {
VariableDeclarator: function (node) {
var _a;
var id = node.id;
if (bannedWords.includes(id.name) &&
((_a = id.typeAnnotation) === null || _a === void 0 ? void 0 : _a.typeAnnotation.type) ===
types_1.AST_NODE_TYPES.TSStringKeyword) {
context.report({
node: node,
message: "The keyword " + id.name + " is banned to be used as a variable name"
});
}
}
};
}
};
index.ts
を修正して
const fuga: number = 100;
console.log(fuga); // rule`no-unused-vars`を抑制するため
eslintを実行すると
$ npm run lint
> eslint-local-rule@1.0.0 lint /Users/path-to-dir/eslint-local-rule
> eslint ./src --ext .js,.ts
errorはでません。逆にstring
型に変更すると、
const fuga: string = 100;
console.log(fuga); // rule`no-unused-vars`を抑制するため
$ npm run lint
> eslint-local-rule@1.0.0 lint /Users/path-to-dir/private/eslint-local-rule
> eslint ./src --ext .js,.ts
/Users/path-to-dir/eslint-local-rule/src/index.ts
1:7 error The keyword fuga is banned to be used as a variable name rulesdir/ban-variable-name
✖ 1 problem (1 error, 0 warnings)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! eslint-local-rule@1.0.0 lint: `eslint ./src --ext .js,.ts`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the eslint-local-rule@1.0.0 lint script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/~/.npm/_logs/2020-12-21T10_23_32_934Z-debug.log
こんな風にerrorとなります。
以上で一通りクソeslint ruleを作成できました。
これからruleをts
で管理していくとなると、元々git管理していたjs
は不要になるので、commitする前に
$ git rm eslint/rules/ban-variable-name.js
{
"scripts": {
+ "build:eslint": "tsc eslint/rules/*.ts",
+ "lint": "npm run build:eslint && eslint ./src --ext .js,.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
}
とすることで、以降は
$ npm run lint
でruleのトランスパイルからlint実行までできます。
本記事の実装内容は全てこちらのブランチで進めています。
また、今回は使いませんでしたが、0からTypeScriptでcustom eslint ruleを作成するには、
https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/experimental-utils/src/eslint-utils/RuleCreator.ts
が便利かと思います。
非常に拙い記事になってしまいましたが、以上。