From edf7187216b6708de1e4ca0fa5b6ad55e952d624 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Tue, 26 Dec 2017 16:47:14 +0100 Subject: [PATCH] style: validate rule configuration BREAKING CHANGE: Due to additional validation while reading commitlint config, previously ignored rule settings are now considered critical errors when starting the CLI. The new behaviour is designed to help developers find issues with their configuration quicker. --- @commitlint/core/src/lint.js | 61 +++++++++++++++++++++++++ @commitlint/core/src/lint.test.js | 75 ++++++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/@commitlint/core/src/lint.js b/@commitlint/core/src/lint.js index 46dede9bcf..3b937e45f3 100644 --- a/@commitlint/core/src/lint.js +++ b/@commitlint/core/src/lint.js @@ -1,3 +1,4 @@ +import util from 'util'; import isIgnored from '@commitlint/is-ignored'; import parse from '@commitlint/parse'; import implementations from '@commitlint/rules'; @@ -30,6 +31,66 @@ export default async (message, rules = {}, opts = {}) => { ); } + const invalid = entries(rules) + .map(([name, config]) => { + if (!Array.isArray(config)) { + return new Error( + `config for rule ${name} must be array, received ${util.inspect( + config + )} of type ${typeof config}` + ); + } + + if (config.length !== 2 && config.length !== 3) { + return new Error( + `config for rule ${name} must be 2 or 3 items long, received ${util.inspect( + config + )} of length ${config.length}` + ); + } + + const [level, when] = config; + + if (typeof level !== 'number' || isNaN(level)) { + return new Error( + `level for rule ${name} must be number, received ${util.inspect( + level + )} of type ${typeof level}` + ); + } + + if (level < 0 || level > 2) { + return new RangeError( + `level for rule ${name} must be between 0 and 2, received ${util.inspect( + level + )}` + ); + } + + if (typeof when !== 'string') { + return new Error( + `condition for rule ${name} must be string, received ${util.inspect( + when + )} of type ${typeof when}` + ); + } + + if (when !== 'never' && when !== 'always') { + return new Error( + `condition for rule ${name} must be "always" or "never", received ${util.inspect( + when + )}` + ); + } + + return null; + }) + .filter(item => item instanceof Error); + + if (invalid.length > 0) { + throw new Error(invalid.map(i => i.message).join('\n')); + } + // Validate against all rules const results = entries(rules) .filter(entry => { diff --git a/@commitlint/core/src/lint.test.js b/@commitlint/core/src/lint.test.js index e33f1d485b..f845b1d623 100644 --- a/@commitlint/core/src/lint.test.js +++ b/@commitlint/core/src/lint.test.js @@ -53,10 +53,81 @@ test('positive on stub message and opts', async t => { t.true(actual.valid); }); -test('should throw for invalid rule names', async t => { +test('throws for invalid rule names', async t => { const error = await t.throws( lint('foo', {foo: [2, 'always'], bar: [1, 'never']}) ); - t.is(error.message.indexOf('Found invalid rule names: foo, bar'), 0); + t.is(error.message.indexOf('Found missing rule names: foo, bar'), 0); +}); + +test('throws for invalid rule config', async t => { + const error = await t.throws( + lint('type(scope): foo', { + 'type-enum': 1, + 'scope-enum': {0: 2, 1: 'never', 2: ['foo'], length: 3} + }) + ); + + t.true(error.message.indexOf('type-enum must be array') > -1); + t.true(error.message.indexOf('scope-enum must be array') > -1); +}); + +test('throws for rule with invalid length', async t => { + const error = await t.throws( + lint('type(scope): foo', {'type-enum': [], 'scope-enum': [1, 2, 3, 4]}) + ); + + t.true(error.message.indexOf('type-enum must be 2 or 3 items long') > -1); + t.true(error.message.indexOf('scope-enum must be 2 or 3 items long') > -1); +}); + +test('throws for rule with invalid level', async t => { + const error = await t.throws( + lint('type(scope): foo', { + 'type-enum': ['2', 'always'], + 'header-max-length': [{}, 'always'] + }) + ); + + t.true(error.message.indexOf('rule type-enum must be number') > -1); + t.true(error.message.indexOf('rule type-enum must be number') > -1); +}); + +test('throws for rule with out of range level', async t => { + const error = await t.throws( + lint('type(scope): foo', { + 'type-enum': [-1, 'always'], + 'header-max-length': [3, 'always'] + }) + ); + + t.true(error.message.indexOf('rule type-enum must be between 0 and 2') > -1); + t.true(error.message.indexOf('rule type-enum must be between 0 and 2') > -1); +}); + +test('throws for rule with invalid condition', async t => { + const error = await t.throws( + lint('type(scope): foo', { + 'type-enum': [1, 2], + 'header-max-length': [1, {}] + }) + ); + + t.true(error.message.indexOf('type-enum must be string') > -1); + t.true(error.message.indexOf('header-max-length must be string') > -1); +}); + +test('throws for rule with out of range condition', async t => { + const error = await t.throws( + lint('type(scope): foo', { + 'type-enum': [1, 'foo'], + 'header-max-length': [1, 'bar'] + }) + ); + + t.true(error.message.indexOf('type-enum must be "always" or "never"') > -1); + t.true( + error.message.indexOf('header-max-length must be "always" or "never"') > -1 + ); });