Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
95b592c
feat(cli): add --show-position flag to display error location
escapedcat Feb 27, 2026
684d015
test(lint): add tests for getRulePosition function
escapedcat Feb 27, 2026
083f8fc
fix: remove duplicate test names
escapedcat Feb 27, 2026
43ad330
fix: improve position indicator for empty field rules and fallback ha…
escapedcat Feb 27, 2026
f87bca6
fix: resolve remaining PR comments
escapedcat Feb 27, 2026
24a9b02
refactor: export shared Position type from @commitlint/types
escapedcat Feb 27, 2026
28849c6
refactor: generalize line handling and fix edge cases
escapedcat Feb 27, 2026
a8f3b91
feat!: enable position indicator by default
escapedcat Mar 1, 2026
1dbc322
fix(format): strip ANSI codes when computing position indicator offset
escapedcat May 2, 2026
e99b097
fix(lint): compute body start line from actual offset
escapedcat May 2, 2026
cfe7fdd
fix(lint): compute footer start line from actual offset
escapedcat May 2, 2026
a2cdb1f
fix(lint): provide position for body-leading-blank when blank is missing
escapedcat May 2, 2026
a4d4461
fix(lint): provide position for footer-leading-blank when blank is mi…
escapedcat May 2, 2026
a402e38
fix(lint): respect custom parser headerPattern for subject position
escapedcat May 2, 2026
c08992a
fix(format): render position indicator under the failing line for mul…
escapedcat May 2, 2026
ab55526
test(cli): match each input line individually for stdin-failure output
escapedcat May 3, 2026
cfe9a3b
test: cover position indicator fixes
escapedcat May 3, 2026
e0bad19
fix(lint): use lastIndexOf for subject position
escapedcat May 4, 2026
87bfde8
fix(lint): locate type in header instead of assuming raw starts with it
escapedcat May 4, 2026
7d95a7e
fix(lint): normalize CRLF before computing rule positions
escapedcat May 4, 2026
94ca031
fix(lint): point subject-exclamation-mark caret at the bang position
escapedcat May 4, 2026
89612ee
docs(format): document start/end problem fields for position indicator
escapedcat May 4, 2026
32ff054
test(cli): add integration coverage for --show-position default and o…
escapedcat May 4, 2026
dd59f49
fix(lint): pin subject-exclamation-mark caret to the bang adjacent to…
escapedcat May 4, 2026
fde3db1
fix(lint): point leading-blank carets at the actual section boundary
escapedcat May 4, 2026
0884f25
fix(lint): provide body-empty position for header-only commits
escapedcat May 4, 2026
1153f82
fix(lint): clamp body/footer end offset to raw.length
escapedcat May 4, 2026
54fc1d0
refactor(types): adopt Position in LintRuleOutcome and document units
escapedcat May 4, 2026
01945ae
docs(cli): add --show-position to the CLI reference
escapedcat May 4, 2026
3a9ec87
test(lint): add subject position test for custom parserOpts headerPat…
escapedcat May 4, 2026
79451a7
test(cli): assert full-message reprint preserves input line order
escapedcat May 4, 2026
834e8b3
refactor(lint): simplify getRulePosition with field-level helpers
escapedcat May 4, 2026
a1e3f5c
feat(cli): make --show-position opt-in (default off)
escapedcat May 4, 2026
037a6a9
test(config-conventional): assert positions explicitly
escapedcat May 4, 2026
5aed3d3
feat!: enable position indicator by default
escapedcat May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion @commitlint/cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,24 @@ test("should fail for input from stdin with rule from rc", async () => {
expect(result.exitCode).toBe(ExitCode.CommitlintErrorDefault);
});

test("should print position indicator caret by default on failure", async () => {
const cwd = await gitBootstrap("fixtures/simple");
const result = cli(["--color=false"], { cwd })("foo: bar");
const output = await result;
expect(output.stdout).toContain("^");
expect(result.exitCode).toBe(ExitCode.CommitlintErrorDefault);
});

test("should suppress position indicator when --no-show-position is set", async () => {
const cwd = await gitBootstrap("fixtures/simple");
const result = cli(["--color=false", "--no-show-position"], { cwd })(
"foo: bar",
);
const output = await result;
expect(output.stdout).not.toContain("^");
expect(result.exitCode).toBe(ExitCode.CommitlintErrorDefault);
});

