Wondershake 開発者ブログ

LOCARI(ロカリ)の運営会社の開発者ブログです。

ESLint2からESLint3に上げて関連プラグインもアップデートした話

こんにちは。フロントエンドエンジニアの佐々木です。

先日、ESLintまわりを最新にアップデートしたのですが、結構修正することが多かったので備忘録も兼ねて残しておきます。

ちょっと古いですが、ESLintの導入に関してはこちらの記事でご紹介しました。

engineering.wondershake.com

ESLint3へのアップデート

f:id:sskyu:20161026225745p:plain

ESLint導入時には下記のバージョンを使っていました。

  "eslint": "^2.9.0",
  "eslint-config-airbnb": "^9.0.1",
  "eslint-plugin-import": "^1.12.0",
  "eslint-plugin-jsx-a11y": "^1.2.0",
  "eslint-plugin-react": "^5.1.1",
  "babel-eslint": "^6.0.4",

それぞれ最新のバージョンを調べてインストールしました。

  "eslint": "^3.8.1",
  "eslint-config-airbnb": "^12.0.0",
  "eslint-plugin-import": "^1.16.0",
  "eslint-plugin-jsx-a11y": "^2.2.2",
  "eslint-plugin-react": "^6.4.1",
  "babel-eslint": "^7.0.0",

この状態でESLintを走らせると ✖ 108 problems (99 errors, 9 warnings) と、結構な数のエラーが発生してしまいました。
バージョンを上げる前は 0 errors だったので、アップデートに伴ってルールが厳格になっているのがわかります。

.eslintrc を .eslintrc.yml に変換

ESLint3から設定をyamlで書けるようなので書き換えました。
これは好みの問題ですが、jsonよりもyamlの方がメンテしやすいです。

.eslintrc (変更前)

{
  "parser": "babel-eslint",
  "extends": "airbnb",
  "env": {
    "browser": true,
    "node": true
  },
  "globals": {
    "ga": true,
    "$": true,
    "I18n": true,
    "describe": true,
    "it": true,
    "before": true,
    "after": true
  },
  "rules": {
    "new-cap": [2, {
      "capIsNewExceptions": ["Map", "List", "Set", "OrderedSet"] // これはImmutableJS用の記述
    }]
  }
}

.eslintrc.yml (変更後)

---
  parser: babel-eslint
  extends: airbnb
  env:
    browser: true
    node: true

  globals:
    ga: true
    $: true
    I18n: true
    describe: true
    it: true
    before: true
    after: true

  rules:
    new-cap: [2, { capIsNewExceptions: [Map, List, Set, OrderedSet] }]

Lintをパスするよう修正

import/extensions

6:18  error  Unexpected use of file extension "jsx" for "./views/base.jsx"  import/extensions

このエラーは import Base from './views/base.jsx' のように .jsx を読み込んでいる箇所で起きてました。

eslint-plugin-import を見ると import/resolver が必要だとあるのでインストールします。

$ npm i -D eslint-import-resolver-node
# もし webpack を使っていたら eslint-import-resolver-webpack が必要そうです

.eslintrc.yml に追記します。

  # rules と同じインデントレベルに settings を追加
  settings:
    import/resolver: node

これでESLintを走らせれば import/extensions のエラーは回避できます。

class-methods-use-this

34:22  error  Expected 'this' to be used by class method 'comonentWillUnmount'     class-methods-use-this

このエラーの当該箇所は次のようなコードになってました。

class Base extends PureComponent {
  componentDidMount() {
    $(window).on('resize', throttle(this.handleResize, 200));
  }

  comonentWillUnmount() { // NG
    $(window).off('resize');
  }

  // 以下略
}

class methodとして定義されたメソッドのスコープ内で this への参照が無いとエラーになるようです。

componentDidMount でイベントを購読して、componentWillUnmount でイベントの購読を解除するコードなので、これ以上直しようが無いと思ったので warning を出すように変更しました。

.eslintrc.yml の rules に class-methods-use-this: 1 を追記するとエラーから警告に変わります。

根本的に解決するには、状態を持たないJSXは 関数として定義するといいです。

JSX In Depth - React

jsx-a11y/no-static-element-interactions

89:7   error  Visible, non-interactive elements should not have mouse or keyboard event listeners  jsx-a11y/no-static-element-interactions

このエラーは次のようなJSXを書くと発生します。

  render() {
    return <div onClick="this.handleClick" />
  }

div 要素などの本来ユーザーのインタラクションを想定していない要素に対して、onClick などでハンドリングしようとすると発生します。

