Skip to content

feat(cli)!: show position of error in commit input #633#4629

Draft
escapedcat wants to merge 35 commits into
masterfrom
feat/633_show-position-option
Draft

feat(cli)!: show position of error in commit input #633#4629
escapedcat wants to merge 35 commits into
masterfrom
feat/633_show-position-option

Conversation

@escapedcat

@escapedcat escapedcat commented Feb 27, 2026

Copy link
Copy Markdown
Member

Closes #633.

Adds a position indicator (^) under the commit input pointing at
where each linting error occurs, similar to how TypeScript / Rust
point at the offending span. For multi-line commits the caret renders
under the failing row (body / footer rules included), not at the end
of input.

$ echo 'foo: not good' | commitlint
⧗ input: foo: not good
^
✖ type must be one of [...] [type-enum]

Changes

  • New --show-position CLI flag (default true); equivalent
    showPosition option in @commitlint/format. Disable with
    --no-show-position or showPosition: false.
  • LintRuleOutcome and FormattableProblem gain optional
    start / end Position fields, exported from @commitlint/types.
  • @commitlint/lint computes positions field-by-field (type / scope /
    subject / header / body / footer span based on the rule prefix),
    with exact-character placement for subject-exclamation-mark,
    body-leading-blank, and footer-leading-blank.
  • @commitlint/format renders the caret under the failing line and
    indents multi-line input continuation under the input column.

BREAKING CHANGE

The position indicator is shown by default and an extra line is
inserted between the input and the problem list. Tools that scrape
commitlint stdout by line number need updating, or can disable the
indicator with --no-show-position / showPosition: false.

Notes

  • Parser-aware: positions are located within the parsed header /
    body / footer rather than computed from the default
    type(scope): subject layout, so custom parserOpts.headerPattern
    values still get a caret. CRLF input is normalized.
  • One known limitation, deferred to a follow-up: when
    parserOpts.commentChar filters comment lines, offsets computed
    against parsed.raw can drift relative to the rendered input.

@qodo-code-review

qodo-code-review Bot commented Feb 27, 2026

Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #633
🟢 Extend (“pimp”) the rules/core API as needed so rule violations can provide
location/position information that the formatter/CLI can render.
Make the error position in the commit message more visible in the output by adding a
visual indicator (e.g., a squiggly/~~~ underline) pointing to the problematic area.
Confirm the UX matches the ticket’s intended default behavior (PR makes it opt-in via
--show-position) and verify output correctness across real commit messages/rules beyond
the covered tests.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Edge case handling: The new getPositionIndicator() logic relies on hard-coded padding and \n\n splitting and
may mis-handle inputs with \r\n, multi-line bodies/footers, or differing rendered prefix
lengths, potentially producing missing/misaligned indicators rather than a clear graceful
fallback.

