ESLintでソースコードの品質を保とう - osamu38/node-express-curriculum GitHub Wiki

ESLintでソースコードの品質を保とう

目次

  • ESLintとは
  • なぜESLintが必要なのか
  • ESLintを導入してみよう
  • ESLintを実行してみよう

ESLintとは

ESLintは特定のスタイルガイドラインに準拠していない問題のパターンやコードを検索するために使用される静的解析の一種です。

ソースコードの中でルールから外れている記述があればエラーを出してくれるので、ソースコードの品質を上げつつもバグも減らせる、一石二鳥なツールです。

なぜESLintが必要なのか

僕が思うESLintが必要だと思うパターン

  • どんな現場にも書き方のルールがある(そもそもルールが曖昧だったりする)
  • 納期が厳しいとルールに則っていない記述が増える
  • // TODO あとで直す ←直さない
  • ルールを守らない人が増える
  • そろそろ直したいなーでもルールってなんだっけ
  • カオス

ESLintはこのような負の連鎖をなくそうという試みです。

ESLintを導入してみよう

ESLintの利点として「自分でルールを作る」ことができるのですが、自分でルールを作るのは面倒なので、airbnbのルールを使ってみます。(でもちょっとだけルールを変更しています)

$ npm install -g eslint eslint-config-airbnb eslint-plugin-react

.eslintrcを作成します。

{
  "extends": "airbnb",
  "env": {
    "browser": true,
    "node": true
  },
  "rules": {
    "func-names": 0,
    "comma-dangle": 0,
    "space-before-function-paren": 0,
    "new-cap": 0
  }
}

これだけで準備は完了です。

ESLintを実行してみよう

ではさっそく実行してみましょう。

$ eslint ./

.
.
.

/Users/osamu38/Desktop/work/node-test/setUser.js
  1:1   error  Unexpected var, use let or const instead                         no-var
  4:3   error  Unexpected var, use let or const instead                         no-var
  6:5   error  All 'var' declarations must be at the top of the function scope  vars-on-top
  6:5   error  Unexpected var, use let or const instead                         no-var
  6:17  error  Unexpected string concatenation                                  prefer-template
  7:29  error  Unexpected function expression                                   prefer-arrow-callback
  9:9   error  Assignment to function parameter 'res'                           no-param-reassign
  9:38  error  Infix operators must be spaced                                   space-infix-ops

✖ 124 problems (124 errors, 0 warnings)

124 problems だと...

全体的にlintをかけたので、エラーの数が多くなってしまいました。

では1番エラーの多かったroutes/boards.jsのみに対象を絞ってlintしてみましょう。

$ eslint routes/boards.js

/Users/osamu38/Desktop/work/node-test/routes/boards.js
   1:1   error  Unexpected var, use let or const instead        no-var
   2:1   error  Unexpected var, use let or const instead        no-var
   3:1   error  Unexpected var, use let or const instead        no-var
   4:1   error  Unexpected var, use let or const instead        no-var
   5:1   error  Unexpected var, use let or const instead        no-var
   6:1   error  Unexpected var, use let or const instead        no-var
   7:1   error  Unexpected var, use let or const instead        no-var
  14:26  error  Unexpected function expression                  prefer-arrow-callback
  15:3   error  Unexpected var, use let or const instead        no-var
  16:3   error  Unexpected var, use let or const instead        no-var
  16:23  error  Unexpected string concatenation                 prefer-template
  17:1   error  Line 17 exceeds the maximum line length of 100  max-len
  17:3   error  Unexpected var, use let or const instead        no-var
  17:26  error  Unexpected string concatenation                 prefer-template
  18:35  error  Unexpected function expression                  prefer-arrow-callback
  19:40  error  Unexpected function expression                  prefer-arrow-callback
  19:49  error  'err' is already declared in the upper scope    no-shadow
  29:56  error  Unexpected function expression                  prefer-arrow-callback
  30:3   error  Unexpected var, use let or const instead        no-var
  31:3   error  Unexpected var, use let or const instead        no-var
  32:3   error  Unexpected var, use let or const instead        no-var
  33:3   error  Unexpected var, use let or const instead        no-var
  33:35  error  Infix operators must be spaced                  space-infix-ops
  34:3   error  Unexpected var, use let or const instead        no-var
  35:36  error  Unexpected function expression                  prefer-arrow-callback
  36:5   error  Unexpected var, use let or const instead        no-var
  37:1   error  Line 37 exceeds the maximum line length of 100  max-len
  37:5   error  Unexpected var, use let or const instead        no-var
  37:17  error  Unexpected string concatenation                 prefer-template
  38:29  error  Unexpected function expression                  prefer-arrow-callback
  38:43  error  'rows' is defined but never used                no-unused-vars
  39:20  error  Unexpected string concatenation                 prefer-template