この場合はHTMLのマークアップ上でクリッカブルな要素ということを示さないといけないので、div の代わりに button などで代替するのがいいでしょう。

  render() {
    return <button onClick="this.handleClick" />
  }

react/forbid-prop-types

6:5   error  Prop type `object` is forbidden    react/forbid-prop-types

ここが一番修正箇所が多いエラーでした。
このエラーは ReactComponent の propTypesReact.PropTypes.arrayReact.PropTypes.object を使っていると発生します。

props に React.PropTypes.array と指定されても、Array型なのはわかりますが配列の中身が何の型なのかがわかりません。
何の型を持った配列なのかを明示的に示さないとエラーになるようになりました。

このLintをパスするには PropTypes.array の代わりに PropTypes.arrayOf(), PropTypes.object の代わりに PropTypes.shape() を使います。

このあたりの書き方は本家のドキュメントが参考になりました。

Typechecking With PropTypes - React

例えば次のようなコードがある場合、

// @file sites.jsx
import React, { Component, PropTypes } from 'react';

export default class Sites extends Component {
  static propTypes = {
    sites: PropTypes.array.isRequired,  // NG
    currentSite: PropTypes.object,      // NG
  }
  // 以下略
}

このように直します。

// @file sites.jsx
import React, { Component, PropTypes } from 'react';

export default class Sites extends Component {
  static propTypes = {
    sites: PropTypes.arrayOf(PropTypes.shape({  // OK
      name: PropTypes.string.isRequired,
      url: PropTypes.string,
    })).isRequired,
    currentSite: PropTypes.shape({              // OK
      name: PropTypes.string.isRequired,
      url: PropTypes.string,
    }),
  }
  // 以下略
}

currentSitesite 型のオブジェクトを値に持つとすれば、sitessite 型の配列を値に持つと言えそうです。

上記のコードは冗長なので、型定義をまとめたファイルを constants/prop_types.js として作りました。

// @file prop_types.js
import { PropTypes } from 'react';

export const site = PropTypes.shape({
  name: PropTypes.string.isRequired,
  url: PropTypes.string,
});

export sites = PropTypes.arrayOf(site);

型定義ファイルを参照するように変更するとすっきりします。

// @file sites.jsx
import React, { Component } from 'react';
import * as pTypes from '../constants/prop_types';

export default class Sites extends Component {
  static propTypes = {
    sites: pTypes.sites.isRequired,
    currentSite: pTypes.site,
  }
  // 以下略
}

react/no-unused-prop-types

PropTypes.shape() を全て外部のファイル(constants/prop_types.js)で定義すればいいのですが、.jsx ファイル内で PropTypes.shape() を定義すると以下のようなエラーが出ます。

6:12  error  'item.key' PropType is defined but prop is never used    react/no-unused-prop-types

このエラーを抑制するためのオプションが用意されているので、.eslintrc.yml の rules に下記を追記して無効化しましょう。

  rules:
    react/no-unused-prop-types: [2, { skipShapeProps: true }]

Code Climate

ESLintのバージョンアップに伴い、CodeClimateの設定で少し悩んだので書いておきます。

CodeClimate上で実行するESLintのバージョンはデフォルトで1系が使われるようです。

Note:

If no channel is specified, ESLint v1.10.3 is used for analysis.

https://docs.codeclimate.com/docs/eslint

ということで、ESLint 3系を使って欲しい場合は .codeclimate.yml に追記します。

---
engines:
  eslint:
    enabled: true
    channel: eslint-3  # ESLint3系 を使うようにする

ちなみにこの eslint-3 で使われる ESLint のバージョンは現時点では 3.6.1 でした。

これでうまく行くはずだったのですが、ローカルでESLintが通っていても CodeClimate 上では import/no-unresolved のエラーが出る現象に悩まされました。

import/no-unresolved が出ている箇所は import React from 'react'; のように、npm module を読み込んでいる箇所で、参照先が絶対パスで始まるとエラーになるようでした。

これはCodeClimate上での結果が正しくないので、.codeclimate.yml に下記を追加してチェックを無効化しました。

---
engines:
  eslint:
    enabled: true
    channel: eslint-3
    checks:
      import/no-unresolved: # このチェックを無効化する
        enabled: false

まとめ

ESLintまわりを最新にアップデートして発生したエラーを無くしていく過程をご紹介しました。

記事が長くなるので紹介できなかったルールもあったのですが、React を使うなら eslint-config-airbnb は入れておいたほうがいいと改めて思いました。
このLint設定で書いていけば最低限のコードの品質は保てると思います。

Wondershake では Web エンジニアや iOSAndroid ディベロッパーを募集しています。 興味が湧いた方は是非こちらからご応募下さい!

www.wantedly.com