Skip to content

Commit 97c5cf3

Browse files
committed
feat(lint): allow for custom commit parser function
This change makes it possible to pass a custom function to the top level lint function, which is passed through to the parse function and consequently passed through to the parser exposed by convential-commits-parser. In linting it is important to parse content to be linted with a high level of fidelity so linting can be applied accurately. The default parser the is used, conventional-commits-parser makes many assumptions about the format of the commit to perform its parsing which leads to parsing errors and inaccuracies which in turn lead to many false positives in linting, and incorrect or invalid errors in additional confusing error messages. This change will allow end users utilize a more accurate parser for their linting purposes in the short term while issues with the default parser are reconciled Resolves: #4816
1 parent 9cd6a50 commit 97c5cf3

4 files changed

Lines changed: 152 additions & 1 deletion

File tree

@commitlint/lint/src/lint.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { test, expect } from "vitest";
2+
import type { Parser } from "@commitlint/types";
23
import { RuleConfigSeverity } from "@commitlint/types";
34

45
import lint from "./lint.js";
@@ -308,3 +309,79 @@ test("passes for async rule", async () => {
308309

309310
expect(report.valid).toBe(true);
310311
});
312+
313+
test("uses custom parser when provided", async () => {
314+
const customParser: Parser = (message) => ({
315+
type: "feat",
316+
scope: null,
317+
subject: "my-feature",
318+
body: null,
319+
footer: null,
320+
header: message.trim(),
321+
});
322+
323+
const report = await lint(
324+
"any message",
325+
{
326+
"type-enum": [RuleConfigSeverity.Error, "always", ["feat"]],
327+
},
328+
{ parser: customParser },
329+
);
330+
331+
expect(report.valid).toBe(true);
332+
expect(report.errors.length).toBe(0);
333+
});
334+
335+
test("custom parser output is used by rules", async () => {
336+
const customParser: Parser = (_message) => ({
337+
type: null,
338+
scope: "auth",
339+
subject: "fix login",
340+
body: null,
341+
footer: null,
342+
header: "scope(subject): description",
343+
});
344+
345+
const report = await lint(
346+
"any message",
347+
{
348+
"type-empty": [RuleConfigSeverity.Error, "never"],
349+
},
350+
{ parser: customParser },
351+
);
352+
353+
expect(report.valid).toBe(false);
354+
expect(report.errors.length).toBe(1);
355+
});
356+
357+
test("custom parser works alongside parserOpts", async () => {
358+
const customParser: Parser = (_message) =>
359+
({
360+
type: "chore",
361+
subject: "dep bump",
362+
body: null,
363+
footer: "Refs #42",
364+
header: "chore: dep bump",
365+
references: [
366+
{
367+
raw: "Refs #42",
368+
action: "Refs",
369+
owner: null,
370+
repository: null,
371+
issue: "42",
372+
prefix: "#",
373+
},
374+
],
375+
}) as unknown as ReturnType<Parser>;
376+
377+
const report = await lint(
378+
"any message",
379+
{
380+
"type-enum": [RuleConfigSeverity.Error, "always", ["chore"]],
381+
"references-empty": [RuleConfigSeverity.Error, "never"],
382+
},
383+
{ parser: customParser },
384+
);
385+
386+
expect(report.valid).toBe(true);
387+
});

@commitlint/lint/src/lint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default async function lint(
3737
const parsed =
3838
message === ""
3939
? { header: null, body: null, footer: null }
40-
: await parse(message, undefined, opts.parserOpts);
40+
: await parse(message, opts.parser, opts.parserOpts);
4141

4242
if (parsed.header === null && parsed.body === null && parsed.footer === null) {
4343
// Commit is empty, skip

@commitlint/types/src/lint.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ParserOptions as Options } from "conventional-commits-parser";
22
import { IsIgnoredOptions } from "./is-ignored.js";
33
import { PluginRecords } from "./load.js";
4+
import { Parser } from "./parse.js";
45
import { RuleConfigSeverity, RuleConfigTuple } from "./rules.js";
56

67
export type LintRuleConfig = Record<
@@ -15,6 +16,8 @@ export interface LintOptions {
1516
ignores?: IsIgnoredOptions["ignores"];
1617
/** The parser configuration to use when linting the commit */
1718
parserOpts?: Options;
19+
/** An optional custom parser function for parsing commit messages */
20+
parser?: Parser;
1821

1922
plugins?: PluginRecords;
2023
helpUrl?: string;

docs/reference/configuration.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,77 @@ export default {
253253

254254
:::
255255

256+
### Custom parser function
257+
258+
You can provide a custom commit parsing implementation by supplying a `parser` function in your configuration. This is useful if you have specific parsing requirements that cannot be met by existing presets or options.
259+
260+
The custom function must return an object matching the [`Commit` type from `conventional-commits-parser`](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#commit), which includes all fields from `CommitBase`: `header`, `type`, `scope`, `subject`, `body`, `footer`, `merge`, `revert`, `notes`, `mentions`, and `references`. The `raw` property must be omitted (it is set by commitlint internally).
261+
262+
Omitting any of the array fields (`notes`, `mentions`, `references`) will cause lint rules like `references-empty` to crash, since they expect arrays rather than `undefined`:
263+
264+
::: code-group
265+
266+
```js [commitlint.config.js]
267+
export default {
268+
parserPreset: {
269+
parser: (message, options) => {
270+
// Instantiate your custom parsing implementation
271+
const parser = new MyParser(options);
272+
const parsed = parser.parse(message);
273+
274+
// Return all required CommitBase fields from the parsed result
275+
return {
276+
header: parsed.header ?? message.trim(),
277+
type: parsed.type ?? null,
278+
scope: parsed.scope ?? null,
279+
subject: parsed.subject ?? null,
280+
body: parsed.body ?? null,
281+
footer: parsed.footer ?? null,
282+
merge: null,
283+
revert: null,
284+
notes: parsed.notes ?? [],
285+
mentions: parsed.mentions ?? [],
286+
references: parsed.references ?? [],
287+
};
288+
},
289+
},
290+
};
291+
```
292+
293+
```ts [commitlint.config.ts]
294+
import type { Parser as CommitParser } from "@commitlint/types";
295+
296+
const myParser: CommitParser = (message, options) => {
297+
// Instantiate your custom parsing implementation
298+
const parser = new MyParser(options);
299+
const parsed = parser.parse(message);
300+
301+
return {
302+
header: parsed.header ?? message.trim(),
303+
type: parsed.type ?? null,
304+
scope: parsed.scope ?? null,
305+
subject: parsed.subject ?? null,
306+
body: parsed.body ?? null,
307+
footer: parsed.footer ?? null,
308+
merge: null,
309+
revert: null,
310+
notes: parsed.notes ?? [],
311+
mentions: parsed.mentions ?? [],
312+
references: parsed.references ?? [],
313+
};
314+
};
315+
316+
export default {
317+
parserPreset: {
318+
parser: myParser,
319+
},
320+
};
321+
```
322+
323+
:::
324+
325+
### Common `parserOpts`
326+
256327
### Common `parserOpts`
257328
258329
The parser is powered by [`conventional-commits-parser`](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser). Common options include:

0 commit comments

Comments
 (0)