Skip to content

Commit

Permalink
feat(rules): expand Latin-only characters limitation for `subject-cas…
Browse files Browse the repository at this point in the history
…e` with Unicode support (#3575)
  • Loading branch information
amariq committed Apr 13, 2023
1 parent 8940ccc commit 5f83423
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 2 deletions.
140 changes: 139 additions & 1 deletion @commitlint/rules/src/subject-case.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,44 @@ const messages = {
empty: 'test:\n',
numeric: 'test: 1.0.0',
lowercase: 'test: subject',
lowercase_unicode: 'test: тема', // Bulgarian for `subject`
mixedcase: 'test: sUbJeCt',
uppercase: 'test: SUBJECT',
uppercase_unicode: 'test: ÛNDERWERP', // Frisian for `SUBJECT`
camelcase: 'test: subJect',
camelcase_unicode: 'test: θέΜα', // Greek for `subJect`
kebabcase: 'test: sub-ject',
kebabcase_unicode: 'test: áb-har', // Irish for `sub-ject`
pascalcase: 'test: SubJect',
pascalcase_unicode: 'test: ТақыРып', // Kazakh for `SubJect`
snakecase: 'test: sub_ject',
snakecase_unicode: 'test: сэ_дэв', // Mongolian for `sub_ject`
startcase: 'test: Sub Ject',
startcase_unicode: 'test: Äm Ne', // Swedish for `Sub Ject`
sentencecase: 'test: Sub ject',
sentencecase_unicode: 'test: Мав зуъ', // Tajik for `Sub ject`
};

const parsed = {
empty: parse(messages.empty),
numeric: parse(messages.numeric),
lowercase: parse(messages.lowercase),
lowercase_unicode: parse(messages.lowercase_unicode),
mixedcase: parse(messages.mixedcase),
uppercase: parse(messages.uppercase),
uppercase_unicode: parse(messages.uppercase_unicode),
camelcase: parse(messages.camelcase),
camelcase_unicode: parse(messages.camelcase_unicode),
kebabcase: parse(messages.kebabcase),
kebabcase_unicode: parse(messages.kebabcase_unicode),
pascalcase: parse(messages.pascalcase),
pascalcase_unicode: parse(messages.pascalcase_unicode),
snakecase: parse(messages.snakecase),
snakecase_unicode: parse(messages.snakecase_unicode),
startcase: parse(messages.startcase),
startcase_unicode: parse(messages.startcase_unicode),
sentencecase: parse(messages.sentencecase),
sentencecase_unicode: parse(messages.sentencecase_unicode),
};

test('with empty subject should succeed for "never lowercase"', async () => {
Expand Down Expand Up @@ -63,6 +81,16 @@ test('with lowercase subject should succeed for "always lowercase"', async () =>
expect(actual).toEqual(expected);
});

test('with lowercase unicode subject should fail for "always uppercase"', async () => {
const [actual] = subjectCase(
await parsed.lowercase_unicode,
'always',
'upper-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with mixedcase subject should succeed for "never lowercase"', async () => {
const [actual] = subjectCase(await parsed.mixedcase, 'never', 'lowercase');
const expected = true;
Expand Down Expand Up @@ -93,12 +121,22 @@ test('with uppercase subject should fail for "never uppercase"', async () => {
expect(actual).toEqual(expected);
});

test('with lowercase subject should succeed for "always uppercase"', async () => {
test('with uppercase subject should succeed for "always uppercase"', async () => {
const [actual] = subjectCase(await parsed.uppercase, 'always', 'uppercase');
const expected = true;
expect(actual).toEqual(expected);
});

test('with uppercase unicode subject should fail for "always lowercase"', async () => {
const [actual] = subjectCase(
await parsed.uppercase_unicode,
'always',
'lower-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with camelcase subject should fail for "always uppercase"', async () => {
const [actual] = subjectCase(await parsed.camelcase, 'always', 'uppercase');
const expected = false;
Expand Down Expand Up @@ -135,6 +173,26 @@ test('with camelcase subject should succeed for "always camelcase"', async () =>
expect(actual).toEqual(expected);
});

test('with camelcase unicode subject should fail for "always sentencecase"', async () => {
const [actual] = subjectCase(
await parsed.camelcase_unicode,
'always',
'sentence-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with kebabcase unicode subject should fail for "always camelcase"', async () => {
const [actual] = subjectCase(
await parsed.kebabcase_unicode,
'always',
'camel-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with pascalcase subject should fail for "always uppercase"', async () => {
const [actual] = subjectCase(await parsed.pascalcase, 'always', 'uppercase');
const expected = false;
Expand Down Expand Up @@ -175,6 +233,16 @@ test('with pascalcase subject should fail for "always camelcase"', async () => {
expect(actual).toEqual(expected);
});

test('with pascalcase unicode subject should fail for "always uppercase"', async () => {
const [actual] = subjectCase(
await parsed.pascalcase_unicode,
'always',
'upper-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with snakecase subject should fail for "always uppercase"', async () => {
const [actual] = subjectCase(await parsed.snakecase, 'always', 'uppercase');
const expected = false;
Expand Down Expand Up @@ -211,6 +279,16 @@ test('with snakecase subject should fail for "always camelcase"', async () => {
expect(actual).toEqual(expected);
});

test('with snakecase unicode subject should fail for "never lowercase"', async () => {
const [actual] = subjectCase(
await parsed.snakecase_unicode,
'never',
'lower-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with startcase subject should fail for "always uppercase"', async () => {
const [actual] = subjectCase(await parsed.startcase, 'always', 'uppercase');
const expected = false;
Expand Down Expand Up @@ -253,6 +331,66 @@ test('with startcase subject should succeed for "always startcase"', async () =>
expect(actual).toEqual(expected);
});

test('with startcase unicode subject should fail for "always pascalcase"', async () => {
const [actual] = subjectCase(
await parsed.startcase_unicode,
'always',
'pascal-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with sentencecase subject should succeed for "always sentence-case"', async () => {
const [actual] = subjectCase(
await parsed.sentencecase,
'always',
'sentence-case'
);
const expected = true;
expect(actual).toEqual(expected);
});

test('with sentencecase subject should fail for "never sentencecase"', async () => {
const [actual] = subjectCase(
await parsed.sentencecase,
'never',
'sentence-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with sentencecase subject should fail for "always pascalcase"', async () => {
const [actual] = subjectCase(
await parsed.sentencecase,
'always',
'pascal-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('with sentencecase subject should succeed for "never camelcase"', async () => {
const [actual] = subjectCase(
await parsed.sentencecase,
'never',
'camel-case'
);
const expected = true;
expect(actual).toEqual(expected);
});

test('with sentencecase unicode subject should fail for "always camelcase"', async () => {
const [actual] = subjectCase(
await parsed.sentencecase_unicode,
'always',
'camel-case'
);
const expected = false;
expect(actual).toEqual(expected);
});

test('should use expected message with "always"', async () => {
const [, message] = subjectCase(
await parsed.uppercase,
Expand Down
19 changes: 18 additions & 1 deletion @commitlint/rules/src/subject-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@ import {case as ensureCase} from '@commitlint/ensure';
import message from '@commitlint/message';
import {TargetCaseType, SyncRule} from '@commitlint/types';

/**
* Since the rule requires first symbol of a subject to be a letter, use
* combination of Unicode `Cased_Letter` and `Other_Letter` categories now to
* allow non-Latin alphabets as well.
*
* Do not use `Letter` category directly to avoid capturing `Modifier_Letter`
* (which just modifiers letters, so we probably shouldn't anyway) and to stay
* close to previous implementation.
*
* Also, typescript does not seem to support almost any longhand category name
* (and even short for `Cased_Letter` too) so list all required letter
* categories manually just to prevent it from complaining about unknown stuff.
*
* @see [Unicode Categories]{@link https://www.regular-expressions.info/unicode.html}
*/
const startsWithLetterRegex = /^[\p{Ll}\p{Lu}\p{Lt}\p{Lo}]/iu;

const negated = (when?: string) => when === 'never';

export const subjectCase: SyncRule<TargetCaseType | TargetCaseType[]> = (
Expand All @@ -11,7 +28,7 @@ export const subjectCase: SyncRule<TargetCaseType | TargetCaseType[]> = (
) => {
const {subject} = parsed;

if (typeof subject !== 'string' || !subject.match(/^[a-z]/i)) {
if (typeof subject !== 'string' || !subject.match(startsWithLetterRegex)) {
return [true];
}

Expand Down

0 comments on commit 5f83423

Please sign in to comment.