Files
Nehemiya WickramasingheandGitHub 936a3bc8b6 breaking(code-frame): use 0-based columns to match Babel AST locations (#1… (#17849)
fix(code-frame): use 0-based columns to match Babel AST locations (#17316)

`codeFrameColumns` treated the `column` property as 1-based, but Babel
AST locations use 0-based columns. This caused the caret/underline to
be off by one character when passing AST `.loc` objects directly.

- Removed the `- 1` offset in marker spacing calculation
- Removed the `+ 1` in multiline start marker count
- Changed default column sentinel from `0` to `null` so that
  `column: 0` is treated as a valid position
- Updated falsiness checks (`!startColumn`) to null checks
- Removed `+ 1` workarounds in babel-core and babel-traverse callers

This is a breaking change for external consumers of `@babel/code-frame`
who passed 1-based columns manually.
2026-05-11 09:38:53 -04:00

518 lines
12 KiB
JavaScript

import { stripVTControlCharacters } from "node:util";
import { codeFrameColumns, highlight } from "../lib/index.js";
describe("@babel/code-frame", function () {
test("basic usage", function () {
const rawLines = ["class Foo {", " constructor()", "};"].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 2, column: 15 } }),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor()",
" | ^",
" 3 | };",
].join("\n"),
);
});
test("optional column number", function () {
const rawLines = ["class Foo {", " constructor()", "};"].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 2, column: null } }),
).toEqual(
[" 1 | class Foo {", "> 2 | constructor()", " 3 | };"].join("\n"),
);
});
test("maximum context lines and padding", function () {
const rawLines = [
"/**",
" * Sums two numbers.",
" *",
" * @param a Number",
" * @param b Number",
" * @returns Number",
" */",
"",
"function sum(a, b) {",
" return a + b",
"}",
].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 7, column: 1 } }),
).toEqual(
[
" 5 | * @param b Number",
" 6 | * @returns Number",
"> 7 | */",
" | ^",
" 8 |",
" 9 | function sum(a, b) {",
" 10 | return a + b",
].join("\n"),
);
});
test("no unnecessary padding due to one-off errors", function () {
const rawLines = [
"/**",
" * Sums two numbers.",
" *",
" * @param a Number",
" * @param b Number",
" * @returns Number",
" */",
"",
"function sum(a, b) {",
" return a + b",
"}",
].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 6, column: 1 } }),
).toEqual(
[
" 4 | * @param a Number",
" 5 | * @param b Number",
"> 6 | * @returns Number",
" | ^",
" 7 | */",
" 8 |",
" 9 | function sum(a, b) {",
].join("\n"),
);
});
test("tabs", function () {
const rawLines = [
"\tclass Foo {",
"\t \t\t constructor\t(\t)",
"\t};",
].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 2, column: 24 } }),
).toEqual(
[
" 1 | \tclass Foo {",
"> 2 | \t \t\t constructor\t(\t)",
" | \t \t\t \t \t ^",
" 3 | \t};",
].join("\n"),
);
});
test("opts.linesAbove", function () {
const rawLines = [
"/**",
" * Sums two numbers.",
" *",
" * @param a Number",
" * @param b Number",
" * @returns Number",
" */",
"",
"function sum(a, b) {",
" return a + b",
"}",
].join("\n");
expect(
codeFrameColumns(
rawLines,
{ start: { line: 7, column: 1 } },
{ linesAbove: 1 },
),
).toEqual(
[
" 6 | * @returns Number",
"> 7 | */",
" | ^",
" 8 |",
" 9 | function sum(a, b) {",
" 10 | return a + b",
].join("\n"),
);
});
test("opts.linesBelow", function () {
const rawLines = [
"/**",
" * Sums two numbers.",
" *",
" * @param a Number",
" * @param b Number",
" * @returns Number",
" */",
"",
"function sum(a, b) {",
" return a + b",
"}",
].join("\n");
expect(
codeFrameColumns(
rawLines,
{ start: { line: 7, column: 1 } },
{ linesBelow: 1 },
),
).toEqual(
[
" 5 | * @param b Number",
" 6 | * @returns Number",
"> 7 | */",
" | ^",
" 8 |",
].join("\n"),
);
});
test("opts.linesAbove and opts.linesBelow", function () {
const rawLines = [
"/**",
" * Sums two numbers.",
" *",
" * @param a Number",
" * @param b Number",
" * @returns Number",
" */",
"",
"function sum(a, b) {",
" return a + b",
"}",
].join("\n");
expect(
codeFrameColumns(
rawLines,
{ start: { line: 7, column: 1 } },
{ linesAbove: 1, linesBelow: 1 },
),
).toEqual(
[" 6 | * @returns Number", "> 7 | */", " | ^", " 8 |"].join("\n"),
);
});
test("opts.linesAbove no lines above", function () {
const rawLines = [
"class Foo {",
" constructor() {",
" console.log(arguments);",
" }",
"};",
].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 2 } }, { linesAbove: 0 }),
).toEqual(
[
"> 2 | constructor() {",
" 3 | console.log(arguments);",
" 4 | }",
" 5 | };",
].join("\n"),
);
});
test("opts.linesBelow no lines below", function () {
const rawLines = [
"class Foo {",
" constructor() {",
" console.log(arguments);",
" }",
"};",
].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 2 } }, { linesBelow: 0 }),
).toEqual([" 1 | class Foo {", "> 2 | constructor() {"].join("\n"));
});
test("opts.linesBelow single line", function () {
const rawLines = [
"class Foo {",
" constructor() {",
" console.log(arguments);",
" }",
"};",
].join("\n");
expect(
codeFrameColumns(
rawLines,
{ start: { line: 2 } },
{ linesAbove: 0, linesBelow: 0 },
),
).toEqual(["> 2 | constructor() {"].join("\n"));
});
test("basic usage, new API", function () {
const rawLines = ["class Foo {", " constructor()", "};"].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 2, column: 15 } }),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor()",
" | ^",
" 3 | };",
].join("\n"),
);
});
test("mark multiple columns", function () {
const rawLines = ["class Foo {", " constructor()", "};"].join("\n");
expect(
codeFrameColumns(rawLines, {
start: { line: 2, column: 2 },
end: { line: 2, column: 15 },
}),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor()",
" | ^^^^^^^^^^^^^",
" 3 | };",
].join("\n"),
);
});
test("mark multiple columns across lines", function () {
const rawLines = ["class Foo {", " constructor() {", " }", "};"].join(
"\n",
);
expect(
codeFrameColumns(rawLines, {
start: { line: 2, column: 16 },
end: { line: 3, column: 3 },
}),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor() {",
" | ^",
"> 3 | }",
" | ^^^",
" 4 | };",
].join("\n"),
);
});
test("mark multiple columns across multiple lines", function () {
const rawLines = [
"class Foo {",
" constructor() {",
" console.log(arguments);",
" }",
"};",
].join("\n");
expect(
codeFrameColumns(rawLines, {
start: { line: 2, column: 16 },
end: { line: 4, column: 3 },
}),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor() {",
" | ^",
"> 3 | console.log(arguments);",
" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^",
"> 4 | }",
" | ^^^",
" 5 | };",
].join("\n"),
);
});
test("mark across multiple lines without columns", function () {
const rawLines = [
"class Foo {",
" constructor() {",
" console.log(arguments);",
" }",
"};",
].join("\n");
expect(
codeFrameColumns(rawLines, { start: { line: 2 }, end: { line: 4 } }),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor() {",
"> 3 | console.log(arguments);",
"> 4 | }",
" 5 | };",
].join("\n"),
);
});
test("opts.message", function () {
const rawLines = ["class Foo {", " constructor()", "};"].join("\n");
expect(
codeFrameColumns(
rawLines,
{ start: { line: 2, column: 15 } },
{
message: "Missing {",
},
),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor()",
" | ^ Missing {",
" 3 | };",
].join("\n"),
);
});
test("opts.message without column", function () {
const rawLines = ["class Foo {", " constructor()", "};"].join("\n");
expect(
codeFrameColumns(
rawLines,
{ start: { line: 2 } },
{
message: "Missing {",
},
),
).toEqual(
[
" Missing {",
" 1 | class Foo {",
"> 2 | constructor()",
" 3 | };",
].join("\n"),
);
});
test("opts.message with multiple lines", function () {
const rawLines = [
"class Foo {",
" constructor() {",
" console.log(arguments);",
" }",
"};",
].join("\n");
expect(
codeFrameColumns(
rawLines,
{
start: { line: 2, column: 16 },
end: { line: 4, column: 3 },
},
{
message: "something about the constructor body",
},
),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor() {",
" | ^",
"> 3 | console.log(arguments);",
" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^",
"> 4 | }",
" | ^^^ something about the constructor body",
" 5 | };",
].join("\n"),
);
});
test("opts.message with multiple lines without columns", function () {
const rawLines = [
"class Foo {",
" constructor() {",
" console.log(arguments);",
" }",
"};",
].join("\n");
expect(
codeFrameColumns(
rawLines,
{ start: { line: 2 }, end: { line: 4 } },
{
message: "something about the constructor body",
},
),
).toEqual(
[
" something about the constructor body",
" 1 | class Foo {",
"> 2 | constructor() {",
"> 3 | console.log(arguments);",
"> 4 | }",
" 5 | };",
].join("\n"),
);
});
test("highlight works", function () {
const raw = "const a = 1";
const highlighted = highlight(raw);
expect(stripVTControlCharacters(highlighted)).toBe(raw);
expect(highlighted.length).toBeGreaterThan(raw.length);
});
test("opts.startLine", function () {
const rawLines = "const a = 1;\nconst b = 1";
expect(
codeFrameColumns(rawLines, { start: { line: 102 } }, { startLine: 101 }),
).toMatchInlineSnapshot(`
" 101 | const a = 1;
> 102 | const b = 1"
`);
});
test("should handle 0-based columns from Babel AST locations", function () {
const code = 'x = "foobar";';
// "foobar" (with quotes) starts at column 4 (0-based), ends at column 12 (0-based exclusive)
const loc = {
start: { line: 1, column: 4 },
end: { line: 1, column: 12 },
};
const result = codeFrameColumns(code, loc);
// The underline should cover "foobar" (with quotes) starting at the correct position
expect(result).toEqual(
['> 1 | x = "foobar";', " | ^^^^^^^^"].join("\n"),
);
});
test("should handle 0-based column 0", function () {
const code = '"foobar";';
const loc = {
start: { line: 1, column: 0 },
end: { line: 1, column: 8 },
};
const result = codeFrameColumns(code, loc);
expect(result).toEqual(['> 1 | "foobar";', " | ^^^^^^^^"].join("\n"));
});
test("middle lines in a 4+ line range use each line's own length", function () {
const rawLines = [
"class Foo {",
" constructor() {",
" short",
" a much longer middle line here",
" end",
" }",
"};",
].join("\n");
expect(
codeFrameColumns(rawLines, {
start: { line: 2, column: 16 },
end: { line: 6, column: 3 },
}),
).toEqual(
[
" 1 | class Foo {",
"> 2 | constructor() {",
" | ^",
"> 3 | short",
" | ^^^^^^^^^",
"> 4 | a much longer middle line here",
" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
"> 5 | end",
" | ^^^^^^^",
"> 6 | }",
" | ^^^",
" 7 | };",
].join("\n"),
);
});
});