✖ 32 problems (32 errors, 0 warnings)

それでも32個もエラーが出てしまいましたが、項目毎に解決していきましょう。

17個でno-varというエラーが表示されています。これはvarなんか使わずにletconst使えよという意味です。

routes/boards.jsを開いてください。

var express = require('express');
var router = express.Router();
var moment = require('moment');
var multer = require('multer');
var connection = require('../mysqlConnection');
var upload = multer({ dest: './public/images/uploads/' });
var cloudinary = require('cloudinary');
cloudinary.config({
  cloud_name: '[CLOUD_NAME]',
  api_key: '[API_KEY]',
  api_secret: '[API_SECRET]'
});

router.get('/:board_id', function(req, res) {
  var boardId = req.params.board_id;
  var getBoardQuery = 'SELECT * FROM boards WHERE board_id = ' + boardId;
  var getMessagesQuery = 'SELECT M.message, M.image_path, ifnull(U.user_name, \'名無し\') AS user_name, DATE_FORMAT(M.created_at, \'%Y年%m月%d日 %k時%i分%s秒\') AS created_at FROM messages M LEFT OUTER JOIN users U ON M.user_id = U.user_id WHERE M.board_id = ' + boardId + ' ORDER BY M.created_at ASC';
  connection.query(getBoardQuery, function(err, board) {
    connection.query(getMessagesQuery, function(err, messages) {
      res.render('board', {
        title: board[0].title,
        board: board[0],
        messageList: messages
      });
    });
  });
});

router.post('/:board_id', upload.single('image_file'), function(req, res) {
  var path = req.file.path;
  var message = req.body.message;
  var boardId = req.params.board_id;
  var userId = req.session.user_id? req.session.user_id: 0;
  var createdAt = moment().format('YYYY-MM-DD HH:mm:ss');
  cloudinary.uploader.upload(path, function(result) {
    var imagePath = result.url;
    var query = 'INSERT INTO messages (image_path, message, board_id, user_id, created_at) VALUES ("' + imagePath + '", ' + '"' + message + '", ' + '"' + boardId + '", ' + '"' + userId + '", ' + '"' + createdAt + '")';
    connection.query(query, function(err, rows) {
      res.redirect('/boards/' + boardId);
    });
  });
});

module.exports = router;

確かにvarしかありません。この変数は基本的に変更するものではないため、constに変更しましょう。

routes/boards.jsを以下のように書き換えます。

const express = require('express');
const router = express.Router();
const moment = require('moment');
const multer = require('multer');
const connection = require('../mysqlConnection');
const upload = multer({ dest: './public/images/uploads/' });
const cloudinary = require('cloudinary');
cloudinary.config({
  cloud_name: '[CLOUD_NAME]',
  api_key: '[API_KEY]',
  api_secret: '[API_SECRET]'
});