Referred Code
function getPositionIndicator(
	problems: FormattableProblem[],
	input: string,
): string | undefined {
	const firstError = problems[0];
	if (!firstError?.start || !firstError?.end) {
		return undefined;
	}

	const { start, end } = firstError;
	const padding = "           ";

	const tilde = "~";
	let indicator = "";

	if (start.line === 1) {
		const spacesBefore = Math.max(0, start.column - 1);
		const tildeLength = Math.max(1, end.column - start.column);
		indicator = padding + " ".repeat(spacesBefore) + tilde.repeat(tildeLength);
	} else if (start.line === 2) {
		const headerEndIndex = input.indexOf("\n\n");


 ... (clipped 32 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@codesandbox-ci

codesandbox-ci Bot commented Feb 27, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@qodo-code-review

qodo-code-review Bot commented Feb 27, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Ensure correct position for commit type
Suggestion Impact:Updated the type rule position logic to require raw.startsWith(parsed.type) and set offset to 0, preventing incorrect matches later in the commit message.

code diff:

@@ -45,8 +45,8 @@
 		case "type-min-length":
 		case "type-max-length": {
 			if (!parsed.type) return undefined;
-			const offset = raw.indexOf(parsed.type);
-			if (offset === -1) return undefined;
+			if (!raw.startsWith(parsed.type)) return undefined;
+			const offset = 0;
 			return {
 				start: { line: 1, column: offset + 1, offset },
 				end: {

To ensure the correct position of the commit type is found, replace
raw.indexOf(parsed.type) with a check using raw.startsWith(parsed.type), as the
type must be at the beginning of the commit message.

@commitlint/lint/src/lint.ts [42-58]

 		case "type-enum":
 		case "type-empty":
 		case "type-case":
 		case "type-min-length":
 		case "type-max-length": {
 			if (!parsed.type) return undefined;
-			const offset = raw.indexOf(parsed.type);
-			if (offset === -1) return undefined;
+			if (!raw.startsWith(parsed.type)) return undefined;
+			const offset = 0;
 			return {
 				start: { line: 1, column: offset + 1, offset },
 				end: {
 					line: 1,
 					column: offset + parsed.type.length + 1,
 					offset: offset + parsed.type.length,
 				},
 			};
 		}

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug in the new position-finding logic where indexOf could match the wrong part of the commit message, and the proposed fix using startsWith is more robust and accurate.

Medium
Fix incorrect position indicator length
Suggestion Impact:Updated body/footer line-length calculation to use only the first line of body/footer text and adjusted tilde length clamping to line bounds, addressing multi-line and off-by-one highlighting issues (with a slight variation allowing start.column <= lineLength + 1). Also expanded position indicator input to include warnings.

code diff:

@@ -91,16 +91,15 @@
 		const headerEndIndex = input.indexOf("\n\n");
 		if (headerEndIndex === -1) return undefined;
 
-		const bodyLineStart = headerEndIndex + 2;
-		const charsOnLine = input.slice(bodyLineStart).indexOf("\n");
-		const lineLength =
-			charsOnLine === -1 ? input.length - bodyLineStart : charsOnLine;
+		const bodyText = input.slice(headerEndIndex + 2);
+		const firstBodyLine = bodyText.split("\n")[0];
+		const lineLength = firstBodyLine.length;
 
-		if (start.column <= lineLength) {
+		if (start.column <= lineLength + 1) {
 			const spacesBefore = Math.max(0, start.column - 1);
 			const tildeLength = Math.max(
 				1,
-				Math.min(end.column, lineLength) - start.column,
+				Math.min(end.column - start.column, lineLength - (start.column - 1)),
 			);
 			indicator =
 				padding + " ".repeat(spacesBefore) + tilde.repeat(tildeLength);
@@ -109,12 +108,16 @@
 		const footerStartIndex = input.lastIndexOf("\n\n");
 		if (footerStartIndex === -1) return undefined;
 
-		const footerLineStart = footerStartIndex + 2;
-		const lineLength = input.length - footerLineStart;
+		const footerText = input.slice(footerStartIndex + 2);
+		const firstFooterLine = footerText.split("\n")[0];
+		const lineLength = firstFooterLine.length;
 
-		if (start.column <= lineLength) {
+		if (start.column <= lineLength + 1) {
 			const spacesBefore = Math.max(0, start.column - 1);
-			const tildeLength = Math.max(1, end.column - start.column);
+			const tildeLength = Math.max(
+				1,
+				Math.min(end.column - start.column, lineLength - (start.column - 1)),
+			);
 			indicator =
 				padding + " ".repeat(spacesBefore) + tilde.repeat(tildeLength);

Fix the calculation for the position indicator's length for body and footer
errors to correctly handle multi-line content and prevent off-by-one errors in
highlighting.

@commitlint/format/src/format.ts [90-121]

 	} else if (start.line === 2) {
 		const headerEndIndex = input.indexOf("\n\n");
 		if (headerEndIndex === -1) return undefined;
 
 		const bodyLineStart = headerEndIndex + 2;
-		const charsOnLine = input.slice(bodyLineStart).indexOf("\n");
-		const lineLength =
-			charsOnLine === -1 ? input.length - bodyLineStart : charsOnLine;
+		const bodyText = input.slice(bodyLineStart);
+		const firstBodyLine = bodyText.split("\n")[0];
+		const lineLength = firstBodyLine.length;
 
 		if (start.column <= lineLength) {
 			const spacesBefore = Math.max(0, start.column - 1);
 			const tildeLength = Math.max(
 				1,
-				Math.min(end.column, lineLength) - start.column,
+				Math.min(end.column - start.column, lineLength - (start.column - 1)),
 			);
 			indicator =
 				padding + " ".repeat(spacesBefore) + tilde.repeat(tildeLength);
 		}
 	} else if (start.line === 3) {
 		const footerStartIndex = input.lastIndexOf("\n\n");
 		if (footerStartIndex === -1) return undefined;
 
 		const footerLineStart = footerStartIndex + 2;
-		const lineLength = input.length - footerLineStart;
+		const footerText = input.slice(footerLineStart);
+		const firstFooterLine = footerText.split("\n")[0];
+		const lineLength = firstFooterLine.length;
 
 		if (start.column <= lineLength) {
 			const spacesBefore = Math.max(0, start.column - 1);
-			const tildeLength = Math.max(1, end.column - start.column);
+			const tildeLength = Math.max(
+				1,
+				Math.min(end.column - start.column, lineLength - (start.column - 1)),
+			);
 			indicator =
 				padding + " ".repeat(spacesBefore) + tilde.repeat(tildeLength);
 		}
 	}

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies and fixes a bug in the position indicator logic for multi-line body and footer content, which would otherwise lead to incorrect error highlighting.

Medium
High-level
Rule-specific error location is imprecise

The centralized getRulePosition helper function is imprecise for some rules,
highlighting entire sections like the body. It would be more accurate and
scalable if each rule reported its own specific error coordinates.

Examples:

@commitlint/lint/src/lint.ts [24-147]
function getRulePosition(
	ruleName: string,
	parsed: {
		raw?: string;
		header?: string | null;
		type?: string | null;
		subject?: string | null;
		scope?: string | null;
		body?: string | null;
		footer?: string | null;

 ... (clipped 114 lines)

Solution Walkthrough:

Before:

// In @commitlint/lint/src/lint.ts
async function lint(message, rules, opts) {
  // ...
  const parsed = await parse(message, opts.parserOpts);
  // ...
  const pendingResults = activeRules.map(async ([name, config]) => {
    const rule = allRules.get(name);
    const [valid, message] = await rule(parsed, when, value);

    const position = !valid ? getRulePosition(name, parsed) : undefined;

    return { level, valid, name, message, ...position };
  });
  // ...
}

function getRulePosition(ruleName, parsed) {
  // switch/case on ruleName to guess position
  // For body rules, highlights the entire body
}

After:

// In @commitlint/lint/src/lint.ts
async function lint(message, rules, opts) {
  // ...
  const parsed = await parse(message, opts.parserOpts);
  // ...
  const pendingResults = activeRules.map(async ([name, config]) => {
    const rule = allRules.get(name);
    // Rule returns {valid, message, start?, end?}
    const result = await rule(parsed, when, value);

    return { level, name, ...result };
  });
  // ...
}

// Each rule file would be updated, e.g. body-max-line-length.ts
const bodyMaxLineLength = (parsed, when, value) => {
  // ... logic to find the specific line that is too long
  const errorLine = ...;
  const position = { start: ..., end: ... }; // a precise position
  return { valid: false, message: "...", ...position };
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a design limitation in the new getRulePosition function, where error highlighting can be imprecise for rules targeting large text blocks, which impacts the core quality of the new feature.

Medium
General
Generalize line handling
Suggestion Impact:Updated body/footer position handling to derive the first line length by slicing and splitting on "\n" rather than manual index calculations, and adjusted column bounds/tilde length calculations accordingly (though it did not fully generalize to arbitrary line numbers).

code diff:

@@ -91,16 +91,15 @@
 		const headerEndIndex = input.indexOf("\n\n");
 		if (headerEndIndex === -1) return undefined;
 
-		const bodyLineStart = headerEndIndex + 2;
-		const charsOnLine = input.slice(bodyLineStart).indexOf("\n");
-		const lineLength =
-			charsOnLine === -1 ? input.length - bodyLineStart : charsOnLine;
+		const bodyText = input.slice(headerEndIndex + 2);
+		const firstBodyLine = bodyText.split("\n")[0];
+		const lineLength = firstBodyLine.length;
 
-		if (start.column <= lineLength) {
+		if (start.column <= lineLength + 1) {
 			const spacesBefore = Math.max(0, start.column - 1);
 			const tildeLength = Math.max(
 				1,
-				Math.min(end.column, lineLength) - start.column,
+				Math.min(end.column - start.column, lineLength - (start.column - 1)),
 			);
 			indicator =
 				padding + " ".repeat(spacesBefore) + tilde.repeat(tildeLength);
@@ -109,12 +108,16 @@
 		const footerStartIndex = input.lastIndexOf("\n\n");
 		if (footerStartIndex === -1) return undefined;
 
-		const footerLineStart = footerStartIndex + 2;
-		const lineLength = input.length - footerLineStart;
+		const footerText = input.slice(footerStartIndex + 2);
+		const firstFooterLine = footerText.split("\n")[0];
+		const lineLength = firstFooterLine.length;
 
-		if (start.column <= lineLength) {
+		if (start.column <= lineLength + 1) {
 			const spacesBefore = Math.max(0, start.column - 1);
-			const tildeLength = Math.max(1, end.column - start.column);
+			const tildeLength = Math.max(
+				1,
+				Math.min(end.column - start.column, lineLength - (start.column - 1)),
+			);
 			indicator =
 				padding + " ".repeat(spacesBefore) + tilde.repeat(tildeLength);
 		}

Refactor getPositionIndicator to handle any line number dynamically by splitting
the input into lines, which reduces code duplication and improves
maintainability.

@commitlint/format/src/format.ts [86-121]

-if (start.line === 1) {
-  // handle header
-  ...
-} else if (start.line === 2) {
-  // handle body
-  ...
-} else if (start.line === 3) {
-  // handle footer
-  ...
-}
+const lines = input.replace(/\r\n/g, "\n").split("\n");
+const lineText = lines[start.line - 1];
+if (!lineText) return undefined;
+const spacesBefore = Math.max(0, start.column - 1);
+const tildeLength = Math.max(1, Math.min(end.column, lineText.length + 1) - start.column);
+const prefix = `${enabled ? pc.gray(sign) : sign}   input: `;
+const padding = " ".repeat(prefix.length);
+indicator = padding + " ".repeat(spacesBefore) + tilde.repeat(tildeLength);

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: This suggestion provides a significant refactoring that simplifies the getPositionIndicator function, removes duplicated code, and makes the logic more robust and extensible for handling errors on any line.

Medium
  • Update

This comment was marked as resolved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/format/src/format.ts Outdated
Comment thread @commitlint/format/src/format.ts Outdated
Comment thread @commitlint/types/src/format.ts
Comment thread @commitlint/lint/src/lint.test.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
@escapedcat

Copy link
Copy Markdown
Member Author

@knocte wdyt?

@escapedcat escapedcat requested a review from JounQin February 28, 2026 10:55
@knocte

knocte commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

@knocte wdyt?

I love this! But IMO:

  • Why add a flag and not just enable this by default?
  • Why use multiple ~ chars instead of just one single ^ like most errors use?

@escapedcat

Copy link
Copy Markdown
Member Author
* Why add a flag and not just enable this by default?

I'm worried this would be a breaking change and mess up whatever people do with current output format

* Why use multiple `~` chars instead of just one single `^` like most errors use?

As it was described in the issue and also as I mostly know it from other linters I guess

@knocte

knocte commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

I'm worried this would be a breaking change and mess up whatever people do with current output format

I figured, but think about it: by being a flag this feature would be not discoverable at all, most people will not use it because they don't know about it. As a consequence of this, at some point you will think of making it the default, and then the breaking change will happen. Why not make the breaking change already? Just do a higher version in the bump. And let people file bugs, we'll fix them?

As it was described in the issue

In the issue they use an example of something that has known length (the string "thisDoesNotExist") and so the compiler prints as many ~ chars as the length of the string. But does this really apply to commitlint? Do all rules' violations have to do with a specific string that has certain lenght? My guess is no, for example: if title of commit message is too long (e.g. 65 chars, so exceeds the configured limit 50), is commitlint going to print as many as 15 ~ chars? Also, the longer the title is, the more likely it is that horizontal wrapping will make those chars not really align with the previous string. On the other hand, if you use just one single ^ char here, you could just point to the 50th char, as meaning: "that's the max length for the title".

and also as I mostly know it from other linters I guess

Funny you say that because I spotted some compiler errors from typescript, pasted by people into issues, and all I saw is just a single ^ char myself, maybe the use of ~ is only in special cases.

@escapedcat escapedcat changed the title feat(cli): add --show-position flag to display error location feat(cli)!: add --show-position flag to display error location Mar 1, 2026
@escapedcat escapedcat force-pushed the feat/633_show-position-option branch from 7729dae to 1b2ab42 Compare March 1, 2026 16:33
Comment thread @commitlint/format/src/format.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/format/src/format.test.ts
Comment thread @commitlint/cli/src/cli.ts
Comment thread @commitlint/format/src/format.ts
Comment thread @commitlint/format/src/format.ts Outdated
Comment thread docs/api/format.md Outdated
Comment thread @commitlint/types/src/lint.ts Outdated
Comment thread @commitlint/format/src/format.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
@knocte

knocte commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Wow, copilot feedback is really good

@escapedcat escapedcat force-pushed the feat/633_show-position-option branch 4 times, most recently from 13def8c to 720e751 Compare March 2, 2026 11:40
Adds a new --show-position CLI option that displays a position indicator
(~~~) under the commit input to show exactly where the error occurs,
similar to TypeScript's red squiggly lines.

This helps users quickly identify the problematic part of their commit
message.

Features:
- Add optional start/end position fields to LintRuleOutcome and FormattableProblem
- Add getRulePosition() helper to calculate error positions for various rules
- Add showPosition option to FormatOptions
- Add --show-position CLI flag (opt-in, default false)
- Add tests for position indicator in format package
- Update config-conventional tests to use toMatchObject for backward compatibility

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/api/format.md
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/lint/src/lint.ts Outdated
Comment thread @commitlint/cli/src/cli.ts
escapedcat and others added 6 commits May 4, 2026 13:11
header.indexOf(parsed.subject) returns the wrong occurrence when
type and subject share text. For headers like "foo: foo" the caret
landed on the type rather than the subject for every subject-* rule.

Subject sits at the end of the header in every conventional-commits
grammar, so lastIndexOf is robust against type/scope text appearing
inside the subject string and still works for custom parser presets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous logic short-circuited via raw.startsWith(parsed.type),
so any custom parser preset whose header pattern doesn't begin with
the type token (e.g. ----feat: subject) caused type-* rules to
return no position.

Search for the type within the header so the position is computed
correctly regardless of how the headerPattern places the type token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
raw.indexOf("\n\n") and raw.lastIndexOf("\n\n") never match
"\r\n\r\n", so body-* and footer-* rules returned no position for
Windows-style commit messages.

Normalize raw line endings to \n once at the top of getRulePosition.
The formatter already normalizes the same way before splitting into
lines, so positions still align with the rendered input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
subject-exclamation-mark validates the breaking-change "!" before the
colon, not the subject text itself. Grouping it with the subject-*
rules made the caret land on the subject string, which is misleading
output for the rule that fires when "!" is present or missing.

Split it into its own case: when "!" is present in the header point
at it; otherwise point at the colon (where "!" would belong).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The format API docs added showPosition but didn't mention that
callers must populate start/end on each problem for the caret to
render. Programmatic users of @commitlint/format had no way to know
which fields wire up the new behavior.

Document both fields on the Problem type, including a note that
they are required for the indicator to render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pt-out

The new --show-position flag was only covered by the help-snapshot
assertion, which catches changes to the help text but not regressions
in the yargs default wiring or formatter integration.

Add two CLI-level tests:
- default invocation prints the caret on failure
- --no-show-position suppresses the caret

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@escapedcat escapedcat changed the title feat(cli)!: add --show-position flag to display error location feat(cli)!: show position of error in commit input #633 May 4, 2026
escapedcat and others added 12 commits May 4, 2026 13:36
… colon

header.indexOf("!") matched any "!" in the header, including ones
inside the subject text. For "feat: hello! world" the caret pointed
at the bang inside the subject rather than at the colon position
where the breaking-change marker is missing.

Find the colon first, then check whether "!" sits immediately
before it — that's the only position where the conventional-commits
breaking-change marker is valid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both body-leading-blank and footer-leading-blank had a fallback that
searched raw for "\n\n", which can match a paragraph break *inside*
the body and put the caret on the wrong line. The rule fires for
the leading blank specifically, so the caret should always point at
the section boundary.

- body-leading-blank: point at header.length (header/body boundary).
- footer-leading-blank: locate the footer in raw and point at the
  character immediately before it (body/footer boundary).

Tighten the existing tests to assert the expected offset rather
than just toBeDefined, plus add a regression test for a body that
contains its own "\n\n" paragraph break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a commit has no body section at all, raw.indexOf("\n\n")
returned -1 and the rule reported no position even though it had
fired. Fall back to the end of the header so the caret lands where
the missing body would belong.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parsed.body / parsed.footer may have been trimmed or normalized by
the parser, so bodyStart + parsed.body.length can exceed the actual
raw range. Clamp to raw.length so the end Position never points
past the actual input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier "export shared Position type" refactor missed the
inline { line, column, offset } shapes in LintRuleOutcome. Replace
them with the exported Position type so the public surface uses a
single named shape.

Also document the units on Position itself: line/column are
1-indexed, offset is 0-indexed character (not display-width)
positions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new --show-position flag was wired up in code and the help
snapshot but missed in docs/reference/cli.md. Add it to the options
listing so the rendered docs match the live --help output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tern

The fix that switched type/subject lookup from hard-coded offsets
to header-aware searching wasn't covered by a positive position
assertion against a non-default parserOpts.headerPattern. Add a
test using the type-scope-subject grammar (the same one already
used elsewhere in the suite) to lock in the parser-aware behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous loop did independent toContain checks, which would
incorrectly pass for scrambled output. Walk the lines forward
through stdout and require each next line to appear at or after
the previous one's end, so the test fails if the formatter ever
reorders the input lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the per-rule switch with a small set of field span helpers
plus three exact-character special cases (subject-exclamation-mark,
body-leading-blank, footer-leading-blank). Field is inferred from the
rule-name prefix, which removes the per-new-rule maintenance tax and
keeps behavior consistent across the type/scope/subject/header/body/
footer rule families.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the default-on rollout from "feat!: enable position indicator
by default". The output-format change now ships behind --show-position
so downstream consumers that parse commitlint output are not disrupted.
The flag and its --no-show-position counterpart remain available; the
caret renders identically when enabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace toMatchObject with toEqual and pin the start/end values for
each conventional-config error fixture. toMatchObject silently ignored
the new position fields, leaving them unverified end-to-end; the
explicit expectations lock in field-level position behavior across
type/subject/header/body/footer rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the opt-in rollout. The caret renders by default; the field-
level rewrite has narrowed the bug surface enough that the
interactive-UX win outweighs the small risk of disrupting downstream
output parsers, and shipping it on in this release avoids a separate
default-flip in a future major. Disable with --no-show-position on
the CLI or showPosition: false in formatOptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@knocte

This comment was marked as outdated.

@escapedcat

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +120 to +121
if (blank === -1) return undefined;
const start = blank + 2;
Comment on lines +132 to +133
if (blank === -1) return undefined;
const start = blank + 2;
}
if (ruleName === "footer-leading-blank") {
if (!parsed.footer) return undefined;
const footerStart = raw.indexOf(parsed.footer);
ruleName: string,
parsed: ParsedCommit,
): { start: Position; end: Position } | undefined {
const raw = (parsed.raw || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
Comment on lines +92 to +96
const parenStart = header.indexOf(`(${parsed.scope})`);
const offset =
parenStart >= 0 ? parenStart + 1 : header.lastIndexOf(parsed.scope);
if (offset === -1) return undefined;
return span(raw, offset, offset + parsed.scope.length);
Comment on lines +107 to +110
const padding = " ".repeat(prefixLength);
const caret = "^";
const spacesBefore = Math.max(0, problemWithPosition.start.column - 1);
const text = padding + " ".repeat(spacesBefore) + caret;
@escapedcat escapedcat marked this pull request as draft May 8, 2026 08:26
@knocte

knocte commented May 11, 2026

Copy link
Copy Markdown
Contributor

I'm sad this didn't make it to v21.x.

For 22.x I guess?

@escapedcat

Copy link
Copy Markdown
Member Author

Yeah, will pick it up again eventually

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Make error (position) more visible in output

3 participants