diff --git a/@commitlint/rules/src/scope-enum.test.ts b/@commitlint/rules/src/scope-enum.test.ts index 56b52cfa43..fd551642fb 100644 --- a/@commitlint/rules/src/scope-enum.test.ts +++ b/@commitlint/rules/src/scope-enum.test.ts @@ -1,139 +1,183 @@ import parse from '@commitlint/parse'; import {scopeEnum} from './scope-enum'; - -const messages = { - plain: 'foo(bar): baz', - superfluous: 'foo(): baz', - empty: 'foo: baz', - multiple: 'foo(bar,baz): qux', - multipleCommaSpace: 'foo(bar, baz): qux', -}; - -const parsed = { - plain: parse(messages.plain), - superfluous: parse(messages.superfluous), - empty: parse(messages.empty), - multiple: parse(messages.multiple), - multipleCommaSpace: parse(messages.multipleCommaSpace), +import {RuleConfigCondition} from '@commitlint/types'; + +const messagesByScope = { + single: { + plain: 'foo(bar): baz', + }, + multiple: { + multiple: 'foo(bar,baz): qux', + multipleCommaSpace: 'foo(bar, baz): qux', + }, + none: { + empty: 'foo: baz', + superfluous: 'foo(): baz', + }, }; -test('scope-enum with plain message and always should succeed empty enum', async () => { - const [actual] = scopeEnum(await parsed.plain, 'always', []); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with plain message and never should error empty enum', async () => { - const [actual, message] = scopeEnum(await parsed.plain, 'never', []); - const expected = false; - expect(actual).toEqual(expected); - expect(message).toEqual('scope must not be one of []'); -}); - -test('with plain message should succeed correct enum', async () => { - const [actual] = scopeEnum(await parsed.plain, 'always', ['bar']); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with plain message should error false enum', async () => { - const [actual, message] = scopeEnum(await parsed.plain, 'always', ['foo']); - const expected = false; - expect(actual).toEqual(expected); - expect(message).toEqual('scope must be one of [foo]'); -}); - -test('scope-enum with plain message should error forbidden enum', async () => { - const [actual, message] = scopeEnum(await parsed.plain, 'never', ['bar']); - const expected = false; - expect(actual).toEqual(expected); - expect(message).toEqual('scope must not be one of [bar]'); -}); - -test('scope-enum with plain message should succeed forbidden enum', async () => { - const [actual] = scopeEnum(await parsed.plain, 'never', ['foo']); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with superfluous scope should succeed enum', async () => { - const [actual] = scopeEnum(await parsed.superfluous, 'always', ['bar']); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with superfluous scope and "never" should succeed', async () => { - const [actual] = scopeEnum(await parsed.superfluous, 'never', ['bar']); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with superfluous scope and always should succeed empty enum', async () => { - const [actual] = scopeEnum(await parsed.superfluous, 'always', []); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with superfluous scope and never should succeed empty enum', async () => { - const [actual] = scopeEnum(await parsed.superfluous, 'never', []); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with empty scope and always should succeed empty enum', async () => { - const [actual] = scopeEnum(await parsed.superfluous, 'always', []); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with empty scope and always should succeed filled enum', async () => { - const [actual] = scopeEnum(await parsed.superfluous, 'always', ['foo']); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with empty scope and never should succeed empty enum', async () => { - const [actual] = scopeEnum(await parsed.superfluous, 'never', []); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with multiple scopes should error on message with multiple scopes', async () => { - const [actual, message] = scopeEnum(await parsed.multiple, 'never', [ - 'bar', - 'baz', - ]); - const expected = false; - expect(actual).toEqual(expected); - expect(message).toEqual('scope must not be one of [bar, baz]'); -}); - -test('scope-enum with multiple scopes should error on message with forbidden enum', async () => { - const [actual, message] = scopeEnum(await parsed.multiple, 'never', [ - 'bar', - 'qux', - ]); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with multiple scopes should error on message with superfluous scope', async () => { - const [actual] = scopeEnum(await parsed.multiple, 'never', ['bar']); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with multiple scope should succeed on message with multiple scopes', async () => { - const [actual] = scopeEnum(await parsed.multiple, 'always', ['bar', 'baz']); - const expected = true; - expect(actual).toEqual(expected); -}); - -test('scope-enum with multiple scope with comma+space should succeed on message with multiple scopes', async () => { - const [actual] = scopeEnum(await parsed.multipleCommaSpace, 'always', [ - 'bar', - 'baz', - ]); - const expected = true; - expect(actual).toEqual(expected); +const {single, multiple, none} = messagesByScope; + +const messages = Object.values(messagesByScope).reduce( + (acc, curr) => ({...acc, ...curr}), + {} +); + +const conditions: RuleConfigCondition[] = ['always', 'never']; + +describe('Scope Enum Validation', () => { + conditions.forEach((condition) => { + describe('Enum without Scopes', () => { + Object.keys(messages).forEach((messageType) => { + test(`Succeeds with a '${messageType}' message and '${condition}'`, async () => { + const [actual, message] = scopeEnum( + await parse(messages[messageType]), + condition, + [] + ); + const expected = true; + expect(actual).toEqual(expected); + expect(message).toEqual(''); + }); + }); + }); + + describe('Messages without Scopes', () => { + Object.keys(none).forEach((messageType) => { + const fakeMessage = messages[messageType]; + + it(`Succeeds with a '${messageType}' message and '${condition}' with single-scope enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + condition, + ['bar'] + ); + expect(actual).toBeTruthy(); + expect(message).toBeFalsy(); + }); + + it(`Succeeds with a '${messageType}' message and '${condition}' with multi-scope enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + condition, + ['bar', 'baz'] + ); + expect(actual).toBeTruthy(); + expect(message).toBeFalsy(); + }); + }); + }); + }); + + describe('Always', () => { + describe('Single-Scope Messages', () => { + Object.keys(single).forEach((messageType) => { + const fakeMessage = messages[messageType]; + + it(`Succeeds with a '${messageType}' message when all message scopes are included in single-scope enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'always', + ['bar'] + ); + expect(actual).toBeTruthy(); + expect(message).toEqual('scope must be one of [bar]'); + }); + + test(`Succeeds with a '${messageType}' message when all message scopes are included in multi-scope enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'always', + ['bar', 'baz'] + ); + expect(actual).toBeTruthy(); + expect(message).toEqual('scope must be one of [bar, baz]'); + }); + + test(`Fails with a '${messageType}' message when any message scope is not included in enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'always', + ['foo'] + ); + expect(actual).toBeFalsy(); + expect(message).toEqual('scope must be one of [foo]'); + }); + }); + }); + + describe('Multi-Scope Messages', () => { + Object.keys(multiple).forEach((messageType) => { + const fakeMessage = messages[messageType]; + + test(`Succeeds with a '${messageType}' message when all message scopes are included in enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'always', + ['bar', 'baz'] + ); + expect(actual).toBeTruthy(); + expect(message).toEqual('scope must be one of [bar, baz]'); + }); + + test(`Fails with a '${messageType}' message when no message scopes are included in enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'always', + ['foo'] + ); + expect(actual).toBeFalsy(); + expect(message).toEqual('scope must be one of [foo]'); + }); + + it(`Fails with a '${messageType}' message when only some message scopes are included in enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'always', + ['bar'] + ); + expect(actual).toBeFalsy(); + expect(message).toEqual('scope must be one of [bar]'); + }); + }); + }); + }); + + describe('Never', () => { + describe('Messages with Scopes', () => { + Object.keys({...single, ...multiple}).forEach((messageType) => { + const fakeMessage = messages[messageType]; + + test(`Succeeds with a '${messageType}' message when no message scopes are included in enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'never', + ['foo'] + ); + expect(actual).toBeTruthy(); + expect(message).toEqual('scope must not be one of [foo]'); + }); + + it(`Fails with a '${messageType}' message when any message scope is included in single-scope enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'never', + ['bar'] + ); + expect(actual).toBeFalsy(); + expect(message).toEqual('scope must not be one of [bar]'); + }); + + test(`Fails with a '${messageType}' message when any message scope is included in multi-scope enum`, async () => { + const [actual, message] = scopeEnum( + await parse(fakeMessage), + 'never', + ['bar', 'baz'] + ); + expect(actual).toBeFalsy(); + expect(message).toEqual('scope must not be one of [bar, baz]'); + }); + }); + }); + }); }); diff --git a/@commitlint/rules/src/scope-enum.ts b/@commitlint/rules/src/scope-enum.ts index 3e3633c636..827f72de63 100644 --- a/@commitlint/rules/src/scope-enum.ts +++ b/@commitlint/rules/src/scope-enum.ts @@ -3,30 +3,28 @@ import message from '@commitlint/message'; import {SyncRule} from '@commitlint/types'; export const scopeEnum: SyncRule = ( - parsed, + {scope}, when = 'always', value = [] ) => { - if (!parsed.scope) { + if (!scope || !value.length) { return [true, '']; } // Scopes may contain slash or comma delimiters to separate them and mark them as individual segments. // This means that each of these segments should be tested separately with `ensure`. const delimiters = /\/|\\|, ?/g; - const scopeSegments = parsed.scope.split(delimiters); + const messageScopes = scope.split(delimiters); + const errorMessage = ['scope must', `be one of [${value.join(', ')}]`]; + const isScopeInEnum = (scope: string) => ensure.enum(scope, value); + let isValid; - const negated = when === 'never'; - const result = - value.length === 0 || - scopeSegments.every((scope) => ensure.enum(scope, value)); + if (when === 'never') { + isValid = !messageScopes.some(isScopeInEnum); + errorMessage.splice(1, 0, 'not'); + } else { + isValid = messageScopes.every(isScopeInEnum); + } - return [ - negated ? !result : result, - message([ - `scope must`, - negated ? `not` : null, - `be one of [${value.join(', ')}]`, - ]), - ]; + return [isValid, message(errorMessage)]; }; diff --git a/docs/reference-rules.md b/docs/reference-rules.md index 830d02c016..8622084dee 100644 --- a/docs/reference-rules.md +++ b/docs/reference-rules.md @@ -225,6 +225,10 @@ Infinity ``` [] ``` +- Notes: + - This rule always passes if no scopes are provided in the message or the value is an empty array. + - When set to `always`, all message scopes must be found in the value. + - When set to `never`, none of the message scopes can be found in the value. #### scope-case @@ -347,7 +351,19 @@ Infinity - **rule**: `always` - **value** ```js - ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test']; + [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test', + ]; ``` #### type-case