😽

ESLint の設定から逃げない

2023/08/03に公開

はじめに

.eslintrc ファイルを触る or 見ることはあるけど、内容はあまり理解できていない方も多いのではないでしょうか。

そこで React Native 公式の ESLint 設定である eslint-config-react-native を見ながら ESLint の設定ファイルを読めるようになろう、というのがこの記事の趣旨です。この記事を読むにあたり、React Native 特有の知識は必要ありません。

対象読者は ESLint の設定ファイルを触ったことはあるけど内容を理解できていない方です。入門記事ではないため、ESLint に全く触れたことがない方は、 Getting Started を読んでからこの記事をお読みいただくとより理解しやすいかもしれません。

とは言っても、この記事で扱う内容は決して高度なものではないため、入門者の方がお読みいただいても理解できる箇所は多いと思います。

はじめに、今回見ていく eslint-config-react-native の内容を記載します。こちらが既に理解できる方はこの記事を読む必要はありません。

https://github.com/facebook/react-native/blob/main/packages/eslint-config-react-native/index.js

使いたい方向け

# prettier は内部で使用しているため必須
npm install -D eslint prettier @react-native/eslint-config
.eslintrc
{
  "extends": "@react-native"
}

この記事では eslint-config-react-native の設定ファイルを上から項目順に見ていきます。そのため、説明の順序が分かりづらい箇所もあるかもしれませんが、ご容赦ください。

env

https://eslint.org/docs/latest/use/configure/language-options#specifying-environments

env: {
  es6: true,
},

env で指定した環境のグローバル変数を使用できるようになります。厳密には、ESLint はリンターでありランタイムには関与しないため、 env を設定しなくてもグローバル変数を使用することはできます。しかし、未定義変数として ESLint がエラーを出すため、それを防ぐために設定します。es6es2023 など JavaScript のバージョン以外でよく使われるのは browsernodejest、ブラウザ拡張機能を開発している場合は webextensions あたりです。ESLint のデフォルトは ES5 になっています。複数個設定することも可能です。

env: {
  es6: true,
  node: true,
  jest: true,
},

例えば webextensions: true にした場合、どのようなグローバル変数が使用できるか確認したい場合は globals パッケージの globals.json を参照します。browserchromeopr が追加されることがわかります。falseglobals のところでも確認しますが、書き換え不可という意味です。

https://github.com/sindresorhus/globals/blob/v13.23.0/globals.json#L1910-L1914

ちなみにこの globals.json を ESLint のどこから使用しているかというとこのあたりです。

https://github.com/eslint/eslintrc/blob/v2.1.3/conf/environments.js#L209-L211

parserOptions

https://eslint.org/docs/latest/use/configure/language-options#specifying-parser-options

parserOptions: {
  sourceType: 'module',
  ecmaFeatures: {
    jsx: true,
  },
},

名前の通りパーサーのオプションを設定します。ESLint は JavaScript のコードそのまま評価しているわけではなく、一度 AST (抽象構文木) に変換してから評価します。その変換する役割を担っているのがパーサーです。ESLint はデフォルトのパーサーとして Espree を使用します。

https://github.com/eslint/espree

ESLint と互換性のある場合は、他のパーサーを使用することも可能です。よく使われるのは @babel/eslint-parser や、TypeScript を ESLint が解釈できるようにする @typescript-eslint/parser などです。

パーサーを設定する場合はこのように指定します。eslint-config-react-native の場合は、ファイル拡張子によって使用するパーサーを切り替えているため、のちに解説する overrides の中で指定しています。

parser: '@typescript-eslint/parser',

オプションに設定されている項目を見ていきます。

sourceType はデフォルトで script に設定されており、ES Modules を使用する場合は module にする必要があります。

また、React を使用する場合には ecmaFeatures.jsx の設定が必要です。

ecmaVersion が設定されている例もよく見ますが、enves6: true にすると、parserOptions で自動的に ecmaVersion: 6 が設定されるため、省略することができます。es2023 なども同様です。

https://github.com/eslint/eslintrc/blob/v2.1.3/conf/environments.js#L63-L68

反対に ecmaVersion: 6 を設定しても es6: true が自動的に設定されるわけではありません。

ecmaVersion: 6 は ES6 構文をサポートすること、es6: true は ES6 のグローバル変数を ESLint が解釈できるようにすることが役割です。

「グローバル変数を追加 => 構文をサポート」であり、その逆は成り立たないと勝手に解釈しています。

extends

https://eslint.org/docs/latest/use/configure/configuration-files#extending-configuration-files

extends: ['plugin:prettier/recommended'],

語弊を恐れずに言えば、extends継承 です。公式サイトにもこのような説明があります。

別のコンフィギュレーションファイルの すべての特徴(ルール、プラグイン、言語オプションを含む)を継承し、すべてのオプションを変更することができます。

つまり継承元の設定でプラグインが追加されている場合は、自分で追加しなくとも、そのプラグインは使用できるようになります。

継承元の設定は、プラグイン (eslint-plugin-...) の一部として提供されたり、設定単体 (eslint-config-...) で提供されたりします。プラグインの場合は、 extends に指定する際に plugin: が必要です。eslint-plugin-eslint-config- は省略することが可能です。

つまり、今回の設定は、「プラグインである eslint-plugin-prettierrecommended 設定を継承するよ」という意味になります。

具体的にはこのような設定が継承されます。

https://github.com/prettier/eslint-plugin-prettier/blob/v5.0.1/eslint-plugin-prettier.js#L81-L89

extends した先でさらに extends していますね。設定内容を詳しく見ていきます。

extendspluginsprettier が設定されていますが、extendseslint-config-prettierpluginseslint-plugin-prettier を指します。それぞれの役割は以下です。

  • eslint-config-prettier: Prettier と競合する ESLint のルールを無効化
  • eslint-plugin-prettier: Prettier を ESLint から実行できるようにする

eslint-config-prettier の中身を見てみると、ひたすらスタイルに関わる ESLint のルールを無効化しています。

https://github.com/prettier/eslint-config-prettier/blob/main/index.js

下記のように extends に複数設定している場合、後に書いた設定のルールが優先されます。そのため prettier は最後に書く必要があることに注意しましょう。

extends: [
  'plugin:react/recommended',
  'plugin:react-hooks/recommended',
  'prettier'
],

また、rulesprettier/prettiereslint-plugin-prettierprettier というルールを指しています。(prettier が多くてややこしいですね...)

既に extends についての有益な記事がありますので、さらに詳しく知りたい方はこちらをご参照ください。

https://blog.ojisan.io/eslint-plugin-and-extend/

plugins

https://eslint.org/docs/latest/use/configure/plugins

plugins: [
  'eslint-comments',
  'react',
  'react-hooks',
  'react-native',
  '@react-native',
  'jest',
],

使用するプラグインを配列で指定します。extends 同様、 eslint-plugin- を省略することが可能です。例えば reacteslint-plugin-reactjesteslint-plugin-jest@react-native@react-native/eslint-plugin のことを指します。もちろん省略しないで書くことも可能です。

プラグイン名のルールはこちら

  • eslint-plugin-<plugin-name>
  • @<scope>/eslint-plugin-<plugin-name>
  • @<scope>/eslint-plugin

プラグインにも様々な種類があります (カスタムルールのみでないことに注意してください)

  • カスタムルール: rules で使用
  • カスタム設定: extends で使用
  • カスタム環境: env で使用
  • カスタムプロセッサ: processor で使用 ( 本記事では出てきません )

settings

https://eslint.org/docs/latest/use/configure/configuration-files#adding-shared-settings

ESLint は設定ファイルへの共有設定の追加をサポートしています。プラグインは settings を使用して、すべてのルールで共有される情報を指定します。ESLint の設定ファイルに settings オブジェクトを追加すると、実行されるすべてのルールにそのオブジェクトが提供されます。これはカスタムルールを追加する際に、同じ情報にアクセスして簡単に設定できるようにしたい場合に便利です。

settings: {
  react: {
    version: 'detect',
  },
},

settings で設定したものはルール本体のコードからアクセスできます。

eslint-plugin-react を使用する場合 react.version を設定する必要があります。detect にするとインストールされている React のバージョンが自動で適用されます。

https://github.com/jsx-eslint/eslint-plugin-react#configuration-legacy-eslintrc-

settings.react.version が使用されている箇所はこのあたりです。

https://github.com/jsx-eslint/eslint-plugin-react/blob/v7.33.2/lib/util/version.js#L76-L83

