Skip to content

Commit 180d82b

Browse files
committed
feat: add subject-exclamation-mark rule to improve error messages
When using the conventional commit feature of an `!` in the commit subject the angular config get really confused and gives some error messages that do not relate to the issue due to the message failing at the parse stage. This overrides the angular parser preset to add in the exclamation mark then uses the new rule to give a better error message. The result is with the message "fix!: the fix" previously the error message would be "subject may not be empty" now the error message is "subject must not have an exclamation mark in the subject to identify a breaking change". This message it more descriptive and will give the user info they need to resolve the issue. This also updates the docs to highlight the difference in angular and conventional configs, and point them in the direction of the conventional config if they want to use the `!` in the commit messages
1 parent 3d8fb54 commit 180d82b

File tree

9 files changed

+224
-17
lines changed

9 files changed

+224
-17
lines changed

@commitlint/config-angular/README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
# @commitlint/config-angular
44

5-
Shareable `commitlint` config enforcing the [Angular commit convention](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines).
5+
Shareable `commitlint` config enforcing the [Angular commit convention](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit).
66
Use with [@commitlint/cli](../cli) and [@commitlint/prompt-cli](../prompt-cli).
77

88
## Getting started
@@ -122,6 +122,17 @@ echo "fix: some message." # fails
122122
echo "fix: some message" # passes
123123
```
124124

125+
#### subject-exclamation-mark
126+
127+
- **condition**: `subject` must not have a `!` before the `:` marker
128+
- **rule**: `never`
129+
130+
The [angular commit
131+
convention](hhttps://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit)
132+
dose not use a `!` to define a breaking change in the commit subject. If you
133+
want to use this feature please consider using the [conventional commit
134+
config](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional#commitlintconfig-conventional).
135+
125136
#### header-max-length
126137

127138
- **condition**: `header` has `value` or less characters

@commitlint/config-angular/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const typeEnum = require('@commitlint/config-angular-type-enum');
22

33
module.exports = {
4+
parserPreset: {parserOpts: {headerPattern: /^(\w*)(?:\((.*)\))?!?: (.*)$/}},
45
rules: {
6+
'subject-exclamation-mark': [2, 'never'],
57
'body-leading-blank': [1, 'always'],
68
'footer-leading-blank': [1, 'always'],
79
'header-max-length': [2, 'always', 72],
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import lint from '@commitlint/lint';
2+
import {rules, parserPreset} from '.';
3+
4+
const lintMessage = async (message) => {
5+
const parserOpts = parserPreset.parserOpts;
6+
const m = message.replace(/^\s+/, '').trim();
7+
const result = await lint(m, rules, {parserOpts});
8+
9+
if (result.errors.length > 1) {
10+
throw new Error(
11+
'Commit test should only have one error message to validate against'
12+
);
13+
}
14+
15+
if (result.warnings.length > 1) {
16+
throw new Error(
17+
'Commit test should only have one warning message to validate against'
18+
);
19+
}
20+
21+
return result;
22+
};
23+
24+
test('a valid commit message', async () => {
25+
const result = await lintMessage('test: a valid angular commit');
26+
expect(result.valid).toBe(true);
27+
expect(result.errors).toStrictEqual([]);
28+
expect(result.warnings).toStrictEqual([]);
29+
});
30+
31+
test('a valid message with a scope', async () => {
32+
const result = await lintMessage(
33+
'test(scope): a valid angular commit with a scope'
34+
);
35+
expect(result.valid).toBe(true);
36+
expect(result.errors).toStrictEqual([]);
37+
expect(result.warnings).toStrictEqual([]);
38+
});
39+
40+
test('a valid multi line commit', async () => {
41+
const result = await lintMessage(
42+
`test(scope): a valid angular commit with a scope
43+
44+
Some content in the body`
45+
);
46+
expect(result.valid).toBe(true);
47+
expect(result.errors).toStrictEqual([]);
48+
expect(result.warnings).toStrictEqual([]);
49+
});
50+
51+
test('a leading blank line after header', async () => {
52+
const result = await lintMessage(
53+
`test(scope): a valid angular commit with a scope
54+
Some content in the body`
55+
);
56+
57+
expect(result.valid).toBe(true);
58+
expect(result.errors).toStrictEqual([]);
59+
expect(result.warnings[0].message).toBe('body must have leading blank line');
60+
});
61+
62+
test('an invalid scope', async () => {
63+
const result = await lintMessage(`no: no is not not an invalid commit type`);
64+
65+
expect(result.valid).toBe(false);
66+
expect(result.errors[0].message).toBe(
67+
'type must be one of [build, ci, docs, feat, fix, perf, refactor, revert, style, test]'
68+
);
69+
expect(result.warnings).toStrictEqual([]);
70+
});
71+
72+
test('a long header', async () => {
73+
const result = await lintMessage(
74+
`test: that its an error when there is ia realllllllllllllllllllllly long header`
75+
);
76+
77+
expect(result.valid).toBe(false);
78+
expect(result.errors[0].message).toBe(
79+
'header must not be longer than 72 characters, current length is 79'
80+
);
81+
expect(result.warnings).toStrictEqual([]);
82+
});
83+
84+
test('message header with ! in it', async () => {
85+
const result = await lintMessage(`test!: with a breaking change in the type`);
86+
87+
expect(result.valid).toBe(false);
88+
expect(result.errors[0].message).toBe(
89+
'subject must not have an exclamation mark in the subject to identify a breaking change'
90+
);
91+
expect(result.warnings).toStrictEqual([]);
92+
});
93+
94+
test('message header with ! in it and a scope', async () => {
95+
const result = await lintMessage(
96+
`test(scope)!: with a breaking change in the type`
97+
);
98+
99+
expect(result.valid).toBe(false);
100+
expect(result.errors[0].message).toBe(
101+
'subject must not have an exclamation mark in the subject to identify a breaking change'
102+
);
103+
expect(result.warnings).toStrictEqual([]);
104+
});

@commitlint/config-angular/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"node": ">=v12"
3030
},
3131
"devDependencies": {
32-
"@commitlint/utils": "^12.1.4"
32+
"@commitlint/utils": "^12.1.4",
33+
"@commitlint/lint": "^12.1.4"
3334
},
3435
"dependencies": {
3536
"@commitlint/config-angular-type-enum": "^12.1.4"

@commitlint/config-conventional/index.test.js

+19-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import lint from '@commitlint/lint';
2-
import {rules} from '.';
2+
import {rules, parserPreset} from '.';
3+
4+
const commitLint = async (message) => {
5+
const preset = await require(parserPreset)();
6+
return lint(message, rules, {...preset});
7+
};
38

49
const messages = {
510
invalidTypeEnum: 'foo: some message',
@@ -28,6 +33,7 @@ const messages = {
2833
'fix(scope): some Message',
2934
'fix(scope): some message\n\nBREAKING CHANGE: it will be significant!',
3035
'fix(scope): some message\n\nbody',
36+
'fix(scope)!: some message\n\nbody',
3137
],
3238
};
3339

@@ -107,31 +113,29 @@ const warnings = {
107113
};
108114

109115
test('type-enum', async () => {
110-
const result = await lint(messages.invalidTypeEnum, rules);
116+
const result = await commitLint(messages.invalidTypeEnum);
111117

112118
expect(result.valid).toBe(false);
113119
expect(result.errors).toEqual([errors.typeEnum]);
114120
});
115121

116122
test('type-case', async () => {
117-
const result = await lint(messages.invalidTypeCase, rules);
123+
const result = await commitLint(messages.invalidTypeCase);
118124

119125
expect(result.valid).toBe(false);
120126
expect(result.errors).toEqual([errors.typeCase, errors.typeEnum]);
121127
});
122128

123129
test('type-empty', async () => {
124-
const result = await lint(messages.invalidTypeEmpty, rules);
130+
const result = await commitLint(messages.invalidTypeEmpty);
125131

126132
expect(result.valid).toBe(false);
127133
expect(result.errors).toEqual([errors.typeEmpty]);
128134
});
129135

130136
test('subject-case', async () => {
131137
const invalidInputs = await Promise.all(
132-
messages.invalidSubjectCases.map((invalidInput) =>
133-
lint(invalidInput, rules)
134-
)
138+
messages.invalidSubjectCases.map((invalidInput) => commitLint(invalidInput))
135139
);
136140

137141
invalidInputs.forEach((result) => {
@@ -141,57 +145,57 @@ test('subject-case', async () => {
141145
});
142146

143147
test('subject-empty', async () => {
144-
const result = await lint(messages.invalidSubjectEmpty, rules);
148+
const result = await commitLint(messages.invalidSubjectEmpty);
145149

146150
expect(result.valid).toBe(false);
147151
expect(result.errors).toEqual([errors.subjectEmpty, errors.typeEmpty]);
148152
});
149153

150154
test('subject-full-stop', async () => {
151-
const result = await lint(messages.invalidSubjectFullStop, rules);
155+
const result = await commitLint(messages.invalidSubjectFullStop);
152156

153157
expect(result.valid).toBe(false);
154158
expect(result.errors).toEqual([errors.subjectFullStop]);
155159
});
156160

157161
test('header-max-length', async () => {
158-
const result = await lint(messages.invalidHeaderMaxLength, rules);
162+
const result = await commitLint(messages.invalidHeaderMaxLength);
159163

160164
expect(result.valid).toBe(false);
161165
expect(result.errors).toEqual([errors.headerMaxLength]);
162166
});
163167

164168
test('footer-leading-blank', async () => {
165-
const result = await lint(messages.warningFooterLeadingBlank, rules);
169+
const result = await commitLint(messages.warningFooterLeadingBlank, rules);
166170

167171
expect(result.valid).toBe(true);
168172
expect(result.warnings).toEqual([warnings.footerLeadingBlank]);
169173
});
170174

171175
test('footer-max-line-length', async () => {
172-
const result = await lint(messages.invalidFooterMaxLineLength, rules);
176+
const result = await commitLint(messages.invalidFooterMaxLineLength);
173177

174178
expect(result.valid).toBe(false);
175179
expect(result.errors).toEqual([errors.footerMaxLineLength]);
176180
});
177181

178182
test('body-leading-blank', async () => {
179-
const result = await lint(messages.warningBodyLeadingBlank, rules);
183+
const result = await commitLint(messages.warningBodyLeadingBlank);
180184

181185
expect(result.valid).toBe(true);
182186
expect(result.warnings).toEqual([warnings.bodyLeadingBlank]);
183187
});
184188

185189
test('body-max-line-length', async () => {
186-
const result = await lint(messages.invalidBodyMaxLineLength, rules);
190+
const result = await commitLint(messages.invalidBodyMaxLineLength);
187191

188192
expect(result.valid).toBe(false);
189193
expect(result.errors).toEqual([errors.bodyMaxLineLength]);
190194
});
191195

192196
test('valid messages', async () => {
193197
const validInputs = await Promise.all(
194-
messages.validMessages.map((input) => lint(input, rules))
198+
messages.validMessages.map((input) => commitLint(input))
195199
);
196200

197201
validInputs.forEach((result) => {

@commitlint/rules/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {subjectEmpty} from './subject-empty';
2626
import {subjectFullStop} from './subject-full-stop';
2727
import {subjectMaxLength} from './subject-max-length';
2828
import {subjectMinLength} from './subject-min-length';
29+
import {subjectExclamationMark} from './subject-exclamation-mark';
2930
import {trailerExists} from './trailer-exists';
3031
import {typeCase} from './type-case';
3132
import {typeEmpty} from './type-empty';
@@ -62,6 +63,7 @@ export default {
6263
'subject-full-stop': subjectFullStop,
6364
'subject-max-length': subjectMaxLength,
6465
'subject-min-length': subjectMinLength,
66+
'subject-exclamation-mark': subjectExclamationMark,
6567
'trailer-exists': trailerExists,
6668
'type-case': typeCase,
6769
'type-empty': typeEmpty,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import parse from '@commitlint/parse';
2+
import {subjectExclamationMark} from './subject-exclamation-mark';
3+
4+
const preset = require('conventional-changelog-angular');
5+
6+
const parseMessage = async (str: string) => {
7+
const {parserOpts} = await preset;
8+
return parse(str, undefined, parserOpts);
9+
};
10+
11+
const messages = {
12+
empty: 'test:\n',
13+
with: `test!: subject\n`,
14+
without: `test: subject\n`,
15+
};
16+
17+
const parsed = {
18+
empty: parseMessage(messages.empty),
19+
with: parseMessage(messages.with),
20+
without: parseMessage(messages.without),
21+
};
22+
23+
test('empty against "always" should fail', async () => {
24+
const [actual] = subjectExclamationMark(await parsed.empty, 'always');
25+
const expected = false;
26+
expect(actual).toEqual(expected);
27+
});
28+
29+
test('empty against "never" should succeed', async () => {
30+
const [actual] = subjectExclamationMark(await parsed.empty, 'never');
31+
const expected = true;
32+
expect(actual).toEqual(expected);
33+
});
34+
35+
test('with against "always" should succeed', async () => {
36+
const [actual] = subjectExclamationMark(await parsed.with, 'always');
37+
const expected = true;
38+
expect(actual).toEqual(expected);
39+
});
40+
41+
test('with against "never" should fail', async () => {
42+
const [actual] = subjectExclamationMark(await parsed.with, 'never');
43+
const expected = false;
44+
expect(actual).toEqual(expected);
45+
});
46+
47+
test('without against "always" should fail', async () => {
48+
const [actual] = subjectExclamationMark(await parsed.without, 'always');
49+
const expected = false;
50+
expect(actual).toEqual(expected);
51+
});
52+
53+
test('without against "never" should succeed', async () => {
54+
const [actual] = subjectExclamationMark(await parsed.without, 'never');
55+
const expected = true;
56+
expect(actual).toEqual(expected);
57+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import message from '@commitlint/message';
2+
import {SyncRule} from '@commitlint/types';
3+
4+
export const subjectExclamationMark: SyncRule = (parsed, when = 'always') => {
5+
const input = parsed.header;
6+
if (!input) {
7+
return [true, ''];
8+
}
9+
10+
const negated = when === 'never';
11+
const hasExclamationMark = /!:/.test(input);
12+
13+
return [
14+
negated ? !hasExclamationMark : hasExclamationMark,
15+
message([
16+
'subject',
17+
negated ? 'must not' : 'must',
18+
'have an exclamation mark in the subject to identify a breaking change',
19+
]),
20+
];
21+
};

docs/reference-rules.md

+5
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ Infinity
336336
0
337337
```
338338

339+
#### subject-exclamation-mark
340+
341+
- **condition**: `subject` has exclamation before the `:` marker
342+
- **rule**: `never`
343+
339344
#### type-enum
340345

341346
- **condition**: `type` is found in value

0 commit comments

Comments
 (0)