Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?
@swallowtail62

Custom (local) ESLint rules for TypeScript

この記事は「イエソド アウトプット筋 トレーニング 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を修正

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の設定は、現状こんな感じです。

image.png

.eslintrc.js
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に変更します。

image.png

すると、VSCode上でのeslintの警告はなくなりました。


tscのエラーはありますが、本題ではないので割愛。
tsconfig.json
{
  "compilerOptions": {
    "lib": ["DOM"],
  }
}

を追加すればOKです。ただ、今回は"@tsconfig/node14/tsconfig.json"をベースとしていて、そちらでlibを定義しており、tsconfig.jsonでは同一プロパティの宣言はmergeではなくoverride扱いとなるため、

tsconfig.json
{
  "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が出ました :clap:
ファイル拡張子的にはtsでもTypeScirpt特有のシンタックスが出てこない限りは、parserがJavaScript用のものでも問題ないんですね。

ただ、eslint実行の度に--ext .js,.tsを指定するのは面倒なので、package.jsonにタスクを作ってしまいます。

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を指定します。

.eslintrc.js
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をかけてくれます。

tsconfig.json
"include": ["src/**/*.ts", "src/**/*.js"],

ただ、includeに含まれないファイル、ここでは

.
├── eslint
│   └── rules
│       └── ban-variable-name.js
└── .eslintrc.js

上記の2ファイルをVSCodeで表示している時、

image.png

のようにproject(lint対象)に含めるよう警告が出ます。この対応としては、
lint対象としたい場合は

tsconfig.json
"include": ["src/**/*.ts", "src/**/*.js", "eslint/**/*.js", ".eslintrc.js"],

とすればいいし、lint対象から除外したい場合は、(.eslintrc.jsなんて意味ないですし)

.eslintignore
.eslintrc.js
eslint

のようなファイルをプロジェクトルートに配置しておけば、eslintがそれを読み込んでよしなに解釈してくれます。(記法は.gitignore等と同じ感じです)

custom rule 拡張

せっかくなので型を使って何かしら制約をかけようと思いましたが、なかなかいいのが思い浮かばないのと、前回の拡張でいいかなという思いで、
string型の特定の変数名を禁止する
という使い道のないクソlintを作りたいと思います。

まずは、index.tsを以下のように修正します。

index.ts
const fuga: number = 100;

すると、まだ禁止する型をstringに限定していないため、eslintのerrorが発生します。

image.png

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風にリファクタすると以下のようになります。

eslint/rules/ban-variable-name.ts
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ファイルの中身
eslint/rules/ban-variable-name.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を修正して

src/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型に変更すると、

src/index.ts
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
package.json
{
  "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
が便利かと思います。

非常に拙い記事になってしまいましたが、以上。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?