From bc5cc28ae7407335935baa77236e4d8aabb4a84a Mon Sep 17 00:00:00 2001 From: Vincent Hardouin Date: Tue, 16 Jul 2024 15:29:07 +0200 Subject: [PATCH] feat: add no-column-migration-without-comment Co-authored-by: Mathieu Gilet --- config.js | 1 + index.js | 3 +- rules/no-column-migration-without-comment.js | 83 ++++++ ...o-column-migration-without-comment.test.js | 260 ++++++++++++++++++ 4 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 rules/no-column-migration-without-comment.js create mode 100644 rules/no-column-migration-without-comment.test.js diff --git a/config.js b/config.js index 9366d41..8bd5f1d 100644 --- a/config.js +++ b/config.js @@ -94,6 +94,7 @@ module.exports = [ }, ], '@1024pix/no-sinon-stub-with-args-oneliner': 'error', + '@1024pix/no-column-migration-without-comment': 'error', }, }, ]; diff --git a/index.js b/index.js index ad758d3..7c8b592 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ 'use strict'; const noSinonStubWithArgsOneliner = require('./rules/no-sinon-stub-with-args-oneliner.js'); +const noColumnMigrationWithoutComment = require('./rules/no-column-migration-without-comment.js'); module.exports = { - rules: { 'no-sinon-stub-with-args-oneliner': noSinonStubWithArgsOneliner }, + rules: { 'no-sinon-stub-with-args-oneliner': noSinonStubWithArgsOneliner, 'no-column-migration-without-comment': noColumnMigrationWithoutComment }, }; diff --git a/rules/no-column-migration-without-comment.js b/rules/no-column-migration-without-comment.js new file mode 100644 index 0000000..edcd377 --- /dev/null +++ b/rules/no-column-migration-without-comment.js @@ -0,0 +1,83 @@ +'use strict'; + +function report(context, node) { + context.report({ + node: node, + messageId: 'chainError', + }); +} + + +function getIdentifierName(node, stackName = []) { + switch (node.type) { + case 'Identifier': + return { identifier: node.name, callStack: stackName }; + case 'MemberExpression': + return getIdentifierName(node.object, [...stackName, node.property.name]) + case 'CallExpression': + return getIdentifierName(node.callee, stackName); + case 'AwaitExpression': + return getIdentifierName(node.argument, stackName); + } +} + +const VALID_MEMBER_EXPRESSIONS = ['dropColumn', 'foreign', 'unique', 'index', 'dropUnique', 'dropIndex']; + +module.exports = { + meta: { + type: 'problem', + docs: { + description: + 'All columns should have a comment explaining their purpose', + }, + messages: { + chainError: + '`table.column()` should have a comment explaining its purpose', + }, + }, + create: function(context) { + return { + CallExpression(node) { + const { callee } = node; + + if ( + callee.type === 'MemberExpression' && + callee.object && + callee.object.object && + callee.object.object.name === 'knex' && + callee.object.property && + callee.object.property.name === 'schema' && + ['alterTable', 'createTable', 'table'].includes(callee.property.name) + ) { + const callback = node.arguments[1]; + + if (callback && callback.type === 'ArrowFunctionExpression') { + const tableParam = callback.params[0]; + + if (tableParam && tableParam.type === 'Identifier') { + + const body = callback.body.body ?? [{ expression: callback.body }]; + + body.forEach(statement => { + const columnExpression = statement.expression; + + const { identifier: identifierName, callStack } = getIdentifierName(columnExpression); + + if (!callStack.some((call) => VALID_MEMBER_EXPRESSIONS.includes(call))) { + if (identifierName === tableParam.name) { + const hasComment = callStack.includes('comment'); + + if (!hasComment) { + report(context, node); + } + } + } + + }); + } + } + } + } + }; + } +}; \ No newline at end of file diff --git a/rules/no-column-migration-without-comment.test.js b/rules/no-column-migration-without-comment.test.js new file mode 100644 index 0000000..7ef391a --- /dev/null +++ b/rules/no-column-migration-without-comment.test.js @@ -0,0 +1,260 @@ +'use strict'; + +const rule = require('./no-column-migration-without-comment.js'), + RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2021, sourceType: 'module' }, +}); + +ruleTester.run('no-column-migration-without-comment', rule, { + valid: [ + // createTable + { + name: 'With comment', + code: ` + const up = function (knex) { + return knex.schema.createTable(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).comment('This is a comment'); + }); + }; + `, + }, + + { + name: 'Without using table callback', + code: ` + const up = function (knex) { + return knex.schema.createTable(TABLE_NAME, (table) => { + toto.maFunction(); + }); + }; + `, + }, + + // alterTable + { + name: 'With comment', + code: ` + const up = function (knex) { + return knex.schema.alterTable(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).comment('This is a comment'); + }); + }; + `, + }, + + { + name: 'Without using table callback', + code: ` + const up = function (knex) { + return knex.schema.alterTable(TABLE_NAME, (table) => { + toto.maFunction(); + }); + }; + `, + }, + + // table + + { + name: 'With comment', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).comment('This is a comment'); + }); + }; + `, + }, + + { + name: 'Without using table callback', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + toto.maFunction(); + }); + }; + `, + }, + + + { + name: 'With down function', + code: ` + const down = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.boolean('toto').comment('This is a comment'); + }); + }; + `, + }, + + + { + name: 'With dropColumn', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.dropColumn('toto'); + }); + }; + `, + }, + + { + name: 'With foreign', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.foreign('toto'); + }); + }; + `, + }, + + { + name: 'With unique', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.unique('toto'); + }); + }; + `, + }, + + { + name: 'With index', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.index('toto'); + }); + }; + `, + }, + + { + name: 'With dropUnique', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.dropUnique('toto'); + }); + }; + `, + }, + + { + name: 'With dropIndex', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.dropIndex('toto'); + }); + }; + `, + }, + + { + name: 'With oneliner', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => table.boolean('toto').comment('comment')); + }; + `, + }, + + { + name: 'With await', + code: ` + const up = async function (knex) { + return knex.schema.alterTable(TABLE_NAME, async (table) => { + await table.decimal(COLUMN, 5, 2).notNullable().alter().comment('toto'); + }); + }; + `, + }, + ], + + invalid: [ + + // CreateTable + { + name: 'Create column without comment', + code: ` + const up = function (knex) { + return knex.schema.createTable(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).defaultTo(false); + }); + }; + `, + errors: [{ messageId: 'chainError' }], + }, + { + name: 'Create column without comment', + code: ` + const up = function (knex) { + return knex.schema.createTable(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).defaultTo(false).comment('toto'); + table.boolean(COLUMN_NAME_2).defaultTo(false); + }); + }; + `, + errors: [{ messageId: 'chainError' }], + }, + + // AlterTable + + { + name: 'Create column without comment', + code: ` + const up = function (knex) { + return knex.schema.alterTable(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).defaultTo(false); + }); + }; + `, + errors: [{ messageId: 'chainError' }], + }, + { + name: 'Create column without comment', + code: ` + const up = function (knex) { + return knex.schema.alterTable(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).defaultTo(false).comment('toto'); + table.boolean(COLUMN_NAME_2).defaultTo(false); + }); + }; + `, + errors: [{ messageId: 'chainError' }], + }, + + // table + { + name: 'Create column without comment', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).defaultTo(false); + }); + }; + `, + errors: [{ messageId: 'chainError' }], + }, + { + name: 'Create column without comment', + code: ` + const up = function (knex) { + return knex.schema.table(TABLE_NAME, (table) => { + table.boolean(COLUMN_NAME).defaultTo(false).comment('toto'); + table.boolean(COLUMN_NAME_2).defaultTo(false); + }); + }; + `, + errors: [{ messageId: 'chainError' }], + }, + ], +});