router.get('/:board_id', function(req, res) {
  const boardId = req.params.board_id;
  const getBoardQuery = 'SELECT * FROM boards WHERE board_id = ' + boardId;
  const getMessagesQuery = 'SELECT M.message, M.image_path, ifnull(U.user_name, \'名無し\') AS user_name, DATE_FORMAT(M.created_at, \'%Y年%m月%d日 %k時%i分%s秒\') AS created_at FROM messages M LEFT OUTER JOIN users U ON M.user_id = U.user_id WHERE M.board_id = ' + boardId + ' ORDER BY M.created_at ASC';
  connection.query(getBoardQuery, function(err, board) {
    connection.query(getMessagesQuery, function(err, messages) {
      res.render('board', {
        title: board[0].title,
        board: board[0],
        messageList: messages
      });
    });
  });
});

router.post('/:board_id', upload.single('image_file'), function(req, res) {
  const path = req.file.path;
  const message = req.body.message;
  const boardId = req.params.board_id;
  const userId = req.session.user_id? req.session.user_id: 0;
  const createdAt = moment().format('YYYY-MM-DD HH:mm:ss');
  cloudinary.uploader.upload(path, function(result) {
    const imagePath = result.url;
    const query = 'INSERT INTO messages (image_path, message, board_id, user_id, created_at) VALUES ("' + imagePath + '", ' + '"' + message + '", ' + '"' + boardId + '", ' + '"' + userId + '", ' + '"' + createdAt + '")';
    connection.query(query, function(err, rows) {
      res.redirect('/boards/' + boardId);
    });
  });
});

module.exports = router;

変更したのでlintしてみましょう。

$ eslint routes/boards.js

/Users/osamu38/Desktop/work/node-test/routes/boards.js
  14:26  error  Unexpected function expression                  prefer-arrow-callback
  16:25  error  Unexpected string concatenation                 prefer-template
  17:1   error  Line 17 exceeds the maximum line length of 100  max-len
  17:28  error  Unexpected string concatenation                 prefer-template
  18:35  error  Unexpected function expression                  prefer-arrow-callback
  19:40  error  Unexpected function expression                  prefer-arrow-callback
  19:49  error  'err' is already declared in the upper scope    no-shadow
  29:56  error  Unexpected function expression                  prefer-arrow-callback
  33:37  error  Infix operators must be spaced                  space-infix-ops
  35:36  error  Unexpected function expression                  prefer-arrow-callback
  37:1   error  Line 37 exceeds the maximum line length of 100  max-len
  37:19  error  Unexpected string concatenation                 prefer-template
  38:29  error  Unexpected function expression                  prefer-arrow-callback
  38:43  error  'rows' is defined but never used                no-unused-vars
  39:20  error  Unexpected string concatenation                 prefer-template

✖ 15 problems (15 errors, 0 warnings)

15個に減りましたね。

次にprefer-arrow-callbackというエラーがあります。これは無名関数使うならArrow Function使えよという意味です。

というわけで無名関数をArrow Functionに書き換えたいと思います。

routes/boards.jsを以下のように書き換えます。

// 中略
router.get('/:board_id', (req, res) => {
  const boardId = req.params.board_id;
  const getBoardQuery = 'SELECT * FROM boards WHERE board_id = ' + boardId;
  const getMessagesQuery = 'SELECT M.message, M.image_path, ifnull(U.user_name, \'名無し\') AS user_name, DATE_FORMAT(M.created_at, \'%Y年%m月%d日 %k時%i分%s秒\') AS created_at FROM messages M LEFT OUTER JOIN users U ON M.user_id = U.user_id WHERE M.board_id = ' + boardId + ' ORDER BY M.created_at ASC';
  connection.query(getBoardQuery, (err, board) => {
    connection.query(getMessagesQuery, (err, messages) => {
      res.render('board', {
        title: board[0].title,
        board: board[0],
        messageList: messages
      });
    });
  });
});

router.post('/:board_id', upload.single('image_file'), (req, res) => {
  const path = req.file.path;
  const message = req.body.message;
  const boardId = req.params.board_id;
  const userId = req.session.user_id? req.session.user_id: 0;
  const createdAt = moment().format('YYYY-MM-DD HH:mm:ss');
  cloudinary.uploader.upload(path, result => {
    const imagePath = result.url;
    const query = 'INSERT INTO messages (image_path, message, board_id, user_id, created_at) VALUES ("' + imagePath + '", ' + '"' + message + '", ' + '"' + boardId + '", ' + '"' + userId + '", ' + '"' + createdAt + '")';
    connection.query(query, (err, rows) => {
      res.redirect('/boards/' + boardId);
    });
  });
});
// 中略

