Skip to content

Commit f43433d

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 81358f9 commit f43433d

File tree

8 files changed

+197
-16
lines changed

8 files changed

+197
-16
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://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y).
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](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y)
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],
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import lint from '@commitlint/lint';
2+
import {rules, parserPreset} from '.';
3+
4+
export const testCommit = async ({message, valid, error, warning}) => {
5+
const m = message.replace(/^\s+/, '').trim();
6+
test(m.split('\n')[0], async () => {
7+
const parserOpts = parserPreset.parserOpts;
8+
const result = await lint(m, rules, {parserOpts});
9+
10+
expect(result.valid).toBe(valid);
11+
12+
if (result.errors.length > 1) {
13+
console.log(errors);
14+
fail(
15+
'Commit test should only have one error message to validate against'
16+
);
17+
} else if (result.errors.length) {
18+
expect(result.errors[0].message).toBe(error);
19+
}
20+
21+
if (result.warnings.length > 1) {
22+
fail(
23+
'Commit test should only have one warning message to validate against'
24+
);
25+
} else if (result.warnings.length) {
26+
expect(result.warnings[0].message).toBe(warning);
27+
}
28+
});
29+
};
30+
31+
testCommit({
32+
message: `test: a valid angular commit`,
33+
valid: true,
34+
});
35+
36+
testCommit({
37+
message: `test(scope): a valid angular commit with a scope`,
38+
valid: true,
39+
});
40+
41+
testCommit({
42+
message: `
43+
test(scope): a valid angular commit with a scope
44+
45+
Some content in the body
46+
`,
47+
valid: true,
48+
});
49+
50+
testCommit({
51+
message: `
52+
test(scope): a valid angular commit with a scope
53+
Some content in the body
54+
`,
55+
valid: true,
56+
warning: 'body must have leading blank line',
57+
});
58+
59+
testCommit({
60+
message: `no: no is not not an invalid commit type`,
61+
valid: false,
62+
error:
63+
'type must be one of [build, ci, docs, feat, fix, perf, refactor, revert, style, test]',
64+
});
65+
66+
testCommit({
67+
message: `test: that its an error when there is ia realllllllllllllllllllllly long header`,
68+
valid: false,
69+
error: 'header must not be longer than 72 characters, current length is 79',
70+
});
71+
72+
testCommit({
73+
message: `test!: with a breaking change in the type`,
74+
valid: false,
75+
error:
76+
'subject must not have an exclamation mark in the subject to identify a breaking change',
77+
});
78+
79+
testCommit({
80+
message: `test(scope)!: with a breaking change in the type with scope`,
81+
valid: false,
82+
error:
83+
'subject must not have an exclamation mark in the subject to identify a breaking change',
84+
});

@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 {typeCase} from './type-case';
3031
import {typeEmpty} from './type-empty';
3132
import {typeEnum} from './type-enum';
@@ -61,6 +62,7 @@ export default {
6162
'subject-full-stop': subjectFullStop,
6263
'subject-max-length': subjectMaxLength,
6364
'subject-min-length': subjectMinLength,
65+
'subject-exclamation-mark': subjectExclamationMark,
6466
'type-case': typeCase,
6567
'type-empty': typeEmpty,
6668
'type-enum': typeEnum,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import parse from '@commitlint/parse';
2+
import {subjectExclamationMark} from './subject-exclamation-mark';
3+
4+
const {parserOpts} = require('@commitlint/config-angular');
5+
6+
const messages = {
7+
empty: 'test:\n',
8+
with: `test!: subject\n`,
9+
without: `test: subject\n`,
10+
};
11+
12+
const parsed = {
13+
empty: parse(messages.empty, undefined, parserOpts),
14+
with: parse(messages.with, undefined, parserOpts),
15+
without: parse(messages.without, undefined, parserOpts),
16+
};
17+
18+
test('empty against "always" should fail', async () => {
19+
const [actual] = subjectExclamationMark(await parsed.empty, 'always');
20+
const expected = false;
21+
expect(actual).toEqual(expected);
22+
});
23+
24+
test('empty against "never" should succeed', async () => {
25+
const [actual] = subjectExclamationMark(await parsed.empty, 'never');
26+
const expected = true;
27+
expect(actual).toEqual(expected);
28+
});
29+
30+
test('with against "always" should succeed', async () => {
31+
const [actual] = subjectExclamationMark(await parsed.with, 'always');
32+
const expected = true;
33+
expect(actual).toEqual(expected);
34+
});
35+
36+
test('with against "never" should fail', async () => {
37+
const [actual] = subjectExclamationMark(await parsed.with, 'never');
38+
const expected = false;
39+
expect(actual).toEqual(expected);
40+
});
41+
42+
test('without against "always" should fail', async () => {
43+
const [actual] = subjectExclamationMark(await parsed.without, 'always');
44+
const expected = false;
45+
expect(actual).toEqual(expected);
46+
});
47+
48+
test('without against "never" should succeed', async () => {
49+
const [actual] = subjectExclamationMark(await parsed.without, 'never');
50+
const expected = true;
51+
expect(actual).toEqual(expected);
52+
});
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)