React のバージョンによってルールの無効化や、検査内容を変えたい場合などに使用されています。

// lib/util/version.js
const semver = require('semver');

function test(semverRange, confVer) {
  return semver.satisfies(confVer, semverRange);
}

function testReactVersion(context, semverRange) {
  return test(semverRange, getReactVersionFromContext(context));
}

// lib/rules/no-unsafe.js
module.exports = {
  meta: {},
  // rule 本体
  // context に settings オブジェクトの情報が入っています
  create(context) {
    ....
    const isApplicable = testReactVersion(context, '>= 16.3.0');
    if (!isApplicable) {
      return {};
    }
    ....
  },
};

overrides

https://eslint.org/docs/latest/use/configure/configuration-files#configuration-based-on-glob-patterns

同じディレクトリ内のファイルに対する構成が異なる必要がある場合など、より細かい制御が必要な場合があります。この場合、overrides キーの下に、特定のグロブ・パターンに一致するファイルにのみ適用される構成を、コマンド・ラインで渡すのと同じ形式(例えば、app/*/.test.js )で指定できます。

overrides では特定ファイルのみに適用される設定を書くことができます。

overrides: [
  {
    files: ['*.js'],
    parser: '@babel/eslint-parser',
    plugins: ['ft-flow'],
    rules: {
      // Flow Plugin
      // The following rules are made available via `eslint-plugin-ft-flow`
      'ft-flow/define-flow-type': 1,
      'ft-flow/use-flow-type': 1,
    },
  },
  {
    files: ['*.jsx'],
    parser: '@babel/eslint-parser',
  },
];

これはプロジェクト内の .js.jsx ファイルのみに適用される設定です。パーサーの指定とプラグインである eslint-plugin-ft-flow を追加し Flow のルールを利用できるようにしています。rules の記述方法については後に説明します。

overrides: [
  {
    files: ['*.ts', '*.tsx'],
    parser: '@typescript-eslint/parser',
    plugins: ['@typescript-eslint/eslint-plugin'],
    rules: {
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          destructuredArrayIgnorePattern: '^_',
        },
      ],
      'no-unused-vars': 'off',
      'no-shadow': 'off',
      '@typescript-eslint/no-shadow': 1,
      'no-undef': 'off',
      'func-call-spacing': 'off',
      '@typescript-eslint/func-call-spacing': 1,
    },
  },
];

こちらは TypeScript のコードを ESLint が解釈できるようにする設定をしています。

rules では、typescript-eslint プラグインが提供する @typescript-eslint/no-unused-vars などのルールを有効化しています。これらのルールは、TypeScript の型情報を考慮して未使用の変数を検出してくれます。

同時に、no-unused-vars という ESLint 組み込みのルールは、重複したエラーを防ぐために無効化しています。このように設定することで、TypeScript コードの静的解析をより正確かつ効果的に行うことができます。

typescript-eslint については充実した公式ドキュメントがありますので、詳細はそちらをご参照ください。

https://typescript-eslint.io/

overrides: [
  {
    files: [
      '*.{spec,test}.{js,ts,tsx}',
      '**/__{mocks,tests}__/**/*.{js,ts,tsx}',
    ],
    env: {
      jest: true,
      'jest/globals': true,
    },
    rules: {
      'react-native/no-inline-styles': 0,
      quotes: [1, 'single', { avoidEscape: true, allowTemplateLiterals: true }],
    },
  },
];

この設定については具体的に下記のようなファイルに適用されます。

・example.spec.js
・example.test.ts
・example.spec.tsx
・src/__tests__/util.test.js
・src/__mocks__/api.ts

envjest: true とすることで、afterEachit などのグローバル変数を ESLint が解釈できるようになります。

https://github.com/sindresorhus/globals/blob/v13.23.0/globals.json#L1401-L1418

jest/global: trueeslint-plugin-jest のカスタム環境を設定しています。ただ、設定ファイルを見る限り jest/global は指定しなくても問題なさそうです。

https://github.com/jest-community/eslint-plugin-jest/blob/v27.6.0/src/globals.json

globals

https://eslint.org/docs/latest/use/configure/language-options#specifying-globals