変更したのでlintしてみましょう。

$ eslint routes/boards.js

/Users/osamu38/Desktop/work/node-test/routes/boards.js
  16:25  error  Unexpected string concatenation                 prefer-template
  17:1   error  Line 17 exceeds the maximum line length of 100  max-len
  17:28  error  Unexpected string concatenation                 prefer-template
  19:41  error  'err' is already declared in the upper scope    no-shadow
  33:37  error  Infix operators must be spaced                  space-infix-ops
  37:1   error  Line 37 exceeds the maximum line length of 100  max-len
  37:19  error  Unexpected string concatenation                 prefer-template
  38:35  error  'rows' is defined but never used                no-unused-vars
  39:20  error  Unexpected string concatenation                 prefer-template

✖ 9 problems (9 errors, 0 warnings)

9個に減りましたね。

次にprefer-templateというエラーがあります。これは文字列連結使うならTemplate String使えよという意味です。

というわけで文字列連結をTemplate Stringに書き換えたいと思います。

routes/boards.jsを以下のように書き換えます。

// 中略
router.get('/:board_id', (req, res) => {
  const boardId = req.params.board_id;
  const getBoardQuery = `SELECT * FROM boards WHERE board_id = ${boardId}`;
  const getMessagesQuery = `SELECT M.message, M.image_path, ifnull(U.user_name, '名無し') AS user_name, DATE_FORMAT(M.created_at, '%Y年%m月%d日 %k時%i分%s秒') AS created_at FROM messages M LEFT OUTER JOIN users U ON M.user_id = U.user_id WHERE M.board_id = ${boardId} ORDER BY M.created_at ASC`;
  connection.query(getBoardQuery, (err, board) => {
    connection.query(getMessagesQuery, (err, messages) => {
      res.render('board', {
        title: board[0].title,
        board: board[0],
        messageList: messages
      });
    });
  });
});

router.post('/:board_id', upload.single('image_file'), (req, res) => {
  const path = req.file.path;
  const message = req.body.message;
  const boardId = req.params.board_id;
  const userId = req.session.user_id? req.session.user_id: 0;
  const createdAt = moment().format('YYYY-MM-DD HH:mm:ss');
  cloudinary.uploader.upload(path, result => {
    const imagePath = result.url;
    const query = `INSERT INTO messages (image_path, message, board_id, user_id, created_at) VALUES ('${imagePath}', '${message}', '${boardId}', '${userId}', '${createdAt}')`;
    connection.query(query, (err, rows) => {
      res.redirect(`/boards/${boardId}`);
    });
  });
});
// 中略

変更したのでlintしてみましょう。

$ eslint routes/boards.js

/Users/osamu38/Desktop/work/node-test/routes/boards.js
  17:1   error  Line 17 exceeds the maximum line length of 100  max-len
  19:41  error  'err' is already declared in the upper scope    no-shadow
  33:37  error  Infix operators must be spaced                  space-infix-ops
  37:1   error  Line 37 exceeds the maximum line length of 100  max-len
  38:35  error  'rows' is defined but never used                no-unused-vars

✖ 5 problems (5 errors, 0 warnings)

5個に減りましたね。

次にmax-lenというエラーがあります。これは1行は100文字以内に抑えろよという意味です。

というわけで1行に100文字超えている行をいい感じの位置で改行したいと思います。

routes/boards.jsを以下のように書き換えます。

