ESLint2からESLint3に上げて関連プラグインもアップデートした話
こんにちは。フロントエンドエンジニアの佐々木です。
先日、ESLintまわりを最新にアップデートしたのですが、結構修正することが多かったので備忘録も兼ねて残しておきます。
ちょっと古いですが、ESLintの導入に関してはこちらの記事でご紹介しました。
ESLint3へのアップデート
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-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 の propTypes
で React.PropTypes.array
や React.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, }), } // 以下略 }
currentSite
が site
型のオブジェクトを値に持つとすれば、sites
は site
型の配列を値に持つと言えそうです。
上記のコードは冗長なので、型定義をまとめたファイルを 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 エンジニアや iOS、Android ディベロッパーを募集しています。 興味が湧いた方は是非こちらからご応募下さい!