ESLint のコアルールのいくつかは、実行時にコードが利用できるグローバル変数の知識に依存しています。グローバル変数は実行時に変更されるだけでなく、異なる環境間で大きく変化する可能性があるため、ESLint はあなたの実行環境にどのようなグローバル変数が存在するかについて仮定しません。どのようなグローバル変数が利用可能かについての知識を必要とするルールを使用したい場合は、設定ファイルでグローバル変数を定義するか、ソースコードで設定コメントを使用してください。

ESLint に解釈してもらいたいグローバル変数は env 以外に、globals で手動追加することができます。

globals: {
  __DEV__: true,
  __dirname: false,
  __fbBatchedBridgeConfig: false,
  AbortController: false,
  Blob: true,
  alert: false,
  cancelAnimationFrame: false,
  cancelIdleCallback: false,
  clearImmediate: true,
  clearInterval: false,
  clearTimeout: false,
  console: false,
  document: false,
  ErrorUtils: false,
  escape: false,
  Event: false,
  EventTarget: false,
  exports: false,
  fetch: false,
  File: true,
  FileReader: false,
  FormData: false,
  global: false,
  Headers: false,
  Intl: false,
  Map: true,
  module: false,
  navigator: false,
  process: false,
  Promise: true,
  requestAnimationFrame: true,
  requestIdleCallback: true,
  require: false,
  Set: true,
  setImmediate: true,
  setInterval: false,
  setTimeout: false,
  queueMicrotask: true,
  URL: false,
  URLSearchParams: false,
  WebSocket: true,
  window: false,
  XMLHttpRequest: false,
}

変数の上書きを許可する場合は true 、上書きを禁止する場合は false を設定します。

React Native の設定ファイルは boolean 値で設定していますが、現在は writablereadonly を使用する書き方が推奨されています。falsereadonlytruewritable と等価です。

{
  "globals": {
    "var1": "writable",
    "var2": "readonly"
  }
}

また、env は内部で globals を設定しています。例えば enves2023: true とした場合は、ES2023 までで使用できるグローバル変数が globals に設定されます。

https://github.com/eslint/eslintrc/blob/v2.1.3/conf/environments.js#L117-L122

envbrowser: truenode: true とした場合、手動で setTimeout などを globals に追加する必要はありません。しかし、React Native の実行環境はブラウザや Node.js とは異なるため、browser: true としてしまうと使用できないグローバル変数までもが globals に追加されてしまいます。reactnative: true のような設定は現状存在しないため手動で追加する形になっていいます。

rules

https://eslint.org/docs/latest/use/configure/rules

// 一部省略
rules: {
  // General
  'comma-dangle': [1, 'always-multiline'], // allow or disallow trailing commas
  'no-cond-assign': 1, // disallow assignment in conditional expressions
  'no-console': 0, // disallow use of console (off by default in the node environment)
  ...

  // ESLint Comments Plugin
  // The following rules are made available via `eslint-plugin-eslint-comments`
  'eslint-comments/no-aggregating-enable': 1, // disallows eslint-enable comments for multiple eslint-disable comments
  'eslint-comments/no-unlimited-disable': 1,
  ...

  // React Plugin
  // The following rules are made available via `eslint-plugin-react`.
  'react/display-name': 0,
  'react/jsx-boolean-value': 0,
}

ルールの重大度の種類は 3 種類あります。

  • 0 or off: ルールをオフにする
  • 1 or warn: ワーニング を出す
  • 2 or error: エラー を出す

設定方法は、ルールの名前をキーにして、値に重大度を設定します。プラグインのルールを使用する場合は、plugin-name/rule-name という形式で指定します。plugin-nameeslint-plugin- を省略した名前です。

ルールのオプションを設定したい場合は [1, 'always-multiline'] のように配列で指定します。

まとめ

この記事を通して ESLint 設定ファイルへの苦手意識が少しでも減っていただけたら嬉しいです。

今回は React Native の設定を見てきましたが、ここまで理解できれば Next.js の設定である、eslint-config-next なども問題なく読めると思います。

https://www.npmjs.com/package/eslint-config-next

また、ルールの自作にも挑戦してみるとさらに理解が深まるでしょう。(筆者も挑戦中です)

https://eslint.org/docs/latest/extend/plugins

最後までお読みいただきありがとうございました 🙇‍♂️

GitHubで編集を提案

Discussion