LoginSignup
5
4

More than 3 years have passed since last update.

ESlint custom (local) rule を設定する

Posted at

この記事は「イエソド アウトプット筋 トレーニング Advent Calendar 2020」14日目の記事です。

ESLintで独自ルールを設定する

node projectの開発時、

  • プロジェクト固有でlint掛けたいなー
  • でも別にpackageにするほどではないなー(リポジトリのメンテ面倒だし)

って場合に、独自ルールを追加するやり方を紹介します。

環境

筆者の実行環境。多少違えど問題ないと思います。

  • MacOS Catalina 10.15.7
  • node 12.18.4

サンプル

何はともあれソースコードがないと話にならないと思うので、GitHubのリポジトリに挙げておきます。

前準備

(git cloneするなら意味ないので飛ばしちゃってください)

$ mkdir eslint-local-rule  // プロジェクト名(ディクレトリ名)は適当に
$ cd eslint-local-rule
$ npm init -y
$ npm i -D eslint
$ mkdir src

依存

package.json
{
  "name": "eslint-local-rule",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "eslint": "^7.15.0"
  }
}

実装

lint対象となる、以下のようなindex.jsを用意します。

src/index.js
const unused = 'unused string';

eslintの設定ファイル

eslint公式docに従い、設定ファイルを追加します。
eslintの設定にはいくつか方法がありますが、ここでは.eslintrcを、ファイル形式はjsを、使用します。

.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
  },
  parserOptions: {
    ecmaVersion: 2020
  },
  extends: [
    'eslint:recommended'
  ],
};
parserOptions

docによれば、ecmaVersionのデフォルト値は5です。つまりes5のシンタックスしか認められません。
そうなるとconstなんかは使用できないし、es6(2015)以降の構文が使用できないのは嫌なので、ここでは一旦最新の2020にします。(サンプルコードでES2020で導入されたシンタックスは出てこないので6以上であれば何でもいいです)


さて、ここまで設定することで、eslint実行時にerrorが吐かれるのが期待値です。

$ npx eslint ./src

/Users/path-to-dir/eslint-local-rule/src/index.js
  1:7  error  'unused' is assigned a value but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

ではここから本題の独自ルールの設定の追加をしていきます。

ディレクトリ作成

独自ルールを記述したファイルを配置するため、プロジェクトルートに新たにディレクトリを作成します。ここではeslintという名前にします。

$ mkdir eslint
$ mkdir eslint/rules eslint/tests
$ tree  // 一部省略
.
├── eslint
│   ├── rules
│   └── tests  // 今回は時間の都合上、test書かないです...
├── node_modules
├── package-lock.json
├── package.json
└── src
    └── index.js

ルール実装

それでは、eslint/rulesディレクトリ内にルール用のJavaScriptファイルを実装していきます。
ESLintが使用するruleの記述にはフォーマットが定義されていて、独自ルールを定義する際は、それを参考にしていきます。

簡単のため、ここでは特定の変数名を禁止するというルールを実装していきます。

eslint/rules/ban-variable-name.js
module.exports = {
  meta: {
    // 一旦後回し。空Objectで。
  },
  create(context) {
    return {
      VariableDeclarator: (node) => {
        console.log(node);
      },
    };
  }
};

まずは一旦、eslintと連携して動作するか検証すべく、対象nodeをコンソール出力するだけのスクリプトを書きました。(詳細な説明は後ほど。)

ESLintと連携

さて、この状態でeslintを実行してももちろん、上記のスクリプトは実行されることはありません。つまり、結果は前回実行時と同じハズです。

$ npx eslint ./src

/Users/path-to-dir/eslint-local-rule/src/index.js
  1:7  error  'unused' is assigned a value but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

eslintに先ほど定義したrule(eslint/rules/ban-variable-name.js)を実行してもらうには、以下のプラグインを利用します。

$ npm i -D eslint-plugin-rulesdir

そして、.eslintrc.jsを修正します。

.eslintrc.js
const rulesDirPlugin = require('eslint-plugin-rulesdir');
rulesDirPlugin.RULES_DIR = 'eslint/rules';

module.exports = {
  root: true,
  env: {
    node: true,
  },
  parserOptions: {
    ecmaVersion: 2020
  },
  plugins: ['rulesdir'],
  extends: [
    'eslint:recommended'
  ],
  rules: {
    'rulesdir/ban-variable-name': 'error'
  }
};

