diff --git a/@commitlint/cli/src/cli.test.ts b/@commitlint/cli/src/cli.test.ts index 6dcc637ca7..766f389ca2 100644 --- a/@commitlint/cli/src/cli.test.ts +++ b/@commitlint/cli/src/cli.test.ts @@ -606,6 +606,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] diff --git a/@commitlint/cli/src/cli.ts b/@commitlint/cli/src/cli.ts index 245640f665..e360f01047 100644 --- a/@commitlint/cli/src/cli.ts +++ b/@commitlint/cli/src/cli.ts @@ -134,6 +134,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", + }, strict: { alias: "s", type: "boolean", @@ -374,6 +379,7 @@ async function main(args: MainArgs): Promise { color: flags.color, verbose: flags.verbose, helpUrl, + showPosition: flags["show-position"], }); if (!flags.quiet && output !== "") { diff --git a/@commitlint/cli/src/types.ts b/@commitlint/cli/src/types.ts index cbc9a8956a..032645fdf9 100644 --- a/@commitlint/cli/src/types.ts +++ b/@commitlint/cli/src/types.ts @@ -17,6 +17,7 @@ export interface CliFlags { to?: string; version?: boolean; verbose?: boolean; + "show-position"?: boolean; /** @type {'' | 'text' | 'json'} */ "print-config"?: string; strict?: boolean; diff --git a/@commitlint/config-conventional/src/index.test.ts b/@commitlint/config-conventional/src/index.test.ts index cd847ff2d3..472eef0f24 100644 --- a/@commitlint/config-conventional/src/index.test.ts +++ b/@commitlint/config-conventional/src/index.test.ts @@ -132,21 +132,21 @@ test("type-enum", async () => { const result = await commitLint(messages.invalidTypeEnum); expect(result.valid).toBe(false); - expect(result.errors).toEqual([errors.typeEnum]); + expect(result.errors).toMatchObject([errors.typeEnum]); }); 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).toMatchObject([errors.typeCase, errors.typeEnum]); }); test("type-empty", async () => { const result = await commitLint(messages.invalidTypeEmpty); expect(result.valid).toBe(false); - expect(result.errors).toEqual([errors.typeEmpty]); + expect(result.errors).toMatchObject([errors.typeEmpty]); }); test("subject-case", async () => { @@ -158,7 +158,7 @@ test("subject-case", async () => { invalidInputs.forEach((result) => { expect(result.valid).toBe(false); - expect(result.errors).toEqual([errors.subjectCase]); + expect(result.errors).toMatchObject([errors.subjectCase]); }); }); @@ -166,49 +166,49 @@ test("subject-empty", async () => { const result = await commitLint(messages.invalidSubjectEmpty); expect(result.valid).toBe(false); - expect(result.errors).toEqual([errors.subjectEmpty, errors.typeEmpty]); + expect(result.errors).toMatchObject([errors.subjectEmpty, errors.typeEmpty]); }); test("subject-full-stop", async () => { const result = await commitLint(messages.invalidSubjectFullStop); expect(result.valid).toBe(false); - expect(result.errors).toEqual([errors.subjectFullStop]); + expect(result.errors).toMatchObject([errors.subjectFullStop]); }); test("header-max-length", async () => { const result = await commitLint(messages.invalidHeaderMaxLength); expect(result.valid).toBe(false); - expect(result.errors).toEqual([errors.headerMaxLength]); + expect(result.errors).toMatchObject([errors.headerMaxLength]); }); test("footer-leading-blank", async () => { const result = await commitLint(messages.warningFooterLeadingBlank); expect(result.valid).toBe(true); - expect(result.warnings).toEqual([warnings.footerLeadingBlank]); + expect(result.warnings).toMatchObject([warnings.footerLeadingBlank]); }); 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).toMatchObject([errors.footerMaxLineLength]); }); test("body-leading-blank", async () => { const result = await commitLint(messages.warningBodyLeadingBlank); expect(result.valid).toBe(true); - expect(result.warnings).toEqual([warnings.bodyLeadingBlank]); + expect(result.warnings).toMatchObject([warnings.bodyLeadingBlank]); }); 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).toMatchObject([errors.bodyMaxLineLength]); }); test("valid messages", async () => { diff --git a/@commitlint/format/src/format.test.ts b/@commitlint/format/src/format.test.ts index 3388e96afb..7f3a84e23c 100644 --- a/@commitlint/format/src/format.test.ts +++ b/@commitlint/format/src/format.test.ts @@ -303,3 +303,164 @@ test("format result should not contain `Get help` prefix if helpUrl is not provi expect.arrayContaining([expect.stringContaining("Get help:")]), ); }); + +test("shows position indicator when showPosition is true and error has position", () => { + const actual = format( + { + results: [ + { + errors: [ + { + level: 2, + name: "type-enum", + message: "type must be one of [feat, fix]", + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 4, offset: 3 }, + }, + ], + input: "foo: some message", + }, + ], + }, + { + showPosition: true, + color: false, + }, + ); + + expect(actual).toContain("^"); +}); + +test("does not show position indicator when showPosition is false", () => { + const actual = format( + { + results: [ + { + errors: [ + { + level: 2, + name: "type-enum", + message: "type must be one of [feat, fix]", + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 4, offset: 3 }, + }, + ], + input: "foo: some message", + }, + ], + }, + { + showPosition: false, + color: false, + }, + ); + + expect(actual).not.toContain("^"); +}); + +test("shows position indicator when showPosition is not provided (default)", () => { + const actual = format( + { + results: [ + { + errors: [ + { + level: 2, + name: "type-enum", + message: "type must be one of [feat, fix]", + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 4, offset: 3 }, + }, + ], + input: "foo: some message", + }, + ], + }, + { + color: false, + }, + ); + + expect(actual).toContain("^"); +}); + +test("does not show position indicator when error has no position", () => { + const actual = format( + { + results: [ + { + errors: [ + { + level: 2, + name: "type-enum", + message: "type must be one of [feat, fix]", + }, + ], + input: "foo: some message", + }, + ], + }, + { + showPosition: true, + color: false, + }, + ); + + expect(actual).not.toContain("^"); +}); + +test("shows correct position for subject error", () => { + const actual = format( + { + results: [ + { + errors: [ + { + level: 2, + name: "subject-max-length", + message: "subject must not be longer than 72 characters", + start: { line: 1, column: 10, offset: 9 }, + end: { line: 1, column: 50, offset: 49 }, + }, + ], + input: + "feat: this is a subject that is way too long for the commit message format", + }, + ], + }, + { + showPosition: true, + color: false, + }, + ); + + expect(actual).toContain("^"); +}); + +test("shows position indicator with single caret for longer errors", () => { + const actual = format( + { + results: [ + { + errors: [ + { + level: 2, + name: "header-max-length", + message: "header must not be longer than 100 characters", + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 80, offset: 79 }, + }, + ], + input: + "feat: this is a very long header that exceeds the maximum allowed character limit for the commit message", + }, + ], + }, + { + showPosition: true, + color: false, + }, + ); + + expect(actual).toContain("^"); +}); diff --git a/@commitlint/format/src/format.ts b/@commitlint/format/src/format.ts index 2496d7272e..8c5a96d977 100644 --- a/@commitlint/format/src/format.ts +++ b/@commitlint/format/src/format.ts @@ -5,6 +5,7 @@ import { FormatOptions, FormattableResult, WithInput, + FormattableProblem, } from "@commitlint/types"; const DEFAULT_SIGNS = [" ", "⚠", "✖"] as const; @@ -37,7 +38,7 @@ function formatInput( result: FormattableResult & WithInput, options: FormatOptions = {}, ): string[] { - const { color: enabled = true } = options; + const { color: enabled = true, showPosition = true } = options; const { errors = [], warnings = [], input = "" } = result; if (!input) { @@ -46,13 +47,57 @@ function formatInput( const sign = "⧗"; const decoration = enabled ? pc.gray(sign) : sign; + const prefix = `${decoration} input: `; const decoratedInput = enabled ? pc.bold(input) : input; const hasProblems = errors.length > 0 || warnings.length > 0; - return options.verbose || hasProblems - ? [`${decoration} input: ${decoratedInput}`] - : []; + if (!hasProblems) { + return options.verbose ? [`${prefix}${decoratedInput}`] : []; + } + + const positionIndicator = showPosition + ? getPositionIndicator([...errors, ...warnings], input, prefix.length) + : undefined; + + const lines: string[] = [`${prefix}${decoratedInput}`]; + + if (positionIndicator) { + lines.push(positionIndicator); + } + + return lines; +} + +function getPositionIndicator( + problems: FormattableProblem[], + input: string, + prefixLength: number, +): string | undefined { + const problemWithPosition = problems.find( + (problem) => problem?.start !== undefined && problem?.end !== undefined, + ); + if (!problemWithPosition?.start || !problemWithPosition?.end) { + return undefined; + } + + const padding = " ".repeat(prefixLength); + + const caret = "^"; + + const normalizedInput = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + const lines = normalizedInput.split("\n"); + const targetLine = lines[problemWithPosition.start.line - 1]; + + if (!targetLine) { + return undefined; + } + + const spacesBefore = Math.max(0, problemWithPosition.start.column - 1); + + const indicator = padding + " ".repeat(spacesBefore) + caret; + + return indicator; } export function formatResult( diff --git a/@commitlint/lint/src/lint.test.ts b/@commitlint/lint/src/lint.test.ts index 0d37aca402..29d95f30a3 100644 --- a/@commitlint/lint/src/lint.test.ts +++ b/@commitlint/lint/src/lint.test.ts @@ -169,7 +169,7 @@ test("throws for rule with out of range condition", async () => { }); test("succeds for issue", async () => { - const report = await lint("somehting #1", { + const report = await lint("something #1", { "references-empty": [RuleConfigSeverity.Error, "never"], }); @@ -177,7 +177,7 @@ test("succeds for issue", async () => { }); test("fails for issue", async () => { - const report = await lint("somehting #1", { + const report = await lint("something #1", { "references-empty": [RuleConfigSeverity.Error, "always"], }); @@ -186,7 +186,7 @@ test("fails for issue", async () => { test("succeds for custom issue prefix", async () => { const report = await lint( - "somehting REF-1", + "something REF-1", { "references-empty": [RuleConfigSeverity.Error, "never"], }, @@ -202,7 +202,7 @@ test("succeds for custom issue prefix", async () => { test("fails for custom issue prefix", async () => { const report = await lint( - "somehting #1", + "something #1", { "references-empty": [RuleConfigSeverity.Error, "never"], }, @@ -218,7 +218,7 @@ test("fails for custom issue prefix", async () => { test("fails for custom plugin rule", async () => { const report = await lint( - "somehting #1", + "something #1", { "plugin-rule": [RuleConfigSeverity.Error, "never"], }, @@ -238,7 +238,7 @@ test("fails for custom plugin rule", async () => { test("passes for custom plugin rule", async () => { const report = await lint( - "somehting #1", + "something #1", { "plugin-rule": [RuleConfigSeverity.Error, "never"], }, @@ -297,7 +297,7 @@ test("returns original message with commit header, body and footer, parsing comm test("passes for async rule", async () => { const report = await lint( - "somehting #1", + "something #1", { "async-rule": [RuleConfigSeverity.Error, "never"], }, @@ -314,3 +314,127 @@ test("passes for async rule", async () => { expect(report.valid).toBe(true); }); + +test("returns position for type-enum error", async () => { + const result = await lint("foo: some message", { + "type-enum": [RuleConfigSeverity.Error, "always", ["feat", "fix"]], + }); + expect(result.valid).toBe(false); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].name).toBe("type-enum"); + expect(result.errors[0].start).toEqual({ line: 1, column: 1, offset: 0 }); + expect(result.errors[0].end).toEqual({ line: 1, column: 4, offset: 3 }); +}); + +test("returns position for type-case error", async () => { + const result = await lint("FIX: some message", { + "type-case": [RuleConfigSeverity.Error, "always", "lower-case"], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("type-case"); + expect(result.errors[0].start).toEqual({ line: 1, column: 1, offset: 0 }); + expect(result.errors[0].end).toEqual({ line: 1, column: 4, offset: 3 }); +}); + +test("returns position for type-max-length error", async () => { + const longType = "toolongtype"; + const result = await lint(`${longType}: some message`, { + "type-max-length": [RuleConfigSeverity.Error, "always", 5], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("type-max-length"); + expect(result.errors[0].start).toEqual({ line: 1, column: 1, offset: 0 }); + expect(result.errors[0].end).toEqual({ + line: 1, + column: longType.length + 1, + offset: longType.length, + }); +}); + +test("returns position for scope-enum error", async () => { + const result = await lint("feat(badscope): some message", { + "scope-enum": [RuleConfigSeverity.Error, "always", ["cli", "core"]], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("scope-enum"); + expect(result.errors[0].start).toEqual({ line: 1, column: 6, offset: 5 }); + expect(result.errors[0].end).toEqual({ line: 1, column: 14, offset: 13 }); +}); + +test("returns position for scope-case error", async () => { + const result = await lint("feat(SCOPE): some message", { + "scope-case": [RuleConfigSeverity.Error, "always", "lower-case"], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("scope-case"); + expect(result.errors[0].start).toEqual({ line: 1, column: 6, offset: 5 }); + expect(result.errors[0].end).toEqual({ line: 1, column: 11, offset: 10 }); +}); + +test("returns position for subject-max-length error", async () => { + const longSubject = + "this is a very long subject that exceeds the maximum allowed characters"; + const result = await lint(`feat: ${longSubject}`, { + "subject-max-length": [RuleConfigSeverity.Error, "always", 20], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("subject-max-length"); + expect(result.errors[0].start?.line).toBe(1); + expect(result.errors[0].start?.column).toBeGreaterThan(5); + expect(result.errors[0].end?.line).toBe(1); +}); + +test("returns position for subject-full-stop error", async () => { + const result = await lint("feat: some message.", { + "subject-full-stop": [RuleConfigSeverity.Error, "never", "."], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("subject-full-stop"); + expect(result.errors[0].start?.line).toBe(1); + expect(result.errors[0].start?.column).toBeGreaterThan(5); +}); + +test("returns position for header-max-length error", async () => { + const longHeader = + "feat: this is a very long header that definitely exceeds the maximum allowed character limit for commit messages"; + const result = await lint(longHeader, { + "header-max-length": [RuleConfigSeverity.Error, "always", 50], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("header-max-length"); + expect(result.errors[0].start).toEqual({ line: 1, column: 1, offset: 0 }); + expect(result.errors[0].end).toEqual({ + line: 1, + column: longHeader.length + 1, + offset: longHeader.length, + }); +}); + +test("returns position for body-max-line-length error", async () => { + const longBodyLine = + "this is a body line that is way too long and exceeds the maximum allowed character limit of one hundred characters for each line in the body"; + const result = await lint(`feat: some message\n\n${longBodyLine}`, { + "body-max-line-length": [RuleConfigSeverity.Error, "always", 80], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("body-max-line-length"); + expect(result.errors[0].start?.line).toBe(2); +}); + +test("returns no position for rules without position support", async () => { + const result = await lint("something #1", { + "references-empty": [RuleConfigSeverity.Error, "always"], + }); + expect(result.valid).toBe(false); + expect(result.errors[0].name).toBe("references-empty"); + expect(result.errors[0].start).toBeUndefined(); + expect(result.errors[0].end).toBeUndefined(); +}); + +test("returns correct position for valid commit (no position needed)", async () => { + const result = await lint("feat: add new feature", { + "type-enum": [RuleConfigSeverity.Error, "always", ["feat", "fix"]], + }); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); +}); diff --git a/@commitlint/lint/src/lint.ts b/@commitlint/lint/src/lint.ts index c64bb829e0..21c1cdd603 100644 --- a/@commitlint/lint/src/lint.ts +++ b/@commitlint/lint/src/lint.ts @@ -10,11 +10,190 @@ import type { BaseRule, RuleType, QualifiedRules, + Position, } from "@commitlint/types"; import { RuleConfigSeverity } from "@commitlint/types"; import { buildCommitMessage } from "./commit-message.js"; +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; + }, +): { start: Position; end: Position } | undefined { + const raw = parsed.raw || ""; + if (!raw) return undefined; + + const header = parsed.header || ""; + + switch (ruleName) { + case "type-enum": + case "type-empty": + case "type-case": + case "type-min-length": + case "type-max-length": { + if (!parsed.type) { + if (ruleName === "type-empty") { + const offset = 0; + return { + start: { line: 1, column: offset + 1, offset }, + end: { line: 1, column: offset + 1, offset }, + }; + } + 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, + }, + }; + } + case "scope-enum": + case "scope-empty": + case "scope-case": + case "scope-min-length": + case "scope-max-length": + case "scope-delimiter-style": { + if (!parsed.scope) { + if (ruleName === "scope-empty") { + const typeEnd = parsed.type ? parsed.type.length : 0; + const offset = typeEnd + 1; + return { + start: { line: 1, column: offset + 1, offset }, + end: { line: 1, column: offset + 2, offset: offset + 1 }, + }; + } + return undefined; + } + const scopeStart = raw.indexOf(`(${parsed.scope})`); + if (scopeStart === -1) return undefined; + return { + start: { line: 1, column: scopeStart + 2, offset: scopeStart + 1 }, + end: { + line: 1, + column: scopeStart + parsed.scope.length + 2, + offset: scopeStart + parsed.scope.length + 1, + }, + }; + } + case "subject-empty": + case "subject-case": + case "subject-min-length": + case "subject-max-length": + case "subject-full-stop": + case "subject-exclamation-mark": { + if (!parsed.subject) { + if (ruleName === "subject-empty") { + const typeEnd = parsed.type ? parsed.type.length : 0; + const hasScope = parsed.scope ? parsed.scope.length + 3 : 0; + const separator = ": ".length; + const offset = typeEnd + hasScope + separator; + return { + start: { line: 1, column: offset + 1, offset }, + end: { line: 1, column: offset + 1, offset }, + }; + } + return undefined; + } + const typeEnd = parsed.type ? parsed.type.length : 0; + const hasScope = parsed.scope ? parsed.scope.length + 3 : 0; + const separator = ": ".length; + const subjectStart = typeEnd + hasScope + separator; + return { + start: { line: 1, column: subjectStart + 1, offset: subjectStart }, + end: { + line: 1, + column: subjectStart + parsed.subject.length + 1, + offset: subjectStart + parsed.subject.length, + }, + }; + } + case "header-min-length": + case "header-max-length": + case "header-case": + case "header-full-stop": + case "header-trim": { + if (!header) return undefined; + return { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: header.length + 1, offset: header.length }, + }; + } + case "body-empty": + case "body-min-length": + case "body-max-length": + case "body-case": + case "body-full-stop": + case "body-leading-blank": + case "body-max-line-length": { + if (!parsed.body) { + if (ruleName === "body-empty") { + const bodyOffset = raw.indexOf("\n\n"); + if (bodyOffset === -1) return undefined; + return { + start: { line: 2, column: 1, offset: bodyOffset + 2 }, + end: { line: 2, column: 1, offset: bodyOffset + 2 }, + }; + } + return undefined; + } + const bodyOffset = raw.indexOf("\n\n"); + if (bodyOffset === -1) return undefined; + const bodyStartOffset = bodyOffset + 2; + return { + start: { line: 2, column: 1, offset: bodyStartOffset }, + end: { + line: 2, + column: parsed.body.length + 1, + offset: bodyStartOffset + parsed.body.length, + }, + }; + } + case "footer-empty": + case "footer-min-length": + case "footer-max-length": + case "footer-leading-blank": + case "footer-max-line-length": { + if (!parsed.footer) { + if (ruleName === "footer-empty") { + const footerOffset = raw.lastIndexOf("\n\n"); + if (footerOffset === -1) return undefined; + return { + start: { line: 3, column: 1, offset: footerOffset + 2 }, + end: { line: 3, column: 1, offset: footerOffset + 2 }, + }; + } + return undefined; + } + const footerOffset = raw.lastIndexOf("\n\n"); + if (footerOffset === -1) return undefined; + const footerStartOffset = footerOffset + 2; + return { + start: { line: 3, column: 1, offset: footerStartOffset }, + end: { + line: 3, + column: parsed.footer.length + 1, + offset: footerStartOffset + parsed.footer.length, + }, + }; + } + default: + return undefined; + } +} + export default async function lint( message: string, rawRulesConfig?: QualifiedRules, @@ -169,12 +348,18 @@ export default async function lint( const executableRule = rule as Rule; const [valid, message] = await executableRule(parsed, when, value); - return { + const position = !valid ? getRulePosition(name, parsed) : undefined; + + const outcome: LintRuleOutcome = { level, valid, name, - message, + message: message ?? "", + start: position?.start, + end: position?.end, }; + + return outcome; }); const results = (await Promise.all(pendingResults)).filter( diff --git a/@commitlint/types/src/format.ts b/@commitlint/types/src/format.ts index ca9f87495a..bf7eae426e 100644 --- a/@commitlint/types/src/format.ts +++ b/@commitlint/types/src/format.ts @@ -7,10 +7,18 @@ export type Formatter = ( options: FormatOptions, ) => string; +export interface Position { + line: number; + column: number; + offset: number; +} + export interface FormattableProblem { level: RuleConfigSeverity; name: keyof QualifiedRules; message: string; + start?: Position; + end?: Position; } export interface FormattableResult { @@ -41,4 +49,5 @@ export interface FormatOptions { colors?: readonly [PicocolorsColor, PicocolorsColor, PicocolorsColor]; verbose?: boolean; helpUrl?: string; + showPosition?: boolean; } diff --git a/@commitlint/types/src/lint.ts b/@commitlint/types/src/lint.ts index 7640efc362..f694f4e6a2 100644 --- a/@commitlint/types/src/lint.ts +++ b/@commitlint/types/src/lint.ts @@ -42,4 +42,8 @@ export interface LintRuleOutcome { name: string; /** The message returned from the rule, if invalid */ message: string; + /** The start position of the error in the input */ + start?: { line: number; column: number; offset: number }; + /** The end position of the error in the input */ + end?: { line: number; column: number; offset: number }; } diff --git a/docs/api/format.md b/docs/api/format.md index 55a8ead843..9d4750b840 100644 --- a/docs/api/format.md +++ b/docs/api/format.md @@ -60,6 +60,11 @@ type formatOptions = { * URL to print as help for reports with problems **/ helpUrl: string; + + /** + * Show position indicator (^) for errors in the input line + **/ + showPosition?: boolean; } format(report?: Report = {}, options?: formatOptions = {}) => string[];