Skip to content

Commit e820753

Browse files
authored
feat(lint): allow for custom commit parser function (#4829)
* feat(test): addes a test confirming functionality doesn't exist This adds tests to confirm that functionality for #4816 doesn't exist and doesn't work * 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 1bbd308 commit e820753

3 files changed

Lines changed: 87 additions & 1 deletion

File tree

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

Lines changed: 83 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,85 @@ test("passes for async rule", async () => {
308309

309310
expect(report.valid).toBe(true);
310311
});
312+
313+
test("custom parser output is used by rules", async () => {
314+
// The default parser extracts no type from this message; the custom parser supplies one,
315+
// so type-empty (never) only passes because the custom parser ran.
316+
const customParser: Parser = (message) => ({
317+
type: "feat",
318+
scope: null,
319+
subject: "my-feature",
320+
body: null,
321+
footer: null,
322+
header: message,
323+
});
324+
325+
const report = await lint(
326+
"a message with no conventional type",
327+
{
328+
"type-empty": [RuleConfigSeverity.Error, "never"],
329+
},
330+
{ parser: customParser },
331+
);
332+
333+
expect(report.valid).toBe(true);
334+
expect(report.errors.length).toBe(0);
335+
});
336+
337+
test("custom parser overrides the default parse result", async () => {
338+
// "feat: add thing" is a valid feat to the default parser; the custom parser rewrites the
339+
// type to "wip", so the commit only fails type-enum because the custom parser ran.
340+
const customParser: Parser = (message) => ({
341+
type: "wip",
342+
scope: null,
343+
subject: "add thing",
344+
body: null,
345+
footer: null,
346+
header: message,
347+
});
348+
349+
const report = await lint(
350+
"feat: add thing",
351+
{
352+
"type-enum": [RuleConfigSeverity.Error, "always", ["feat"]],
353+
},
354+
{ parser: customParser },
355+
);
356+
357+
expect(report.valid).toBe(false);
358+
expect(report.errors.length).toBe(1);
359+
});
360+
361+
test("custom parser receives parser options as second argument", async () => {
362+
let receivedOpts: Record<string, unknown> | undefined;
363+
364+
const customParser: Parser = (_message, opts) => {
365+
receivedOpts =
366+
typeof opts === "object" && opts !== null ? (opts as Record<string, unknown>) : {};
367+
return {
368+
header: "custom-parsed-header",
369+
type: "chore",
370+
subject: null,
371+
body: null,
372+
footer: null,
373+
merge: null,
374+
revert: null,
375+
notes: [],
376+
mentions: [],
377+
references: [],
378+
} as unknown as ReturnType<Parser>;
379+
};
380+
381+
const customOpts = { headerPattern: /^custom-parsed-header/ };
382+
383+
await lint(
384+
"any message",
385+
{
386+
"type-enum": [RuleConfigSeverity.Error, "always", ["chore"]],
387+
},
388+
{ parser: customParser, parserOpts: customOpts },
389+
);
390+
391+
expect(receivedOpts).toBeDefined();
392+
expect((receivedOpts as Record<string, unknown>).headerPattern).toBeDefined();
393+
});

@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 type { 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+
/** A custom parser function, used instead of the default conventional-commits-parser */
20+
parser?: Parser;
1821

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

0 commit comments

Comments
 (0)