eslint-plugin-rulesdirプラグインの利用を宣言し、追加ルールの配置されたディレクトリを指定してあげ、rulesプロパティに利用設定を追加します。

この状態で再度eslintを実行すると、、


結果
$ npx eslint ./src
Node {
  type: 'VariableDeclarator',
  start: 6,
  end: 30,
  loc: SourceLocation {
    start: Position { line: 1, column: 6 },
    end: Position { line: 1, column: 30 }
  },
  range: [ 6, 30 ],
  id: Node {
    type: 'Identifier',
    start: 6,
    end: 12,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 6, 12 ],
    name: 'unused',
    parent: [Circular]
  },
  init: Node {
    type: 'Literal',
    start: 15,
    end: 30,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 15, 30 ],
    value: 'unused string',
    raw: "'unused string'",
    parent: [Circular]
  },
  parent: Node {
    type: 'VariableDeclaration',
    start: 0,
    end: 30,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 0, 30 ],
    declarations: [ [Circular] ],
    kind: 'const',
    parent: Node {
      type: 'Program',
      start: 0,
      end: 31,
      loc: [SourceLocation],
      range: [Array],
      body: [Array],
      sourceType: 'script',
      comments: [],
      tokens: [Array],
      parent: null
    }
  }
}

/Users/rikukobayashi/private/eslint-local-rule/src/index.js
  1:7  error  'unused' is assigned a value but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

無事Nodeオブジェクトがコンソール出力されました :clap:

ルール実装に戻る

先ほどほったらかしたruleの説明に戻ります。

exportするObjectのcreateプロパティ(メソッド)で返すObjectでは、keyにSelectorを、valueに実行する関数を指定します。
Selectorというのが公式docにも述べられている通り、ASTのnodeを指定するための文字列です。CSSセレクタのように各属性を用いて指定できるようですが、大抵はnode typeを用いると思います。

VariableDeclaratorは変数宣言の構文のtypeです。
仮にindex.jsを以下のように修正しても該当するNodeは1つしかないので、コンソール出力されるNodeオブジェクトは1つのみです。

src/index.js
const unused = 'unused string';

function main() {
  console.log(unused);
}

main();


コンソール出力
$ npx eslint ./src
Node {
  type: 'VariableDeclarator',
  start: 6,
  end: 30,
  loc: SourceLocation {
    start: Position { line: 1, column: 6 },
    end: Position { line: 1, column: 30 }
  },
  range: [ 6, 30 ],
  id: Node {
    type: 'Identifier',
    start: 6,
    end: 12,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 6, 12 ],
    name: 'unused',
    parent: [Circular]
  },
  init: Node {
    type: 'Literal',
    start: 15,
    end: 30,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 15, 30 ],
    value: 'unused string',
    raw: "'unused string'",
    parent: [Circular]
  },
  parent: Node {
    type: 'VariableDeclaration',
    start: 0,
    end: 31,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 0, 31 ],
    declarations: [ [Circular] ],
    kind: 'const',
    parent: Node {
      type: 'Program',
      start: 0,
      end: 88,
      loc: [SourceLocation],
      range: [Array],
      body: [Array],
      sourceType: 'script',
      comments: [],
      tokens: [Array],
      parent: null
    }
  }
}


一方で、

src/index.js
const unused = 'unused string';

const main = () => {
  console.log(unused);
};

main();

のように関数宣言ではなく、関数式を用いた場合、該当Nodeは2つとなりコンソール出力は増えているでしょう。


