diff --git a/@commitlint/rules/src/header-case.js b/@commitlint/rules/src/header-case.js new file mode 100644 index 0000000000..779f4a5e97 --- /dev/null +++ b/@commitlint/rules/src/header-case.js @@ -0,0 +1,34 @@ +import * as ensure from '@commitlint/ensure'; +import message from '@commitlint/message'; + +const negated = when => when === 'never'; + +export default (parsed, when, value) => { + const {header} = parsed; + + if (typeof header !== 'string' || !header.match(/^[a-z]/i)) { + return [true]; + } + + const checks = (Array.isArray(value) ? value : [value]).map(check => { + if (typeof check === 'string') { + return { + when: 'always', + case: check + }; + } + return check; + }); + + const result = checks.some(check => { + const r = ensure.case(header, check.case); + return negated(check.when) ? !r : r; + }); + + const list = checks.map(c => c.case).join(', '); + + return [ + negated(when) ? !result : result, + message([`header must`, negated(when) ? `not` : null, `be ${list}`]) + ]; +}; diff --git a/@commitlint/rules/src/header-case.test.js b/@commitlint/rules/src/header-case.test.js new file mode 100644 index 0000000000..fe7f501487 --- /dev/null +++ b/@commitlint/rules/src/header-case.test.js @@ -0,0 +1,318 @@ +import test from 'ava'; +import parse from '@commitlint/parse'; +import headerCase from './header-case'; + +const messages = { + numeric: '1.0.0\n', + lowercase: 'header\n', + mixedcase: 'hEaDeR\n', + uppercase: 'HEADER\n', + camelcase: 'heaDer\n', + kebabcase: 'hea-der\n', + pascalcase: 'HeaDer\n', + snakecase: 'hea_der\n', + startcase: 'Hea Der\n' +}; + +const parsed = { + numeric: parse(messages.numeric), + lowercase: parse(messages.lowercase), + mixedcase: parse(messages.mixedcase), + uppercase: parse(messages.uppercase), + camelcase: parse(messages.camelcase), + kebabcase: parse(messages.kebabcase), + pascalcase: parse(messages.pascalcase), + snakecase: parse(messages.snakecase), + startcase: parse(messages.startcase) +}; + +test('with lowercase header should fail for "never lowercase"', async t => { + const [actual] = headerCase(await parsed.lowercase, 'never', 'lowercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with lowercase header should succeed for "always lowercase"', async t => { + const [actual] = headerCase(await parsed.lowercase, 'always', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase header should succeed for "never lowercase"', async t => { + const [actual] = headerCase(await parsed.mixedcase, 'never', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase header should fail for "always lowercase"', async t => { + const [actual] = headerCase(await parsed.mixedcase, 'always', 'lowercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with mixedcase header should succeed for "never uppercase"', async t => { + const [actual] = headerCase(await parsed.mixedcase, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase header should fail for "always uppercase"', async t => { + const [actual] = headerCase(await parsed.mixedcase, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with uppercase header should fail for "never uppercase"', async t => { + const [actual] = headerCase(await parsed.uppercase, 'never', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with lowercase header should succeed for "always uppercase"', async t => { + const [actual] = headerCase(await parsed.uppercase, 'always', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with camelcase header should fail for "always uppercase"', async t => { + const [actual] = headerCase(await parsed.camelcase, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with camelcase header should succeed for "never uppercase"', async t => { + const [actual] = headerCase(await parsed.camelcase, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with camelcase header should fail for "always pascalcase"', async t => { + const [actual] = headerCase(await parsed.camelcase, 'always', 'pascal-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with camelcase header should fail for "always kebabcase"', async t => { + const [actual] = headerCase(await parsed.camelcase, 'always', 'kebab-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with camelcase header should fail for "always snakecase"', async t => { + const [actual] = headerCase(await parsed.camelcase, 'always', 'snake-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with camelcase header should succeed for "always camelcase"', async t => { + const [actual] = headerCase(await parsed.camelcase, 'always', 'camel-case'); + const expected = true; + t.is(actual, expected); +}); + +test('with pascalcase header should fail for "always uppercase"', async t => { + const [actual] = headerCase(await parsed.pascalcase, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with pascalcase header should succeed for "never uppercase"', async t => { + const [actual] = headerCase(await parsed.pascalcase, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with pascalcase header should succeed for "always pascalcase"', async t => { + const [actual] = headerCase(await parsed.pascalcase, 'always', 'pascal-case'); + const expected = true; + t.is(actual, expected); +}); + +test('with pascalcase header should fail for "always kebabcase"', async t => { + const [actual] = headerCase(await parsed.pascalcase, 'always', 'kebab-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with pascalcase header should fail for "always snakecase"', async t => { + const [actual] = headerCase(await parsed.pascalcase, 'always', 'snake-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with pascalcase header should fail for "always camelcase"', async t => { + const [actual] = headerCase(await parsed.pascalcase, 'always', 'camel-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with snakecase header should fail for "always uppercase"', async t => { + const [actual] = headerCase(await parsed.snakecase, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with snakecase header should succeed for "never uppercase"', async t => { + const [actual] = headerCase(await parsed.snakecase, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with snakecase header should fail for "always pascalcase"', async t => { + const [actual] = headerCase(await parsed.snakecase, 'always', 'pascal-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with snakecase header should fail for "always kebabcase"', async t => { + const [actual] = headerCase(await parsed.snakecase, 'always', 'kebab-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with snakecase header should succeed for "always snakecase"', async t => { + const [actual] = headerCase(await parsed.snakecase, 'always', 'snake-case'); + const expected = true; + t.is(actual, expected); +}); + +test('with snakecase header should fail for "always camelcase"', async t => { + const [actual] = headerCase(await parsed.snakecase, 'always', 'camel-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with startcase header should fail for "always uppercase"', async t => { + const [actual] = headerCase(await parsed.startcase, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with startcase header should succeed for "never uppercase"', async t => { + const [actual] = headerCase(await parsed.startcase, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with startcase header should fail for "always pascalcase"', async t => { + const [actual] = headerCase(await parsed.startcase, 'always', 'pascal-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with startcase header should fail for "always kebabcase"', async t => { + const [actual] = headerCase(await parsed.startcase, 'always', 'kebab-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with startcase header should fail for "always snakecase"', async t => { + const [actual] = headerCase(await parsed.startcase, 'always', 'snake-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with startcase header should fail for "always camelcase"', async t => { + const [actual] = headerCase(await parsed.startcase, 'always', 'camel-case'); + const expected = false; + t.is(actual, expected); +}); + +test('with startcase header should succeed for "always startcase"', async t => { + const [actual] = headerCase(await parsed.startcase, 'always', 'start-case'); + const expected = true; + t.is(actual, expected); +}); + +test('should use expected message with "always"', async t => { + const [, message] = headerCase( + await parsed.uppercase, + 'always', + 'lower-case' + ); + t.true(message.indexOf('must be lower-case') > -1); +}); + +test('should use expected message with "never"', async t => { + const [, message] = headerCase(await parsed.uppercase, 'never', 'upper-case'); + t.true(message.indexOf('must not be upper-case') > -1); +}); + +test('with uppercase scope should succeed for "always [uppercase, lowercase]"', async t => { + const [actual] = headerCase(await parsed.uppercase, 'always', [ + 'uppercase', + 'lowercase' + ]); + const expected = true; + t.is(actual, expected); +}); + +test('with lowercase header should succeed for "always [uppercase, lowercase]"', async t => { + const [actual] = headerCase(await parsed.lowercase, 'always', [ + 'uppercase', + 'lowercase' + ]); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase header should fail for "always [uppercase, lowercase]"', async t => { + const [actual] = headerCase(await parsed.mixedcase, 'always', [ + 'uppercase', + 'lowercase' + ]); + const expected = false; + t.is(actual, expected); +}); + +test('with mixedcase header should pass for "always [uppercase, lowercase, camel-case]"', async t => { + const [actual] = headerCase(await parsed.mixedcase, 'always', [ + 'uppercase', + 'lowercase', + 'camel-case' + ]); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase scope should pass for "never [uppercase, lowercase]"', async t => { + const [actual] = headerCase(await parsed.mixedcase, 'never', [ + 'uppercase', + 'lowercase' + ]); + const expected = true; + t.is(actual, expected); +}); + +test('with uppercase scope should fail for "never [uppercase, lowercase]"', async t => { + const [actual] = headerCase(await parsed.uppercase, 'never', [ + 'uppercase', + 'lowercase' + ]); + const expected = false; + t.is(actual, expected); +}); + +test('with numeric header should succeed for "never lowercase"', async t => { + const [actual] = headerCase(await parsed.numeric, 'never', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with numeric header should succeed for "always lowercase"', async t => { + const [actual] = headerCase(await parsed.numeric, 'always', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with numeric header should succeed for "never uppercase"', async t => { + const [actual] = headerCase(await parsed.numeric, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with numeric header should succeed for "always uppercase"', async t => { + const [actual] = headerCase(await parsed.numeric, 'always', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); diff --git a/@commitlint/rules/src/header-full-stop.js b/@commitlint/rules/src/header-full-stop.js new file mode 100644 index 0000000000..f30ce6d12c --- /dev/null +++ b/@commitlint/rules/src/header-full-stop.js @@ -0,0 +1,12 @@ +import message from '@commitlint/message'; + +export default (parsed, when, value) => { + const {header} = parsed; + const negated = when === 'never'; + const hasStop = header[header.length - 1] === value; + + return [ + negated ? !hasStop : hasStop, + message(['header', negated ? 'may not' : 'must', 'end with full stop']) + ]; +}; diff --git a/@commitlint/rules/src/header-full-stop.test.js b/@commitlint/rules/src/header-full-stop.test.js new file mode 100644 index 0000000000..5b41da82e3 --- /dev/null +++ b/@commitlint/rules/src/header-full-stop.test.js @@ -0,0 +1,37 @@ +import test from 'ava'; +import parse from '@commitlint/parse'; +import check from './header-full-stop'; + +const messages = { + with: `header.\n`, + without: `header\n` +}; + +const parsed = { + with: parse(messages.with), + without: parse(messages.without) +}; + +test('with against "always ." should succeed', async t => { + const [actual] = check(await parsed.with, 'always', '.'); + const expected = true; + t.is(actual, expected); +}); + +test('with against "never ." should fail', async t => { + const [actual] = check(await parsed.with, 'never', '.'); + const expected = false; + t.is(actual, expected); +}); + +test('without against "always ." should fail', async t => { + const [actual] = check(await parsed.without, 'always', '.'); + const expected = false; + t.is(actual, expected); +}); + +test('without against "never ." should succeed', async t => { + const [actual] = check(await parsed.without, 'never', '.'); + const expected = true; + t.is(actual, expected); +}); diff --git a/@commitlint/rules/src/index.js b/@commitlint/rules/src/index.js index 4cf3561442..4d61033f57 100644 --- a/@commitlint/rules/src/index.js +++ b/@commitlint/rules/src/index.js @@ -10,6 +10,8 @@ export default { 'footer-max-length': require('./footer-max-length'), 'footer-max-line-length': require('./footer-max-line-length'), 'footer-min-length': require('./footer-min-length'), + 'header-case': require('./header-case'), + 'header-full-stop': require('./header-full-stop'), 'header-max-length': require('./header-max-length'), 'header-min-length': require('./header-min-length'), 'references-empty': require('./references-empty'), diff --git a/docs/reference-rules.md b/docs/reference-rules.md index 12c760a25a..90529a748c 100644 --- a/docs/reference-rules.md +++ b/docs/reference-rules.md @@ -90,6 +90,34 @@ Rule configurations are either of type `array` residing on a key with the rule's 0 ``` +#### header-case +* **condition**: `header` is in case `value` +* **rule**: `always` +```js + 'lowerCase' +``` +* **possible values** +```js + [ + 'lower-case', // default + 'upper-case', // UPPERCASE + 'camel-case', // camelCase + 'kebab-case', // kebab-case + 'pascal-case', // PascalCase + 'sentence-case', // Sentence case + 'snake-case', // snake_case + 'start-case', // Start Case + ] +``` + +#### header-full-stop +* **condition**: `header` ends with `value` +* **rule**: `never` +* **value** +```js + '.' +``` + #### header-max-length * **condition**: `header` has `value` or less characters * **rule**: `always`