-
Notifications
You must be signed in to change notification settings - Fork 919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/3779: Adjust logic for 'never' scope-enum validation #3795
Changes from all commits
2d567b3
6dcec0c
b9cd734
145ca9d
b8c0c00
2a545c5
7c7ce18
57ce694
235eef1
ee4ca6f
ac4e8e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,30 +3,28 @@ import message from '@commitlint/message'; | |
import {SyncRule} from '@commitlint/types'; | ||
|
||
export const scopeEnum: SyncRule<string[]> = ( | ||
parsed, | ||
{scope}, | ||
when = 'always', | ||
value = [] | ||
) => { | ||
if (!parsed.scope) { | ||
if (!scope || !value.length) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The value.length check was further down before; I can't reason out a case where "scopeEnum(..., 'never', [])" should fail, which was the original behavior, but just wanted to call this out. |
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Main logic change, essentially from "!messageScopes.every(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)]; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a lot of changes, but in summary, I organized the old tests by use-case and they are still getting executed; in addition, I added more automated tests to cover all of the message types for the following cases for both always and never: