ESLintでソースコードの品質を保とう - osamu38/node-express-curriculum GitHub Wiki
- ESLintとは
- なぜESLintが必要なのか
- ESLintを導入してみよう
- ESLintを実行してみよう
ESLintは特定のスタイルガイドラインに準拠していない問題のパターンやコードを検索するために使用される静的解析の一種です。
ソースコードの中でルールから外れている記述があればエラーを出してくれるので、ソースコードの品質を上げつつもバグも減らせる、一石二鳥なツールです。
- どんな現場にも書き方のルールがある(そもそもルールが曖昧だったりする)
- 納期が厳しいとルールに則っていない記述が増える
-
// TODO あとで直す
←直さない - ルールを守らない人が増える
- そろそろ直したいなーでもルールってなんだっけ
- カオス
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 ./
.
.
.
/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)
全体的に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
なんか使わずにlet
かconst
使えよという意味です。
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!!
前のページ:アプリケーションを公開しよう