コンソール出力
$ npx eslint ./src
Node {
  type: 'VariableDeclarator',
  start: 6,
  end: 30,
  loc: SourceLocation {
    start: Position { line: 1, column: 6 },
    end: Position { line: 1, column: 30 }
  },
  range: [ 6, 30 ],
  id: Node {
    type: 'Identifier',
    start: 6,
    end: 12,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 6, 12 ],
    name: 'unused',
    parent: [Circular]
  },
  init: Node {
    type: 'Literal',
    start: 15,
    end: 30,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 15, 30 ],
    value: 'unused string',
    raw: "'unused string'",
    parent: [Circular]
  },
  parent: Node {
    type: 'VariableDeclaration',
    start: 0,
    end: 31,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 0, 31 ],
    declarations: [ [Circular] ],
    kind: 'const',
    parent: Node {
      type: 'Program',
      start: 0,
      end: 88,
      loc: [SourceLocation],
      range: [Array],
      body: [Array],
      sourceType: 'script',
      comments: [],
      tokens: [Array],
      parent: null
    }
  }
}
Node {
  type: 'VariableDeclarator',
  start: 39,
  end: 78,
  loc: SourceLocation {
    start: Position { line: 3, column: 6 },
    end: Position { line: 5, column: 1 }
  },
  range: [ 39, 78 ],
  id: Node {
    type: 'Identifier',
    start: 39,
    end: 43,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 39, 43 ],
    name: 'main',
    parent: [Circular]
  },
  init: Node {
    type: 'ArrowFunctionExpression',
    start: 46,
    end: 78,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 46, 78 ],
    id: null,
    expression: false,
    generator: false,
    async: false,
    params: [],
    body: Node {
      type: 'BlockStatement',
      start: 52,
      end: 78,
      loc: [SourceLocation],
      range: [Array],
      body: [Array],
      parent: [Circular]
    },
    parent: [Circular]
  },
  parent: Node {
    type: 'VariableDeclaration',
    start: 33,
    end: 78,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 33, 78 ],
    declarations: [ [Circular] ],
    kind: 'const',
    parent: Node {
      type: 'Program',
      start: 0,
      end: 88,
      loc: [SourceLocation],
      range: [Array],
      body: [Array],
      sourceType: 'script',
      comments: [],
      tokens: [Array],
      parent: null
    }
  }
}



さて、先ほど提起した特定の変数名を禁止するというルールですが、例えば具体的にhogeという言葉を禁止するとします。
その場合は以下のようなruleになるでしょう。

eslint/rules/ban-variable-name.js
module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "Ban some keywords from being used as a variable name",
      category: "Variables",
    },
    schema: [], // no options
  },
  create(context) {
    return {
      VariableDeclarator: (node) => {
        if (node.id.name === "hoge") {
          context.report({
            node,
            message: `The keyword ${node.id.name} is banned to be used as a variable name`,
          });
        }
      },
    };
  },
};

index.jsを修正して

src/index.js
const unused = 'unused string';

const hoge = () => {
  console.log(unused);
}

hoge();

eslintを実行すると

$ 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

✖ 1 problem (1 error, 0 warnings)

無事怒られました :clap:

ただ、今のままhogeというキーワードのみに固定されるのは芸がないので、eslintの設定オプションで指定できるようにしましょう。

eslint/rules/ban-variable-name.js
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 ? option.words : [];
    return {
      VariableDeclarator: (node) => {
        if (bannedWords.includes(node.id.name)) {
          context.report({
            node,
            message: `The keyword ${node.id.name} is banned to be used as a variable name`,
          });
        }
      },
    };
  },
};

schemaプロパティ

docにある通り、ruleのoptionに制約を付与することができます。json-shemaで定義されているプロパティなら何でも使えるので、好きに定義できます。

ここでは、wordsプロパティ名を持つObjectをオプションに指定したので、利用(設定)する場合はこんな感じになります。

.eslintrc.js
module.exports = {
  ...省略,
  rules: {
    'rulesdir/ban-variable-name': ['error', { words: ['hoge'] }]
  }
};

この設定でeslintを実行してみると

$ 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

✖ 1 problem (1 error, 0 warnings)

OK :clap:

ついでに試しにちょっと修正してみます。

src/index.js
const unused = 'unused string';

const hoge = () => {
  console.log(unused);
}

hoge();

const fuga = 100;
.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

✖ 3 problems (3 errors, 0 warnings)

ちゃんと正しくオプションが反映されていますね。めでたし :clap:

VSCodeとの連携

VSCode user向けの話になります。

上記までで、独自ルールをESLint cliと連携させることはできました。
普段、VSCodeで開発していてeslintを使用している大抵の人は、extensionを使ってリアルタイム(リアクティブ?)でeslintの警告が見れるようにしていると思います。
上記で追加した独自ルールも同様に、(vscodeの設定に手を加えることなく)警告が見れるようになります。

警告が表示されない場合はVSCodeを再起動してみてください。改めてeslint extensionが設定ファイルを読み込んで表示してくれるようになるハズです。

おわり

こんな感じで簡単にカスタムルールを追加できるよっていうのをお話しました。

本来であればちゃんとtestを書きましょうってところで、eslintが提供してくれているRuleTesterを使ったテストのことまで書こうと思いましたが、ちょっと時間的に厳しいのでまたの機会に。。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4