test("should work with --config option", async () => {
const file = "config/commitlint.config.js";
const cwd = await gitBootstrap("fixtures/specify-config-file");
Expand Down Expand Up @@ -495,7 +513,14 @@ test("should print full commit message when input from stdin fails", async () =>
// output text in plain text so we can compare it
const result = cli(["--color=false"], { cwd })(input);
const output = await result;
expect(output.stdout.trim()).toContain(input);
// Each input line must appear in stdout *in the original order* —
// independent toContain checks would let scrambled output pass.
let cursor = 0;
for (const line of input.split("\n").filter((l) => l.length > 0)) {
const found = output.stdout.indexOf(line, cursor);
expect(found).toBeGreaterThanOrEqual(cursor);
cursor = found + line.length;
}
expect(result.exitCode).toBe(ExitCode.CommitlintErrorDefault);
});

Expand Down Expand Up @@ -644,6 +669,7 @@ test("should print help", async () => {
-q, --quiet toggle console output [boolean] [default: false]
-t, --to upper end of the commit range to lint; applies if edit=false [string]
-V, --verbose enable verbose output for reports without problems [boolean]
--show-position show position of error in output [boolean] [default: true]
-s, --strict enable strict mode; result code 2 for warnings, 3 for errors [boolean]
--options path to a JSON file or Common.js module containing CLI options
-v, --version display version information [boolean]
Expand Down
6 changes: 6 additions & 0 deletions @commitlint/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ const cli = yargs(process.argv.slice(2))
type: "boolean",
description: "enable verbose output for reports without problems",
},
"show-position": {
type: "boolean",
default: true,
description: "show position of error in output",
Comment thread
escapedcat marked this conversation as resolved.
},
strict: {
alias: "s",
type: "boolean",
Expand Down Expand Up @@ -398,6 +403,7 @@ async function main(args: MainArgs): Promise<void> {
color: flags.color,
verbose: flags.verbose,
helpUrl,
showPosition: flags["show-position"],
});

if (!flags.quiet && output !== "") {
Expand Down
1 change: 1 addition & 0 deletions @commitlint/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface CliFlags {
to?: string;
version?: boolean;
verbose?: boolean;
"show-position"?: boolean;
/** @type {'' | 'text' | 'json'} */
"print-config"?: string;
strict?: boolean;
Expand Down
126 changes: 114 additions & 12 deletions @commitlint/config-conventional/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,44 @@ test("type-enum", async () => {
const result = await commitLint(messages.invalidTypeEnum);

expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.typeEnum]);
expect(result.errors).toEqual([
{
...errors.typeEnum,
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 4, offset: 3 },
},
]);
});

test("type-case", async () => {
const result = await commitLint(messages.invalidTypeCase);

expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.typeCase, errors.typeEnum]);
expect(result.errors).toEqual([
{
...errors.typeCase,
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 4, offset: 3 },
},
{
...errors.typeEnum,
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 4, offset: 3 },
},
]);
});

test("type-empty", async () => {
const result = await commitLint(messages.invalidTypeEmpty);

expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.typeEmpty]);
expect(result.errors).toEqual([
{
...errors.typeEmpty,
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 },
},
]);
});

test("subject-case", async () => {
Expand All @@ -156,59 +179,138 @@ test("subject-case", async () => {
),
);

invalidInputs.forEach((result) => {
const headerPrefix = "fix(scope): ";
invalidInputs.forEach((result, i) => {
const input = messages.invalidSubjectCases[i];
const subject = input.slice(headerPrefix.length);
const offset = headerPrefix.length;
expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.subjectCase]);
expect(result.errors).toEqual([
{
...errors.subjectCase,
start: { line: 1, column: offset + 1, offset },
end: {
line: 1,
column: offset + subject.length + 1,
offset: offset + subject.length,
},
},
]);
});
});

test("subject-empty", async () => {
const result = await commitLint(messages.invalidSubjectEmpty);

expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.subjectEmpty, errors.typeEmpty]);
// "fix:" — header length 4; type "fix" at offset 0 length 3.
expect(result.errors).toEqual([
{
...errors.subjectEmpty,
start: { line: 1, column: 5, offset: 4 },
end: { line: 1, column: 5, offset: 4 },
},
{
...errors.typeEmpty,
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 },
},
]);
});

test("subject-full-stop", async () => {
const result = await commitLint(messages.invalidSubjectFullStop);

expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.subjectFullStop]);
// "fix: some message." — subject "some message." at offset 5, length 13
// (parser keeps the trailing period in parsed.subject).
expect(result.errors).toEqual([
{
...errors.subjectFullStop,
start: { line: 1, column: 6, offset: 5 },
end: { line: 1, column: 19, offset: 18 },
},
]);
});

test("header-max-length", async () => {
const result = await commitLint(messages.invalidHeaderMaxLength);
const header = messages.invalidHeaderMaxLength;

expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.headerMaxLength]);
expect(result.errors).toEqual([
{
...errors.headerMaxLength,
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: header.length + 1, offset: header.length },
},
]);
});

test("footer-leading-blank", async () => {
const result = await commitLint(messages.warningFooterLeadingBlank);
const message = messages.warningFooterLeadingBlank;
const footerOffset = message.indexOf("BREAKING CHANGE") - 1;

expect(result.valid).toBe(true);
expect(result.warnings).toEqual([warnings.footerLeadingBlank]);
expect(result.warnings).toEqual([
{
...warnings.footerLeadingBlank,
start: {
line: 3,
column: message.split("\n")[2].length + 1,
offset: footerOffset,
},
end: {
line: 3,
column: message.split("\n")[2].length + 1,
offset: footerOffset,
},
},
]);
});

test("footer-max-line-length", async () => {
const result = await commitLint(messages.invalidFooterMaxLineLength);

expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.footerMaxLineLength]);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toMatchObject(errors.footerMaxLineLength);
expect(result.errors[0].start).toBeDefined();
expect(result.errors[0].end).toBeDefined();
});

test("body-leading-blank", async () => {
const result = await commitLint(messages.warningBodyLeadingBlank);
const message = messages.warningBodyLeadingBlank;
const headerLength = message.split("\n")[0].length;

expect(result.valid).toBe(true);
expect(result.warnings).toEqual([warnings.bodyLeadingBlank]);
expect(result.warnings).toEqual([
{
...warnings.bodyLeadingBlank,
start: {
line: 1,
column: headerLength + 1,
offset: headerLength,
},
end: {
line: 1,
column: headerLength + 1,
offset: headerLength,
},
},
]);
});

test("body-max-line-length", async () => {
const result = await commitLint(messages.invalidBodyMaxLineLength);

expect(result.valid).toBe(false);
expect(result.errors).toEqual([errors.bodyMaxLineLength]);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toMatchObject(errors.bodyMaxLineLength);
expect(result.errors[0].start).toBeDefined();
expect(result.errors[0].end).toBeDefined();
});

test("valid messages", async () => {
Expand Down
Loading