// 中略
router.get('/:board_id', (req, res) => {
  const boardId = req.params.board_id;
  const getBoardQuery = `
    SELECT *
    FROM boards
    WHERE board_id = ${boardId}
  `;
  const getMessagesQuery = `
    SELECT M.message,
           M.image_path,
           ifnull(U.user_name, '名無し') AS user_name,
           DATE_FORMAT(M.created_at, '%Y年%m月%d日 %k時%i分%s秒') AS created_at
    FROM   messages M
           LEFT OUTER JOIN users U
                        ON M.user_id = U.user_id
    WHERE  M.board_id = ${boardId}
    ORDER  BY M.created_at ASC
  `;
  connection.query(getBoardQuery, (err, board) => {
    connection.query(getMessagesQuery, (err, messages) => {
      res.render('board', {
        title: board[0].title,
        board: board[0],
        messageList: messages
      });
    });
  });
});

router.post('/:board_id', upload.single('image_file'), (req, res) => {
  const path = req.file.path;
  const message = req.body.message;
  const boardId = req.params.board_id;
  const userId = req.session.user_id? req.session.user_id: 0;
  const createdAt = moment().format('YYYY-MM-DD HH:mm:ss');
  cloudinary.uploader.upload(path, result => {
    const imagePath = result.url;
    const query = `
      INSERT INTO messages
                 (image_path,
                  message,
                  board_id,
                  user_id,
                  created_at)
      VALUES    ('${imagePath}',
                 '${message}',
                 '${boardId}',
                 '${userId}',
                 '${createdAt}')
    `;
    connection.query(query, (err, rows) => {
      res.redirect(`/boards/${boardId}`);
    });
  });
});
// 中略

変更したのでlintしてみましょう。

$ eslint routes/boards.js

/Users/osamu38/Desktop/work/node-test/routes/boards.js
  33:41  error  'err' is already declared in the upper scope  no-shadow
  47:37  error  Infix operators must be spaced                space-infix-ops
  64:35  error  'rows' is defined but never used              no-unused-vars

✖ 3 problems (3 errors, 0 warnings)

3個に減りましたね。

次にno-shadowというエラーがあります。これはひとつ上のscopeで同じ変数名が使われてるからダメだよという意味です。

routes/boards.jsを開いてください。

// 中略
 connection.query(getBoardQuery, (err, board) => {
    connection.query(getMessagesQuery, (err, messages) => {
// 中略

2つめのerrがかぶっていますね。変数名を変えてあげましょう。

routes/boards.jsを以下のように書き換えます。

// 中略
 connection.query(getBoardQuery, (getBoardQueryErr, board) => {
    connection.query(getMessagesQuery, (getMessagesQueryErr, messages) => {
// 中略

変更したのでlintしてみましょう。

$ eslint routes/boards.js

/Users/osamu38/Desktop/work/node-test/routes/boards.js
  47:37  error  Infix operators must be spaced    space-infix-ops
  64:35  error  'rows' is defined but never used  no-unused-vars

✖ 2 problems (2 errors, 0 warnings)

2個に減りましたね。

次にspace-infix-opsというエラーがあります。これは三項演算子を使うときに?:の左右にスペースを空けてという意味です。

routes/boards.jsを以下のように書き換えます。

  // 中略
  const boardId = req.params.board_id;
  const userId = req.session.user_id ? req.session.user_id : 0;
  const createdAt = moment().format('YYYY-MM-DD HH:mm:ss');
  // 中略

変更したのでlintしてみましょう。

$ eslint routes/boards.js

/Users/osamu38/Desktop/work/node-test/routes/boards.js
  64:35  error  'rows' is defined but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

ついに残り1つになりました。

次にno-unused-varsというエラーがあります。これは使っていないcallbackの引数を消してという意味です。

routes/boards.jsを以下のように書き換えます。

    // 中略
    connection.query(query, () => {
      res.redirect(`/boards/${boardId}`);
      // 中略

変更したのでlintしてみましょう。

$ eslint routes/boards.js

エラー消えたぁあああ!!!!

このようにしてソースコードの品質を地道に高めることによって、秩序を保つことができます。

Let's lint!!

前のページ:アプリケーションを公開しよう

次のページ:

⚠️ **GitHub.com Fallback** ⚠️