From 08a179a4a4c7b8780f74420db61192afa6230db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 24 May 2025 15:49:23 +0900 Subject: [PATCH 01/55] feat: add support for `getLocFromIndex` and `getIndexFromLoc` --- packages/plugin-kit/src/source-code.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index cdede465..93040766 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -270,6 +270,14 @@ export class TextSourceCodeBase { ); } + getLocFromIndex() { + // TODO + } + + getIndexFromLoc() { + // TODO + } + /** * Returns the range information for the given node or token. * @param {Options['SyntaxElementWithLoc']} nodeOrToken The node or token to get the range information for. From adc573ffed6cc93ef5175eca09ca5626fdd48fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 5 Jun 2025 23:00:18 +0900 Subject: [PATCH 02/55] wip: complete `getLocFromIndex` --- packages/plugin-kit/src/source-code.js | 136 +++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 7 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 2b69ec28..6e5b9f99 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -63,6 +63,32 @@ function hasPosStyleRange(node) { return "position" in node; } +/** + * Performs binary search to find the line number containing a given character index. + * Returns the lower bound - the index of the first element greater than the target. + * **Please note that the `lineStartIndices` should be sorted in ascending order**. + * - Time Complexity: O(log n) - Significantly faster than linear search for large files. + * @param {number[]} lineStartIndices Sorted array of line start indices. + * @param {number} target The character index to find the line number for. + * @returns {number} The 1-based line number for the target index. + */ +function findLineNumberBinarySearch(lineStartIndices, target) { + let low = 0; + let high = lineStartIndices.length; + + while (low < high) { + const mid = ((low + high) / 2) | 0; // Use bitwise OR to floor the division. + + if (target < lineStartIndices[mid]) { + high = mid; + } else { + low = mid + 1; + } + } + + return low; +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- @@ -224,7 +250,25 @@ export class TextSourceCodeBase { * The lines of text in the source code. * @type {Array} */ - #lines; + #lines = []; + + /** + * The indices of the start of each line in the source code. + * @type {Array} + */ + #lineStartIndices = [0]; + + /** + * The line number at which the parser starts counting. + * @type {0|1} + */ + #lineStart; + + /** + * The column number at which the parser starts counting. + * @type {0|1} + */ + #columnStart; /** * The AST of the source code. @@ -243,12 +287,30 @@ export class TextSourceCodeBase { * @param {Object} options The options for the instance. * @param {string} options.text The source code text. * @param {Options['RootNode']} options.ast The root AST node. - * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. + * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. Defaults to `/\r?\n/gu`. + * @param {0|1} [options.lineStart] The line number at which the parser starts counting. Defaults to `1` for ESTree compatibility. + * @param {0|1} [options.columnStart] The column number at which the parser starts counting. Defaults to `0` for ESTree compatibility. */ - constructor({ text, ast, lineEndingPattern = /\r?\n/u }) { + constructor({ + text, + ast, + lineEndingPattern = /\r?\n/gu, + lineStart = 1, + columnStart = 0, + }) { this.ast = ast; this.text = text; - this.#lines = text.split(lineEndingPattern); + this.#lineStart = lineStart; + this.#columnStart = columnStart; + + let match; + while ((match = lineEndingPattern.exec(text))) { + this.#lines.push( + text.slice(this.#lineStartIndices.at(-1), match.index), + ); + this.#lineStartIndices.push(match.index + match[0].length); + } + this.#lines.push(text.slice(this.#lineStartIndices.at(-1))); } /** @@ -271,12 +333,72 @@ export class TextSourceCodeBase { ); } - getLocFromIndex() { - // TODO + /** + * Converts a source text index into a `{ line: number, column: number }` pair. + * @param {number} index The index of a character in a file. + * @throws {TypeError|RangeError} If non-numeric index or index out of range. + * @returns {{line: number, column: number}} A `{ line: number, column: number }` location object with 0 or 1-indexed line and 0 or 1-indexed column based on language. + */ + getLocFromIndex(index) { + if (typeof index !== "number") { + throw new TypeError("Expected `index` to be a number."); + } + + if (index < 0 || index > this.text.length) { + throw new RangeError( + `Index out of range (requested index ${index}, but source text has length ${this.text.length}).`, + ); + } + + /* + * For an argument of `this.text.length`, return the location one "spot" past the last character + * of the file. If the last character is a linebreak, the location will be column 0 of the next + * line; otherwise, the location will be in the next column on the same line. + * + * See `getIndexFromLoc` for the motivation for this special case. + */ + if (index === this.text.length) { + return { + line: this.#lines.length - 1 + this.#lineStart, + // @ts-expect-error `this.#lines` is always non-empty here. See constructor. + column: this.#lines.at(-1).length + this.#columnStart, + }; + } + + /* + * To figure out which line `index` is on, determine the last place at which index could + * be inserted into `lineStartIndices` to keep the list sorted. + */ + const lineNumber = + // @ts-expect-error `this.#lineStartIndices` is always non-empty here. See constructor. + (index >= this.#lineStartIndices.at(-1) + ? this.#lineStartIndices.length + : findLineNumberBinarySearch(this.#lineStartIndices, index)) - + 1 + + this.#lineStart; + + return { + line: lineNumber, + column: + index - + this.#lineStartIndices[lineNumber - 1] + + this.#columnStart, + }; } - getIndexFromLoc() { + /** + * Converts a `{ line: number, column: number }` pair into a source text index. + * @param {Object} loc A line/column location. + * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.) + * @param {number} loc.column The column number of the location. (0 or 1-indexed based on language.) + * @throws {TypeError|RangeError} If `loc` is not an object with a numeric + * `line` and `column`, if the `line` is less than or equal to zero or + * the `line` or `column` is out of the expected range. + * @returns {number} The index of the line/column location in a file. + */ + getIndexFromLoc(loc) { // TODO + return loc.line; } /** From 8def91f5cb76a3eaa2f6c38542a49ba4b55ef0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 5 Jun 2025 23:16:47 +0900 Subject: [PATCH 03/55] wip: resolve ts error --- packages/plugin-kit/src/source-code.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 6e5b9f99..e7260b4e 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -360,7 +360,7 @@ export class TextSourceCodeBase { if (index === this.text.length) { return { line: this.#lines.length - 1 + this.#lineStart, - // @ts-expect-error `this.#lines` is always non-empty here. See constructor. + // @ts-ignore `this.#lines` is always non-empty here. See constructor. (Please avoid using `@ts-expect-error`, as it causes a build error.) column: this.#lines.at(-1).length + this.#columnStart, }; } @@ -370,7 +370,7 @@ export class TextSourceCodeBase { * be inserted into `lineStartIndices` to keep the list sorted. */ const lineNumber = - // @ts-expect-error `this.#lineStartIndices` is always non-empty here. See constructor. + // @ts-ignore `this.#lineStartIndices` is always non-empty here. See constructor. (Please avoid using `@ts-expect-error`, as it causes a build error.) (index >= this.#lineStartIndices.at(-1) ? this.#lineStartIndices.length : findLineNumberBinarySearch(this.#lineStartIndices, index)) - From b9f5a170f6001887dcff98613fba7c9523a11065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 5 Jun 2025 23:25:27 +0900 Subject: [PATCH 04/55] wip: add tests for types --- packages/plugin-kit/tests/types/types.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/plugin-kit/tests/types/types.test.ts b/packages/plugin-kit/tests/types/types.test.ts index 4ca94fd2..ee7f2f63 100644 --- a/packages/plugin-kit/tests/types/types.test.ts +++ b/packages/plugin-kit/tests/types/types.test.ts @@ -87,6 +87,7 @@ sourceCode.text satisfies string; sourceCode.lines satisfies string[]; sourceCode.getAncestors({}) satisfies object[]; sourceCode.getLoc({}) satisfies SourceLocation; +sourceCode.getLocFromIndex(0) satisfies { line: number; column: number }; sourceCode.getParent({}) satisfies object | undefined; sourceCode.getRange({}) satisfies SourceRange; sourceCode.getText() satisfies string; @@ -141,6 +142,10 @@ sourceCodeWithOptions.getAncestors({ value: "" }) satisfies { value: string; }[] satisfies CustomOptions["SyntaxElementWithLoc"][]; sourceCodeWithOptions.getLoc({ value: "" }) satisfies SourceLocation; +sourceCodeWithOptions.getLocFromIndex(0) satisfies { + line: number; + column: number; +}; sourceCodeWithOptions.getParent({ value: "" }) satisfies | { value: string } | undefined satisfies CustomOptions["SyntaxElementWithLoc"] | undefined; @@ -153,6 +158,8 @@ sourceCodeWithOptions.getAncestors({}); // @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getLoc({}); // @ts-expect-error Wrong type should be caught +sourceCodeWithOptions.getLocFromIndex("foo"); +// @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getParent({}); // @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getRange({}); From 9c2c153268da1a987d2a72691bd49c0bdde00955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 7 Jun 2025 16:03:31 +0900 Subject: [PATCH 05/55] wip: complete tests for `getLocFromIndex` --- packages/plugin-kit/src/source-code.js | 2 +- packages/plugin-kit/tests/source-code.test.js | 325 ++++++++++++++++++ 2 files changed, 326 insertions(+), 1 deletion(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index e7260b4e..18cd7b92 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -381,7 +381,7 @@ export class TextSourceCodeBase { line: lineNumber, column: index - - this.#lineStartIndices[lineNumber - 1] + + this.#lineStartIndices[lineNumber - this.#lineStart] + this.#columnStart, }; } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index dcb90842..bc7ccdab 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -155,6 +155,331 @@ describe("source-code", () => { }); }); + describe("getLocFromIndex()", () => { + it("should throw an error for non-numeric index", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getLocFromIndex("5"); + }, + TypeError, + "Expected `index` to be a number.", + ); + + assert.throws( + () => { + sourceCode.getLocFromIndex(null); + }, + TypeError, + "Expected `index` to be a number.", + ); + }); + + it("should throw an error for negative index", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getLocFromIndex(-1); + }, + RangeError, + /Index out of range/u, + ); + }); + + it("should throw an error for index beyond text length", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getLocFromIndex(text.length + 1); + }, + RangeError, + /Index out of range/u, + ); + }); + + it("should handle the special case of `text.length`", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 2, + column: 3, + }, + ); + }); + + it("should handle the special case of `text.length` when lineStart is 0 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 0, + }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 1, + column: 3, + }, + ); + }); + + it("should handle the special case of `text.length` when lineStart is 1 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 1, + }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 2, + column: 4, + }, + ); + }); + + it("should convert index to location", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 1, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 1, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(4), { + line: 2, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(5), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(6), { + line: 2, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(7), { + line: 2, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(8), { + line: 2, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 3, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(10), { + line: 3, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(11), { + line: 3, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(12), { + line: 3, + column: 3, + }); + }); + + it("should convert index to location when lineStart is 0 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 0, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 0, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 0, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 0, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 0, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(4), { + line: 1, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(5), { + line: 1, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(6), { + line: 1, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(7), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(8), { + line: 1, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 2, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(10), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(11), { + line: 2, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(12), { + line: 2, + column: 3, + }); + }); + + it("should convert index to location when lineStart is 1 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 1, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 1, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 1, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(4), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(5), { + line: 2, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(6), { + line: 2, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(7), { + line: 2, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(8), { + line: 2, + column: 5, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 3, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(10), { + line: 3, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(11), { + line: 3, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(12), { + line: 3, + column: 4, + }); + }); + + it("should handle empty text", () => { + const ast = {}; + const text = ""; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 0, + }); + }); + + it("should handle text with only line breaks", () => { + const ast = {}; + const text = "\n\r\n"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 2, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 3, + column: 0, + }); + }); + }); + describe("getRange()", () => { it("should return a range object when a range property is present", () => { const ast = { From 7655663202a723be0b310b53fe9f4062d9df810c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 7 Jun 2025 17:15:35 +0900 Subject: [PATCH 06/55] wip: update `README.md` --- packages/plugin-kit/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/plugin-kit/README.md b/packages/plugin-kit/README.md index 445c5054..315e2b83 100644 --- a/packages/plugin-kit/README.md +++ b/packages/plugin-kit/README.md @@ -207,6 +207,8 @@ The `TextSourceCodeBase` class is intended to be a base class that has several o - `lines` - an array of text lines that is created automatically when the constructor is called. - `getLoc(node)` - gets the location of a node. Works for nodes that have the ESLint-style `loc` property and nodes that have the Unist-style [`position` property](https://github.com/syntax-tree/unist?tab=readme-ov-file#position). If you're using an AST with a different location format, you'll still need to implement this method yourself. +- `getLocFromIndex(index)` - Converts a source text index into a `{ line: number, column: number }` pair. +- `getIndexFromLoc(loc)` - Converts a `{ line: number, column: number }` pair into a source text index. - `getRange(node)` - gets the range of a node within the source text. Works for nodes that have the ESLint-style `range` property and nodes that have the Unist-style [`position` property](https://github.com/syntax-tree/unist?tab=readme-ov-file#position). If you're using an AST with a different range format, you'll still need to implement this method yourself. - `getText(nodeOrToken, charsBefore, charsAfter)` - gets the source text for the given node or token that has range information attached. Optionally, can return additional characters before and after the given node or token. As long as `getRange()` is properly implemented, this method will just work. - `getAncestors(node)` - returns the ancestry of the node. In order for this to work, you must implement the `getParent()` method yourself. From bdb8f114dfc8aa037d239892e1730130b31c3bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 7 Jun 2025 17:17:16 +0900 Subject: [PATCH 07/55] wip: add `@public` --- packages/plugin-kit/src/source-code.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 18cd7b92..ed75c79f 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -338,6 +338,7 @@ export class TextSourceCodeBase { * @param {number} index The index of a character in a file. * @throws {TypeError|RangeError} If non-numeric index or index out of range. * @returns {{line: number, column: number}} A `{ line: number, column: number }` location object with 0 or 1-indexed line and 0 or 1-indexed column based on language. + * @public */ getLocFromIndex(index) { if (typeof index !== "number") { @@ -395,6 +396,7 @@ export class TextSourceCodeBase { * `line` and `column`, if the `line` is less than or equal to zero or * the `line` or `column` is out of the expected range. * @returns {number} The index of the line/column location in a file. + * @public */ getIndexFromLoc(loc) { // TODO From b970ccc53cacae14eb6642a8aa84709682b3bf43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 7 Jun 2025 17:22:56 +0900 Subject: [PATCH 08/55] wip: add more test cases --- packages/plugin-kit/tests/source-code.test.js | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index bc7ccdab..b126ac88 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -168,7 +168,6 @@ describe("source-code", () => { TypeError, "Expected `index` to be a number.", ); - assert.throws( () => { sourceCode.getLocFromIndex(null); @@ -176,6 +175,27 @@ describe("source-code", () => { TypeError, "Expected `index` to be a number.", ); + assert.throws( + () => { + sourceCode.getLocFromIndex(undefined); + }, + TypeError, + "Expected `index` to be a number.", + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(true); + }, + TypeError, + "Expected `index` to be a number.", + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(false); + }, + TypeError, + "Expected `index` to be a number.", + ); }); it("should throw an error for negative index", () => { From 1e319ee0a2722d6c31857d0b3873331131a7df55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 7 Jun 2025 17:27:07 +0900 Subject: [PATCH 09/55] wip: add type tests for `getIndexFromLoc` --- packages/plugin-kit/tests/types/types.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/plugin-kit/tests/types/types.test.ts b/packages/plugin-kit/tests/types/types.test.ts index ee7f2f63..0f2eaa0d 100644 --- a/packages/plugin-kit/tests/types/types.test.ts +++ b/packages/plugin-kit/tests/types/types.test.ts @@ -88,6 +88,7 @@ sourceCode.lines satisfies string[]; sourceCode.getAncestors({}) satisfies object[]; sourceCode.getLoc({}) satisfies SourceLocation; sourceCode.getLocFromIndex(0) satisfies { line: number; column: number }; +sourceCode.getIndexFromLoc({ line: 1, column: 0 }) satisfies number; sourceCode.getParent({}) satisfies object | undefined; sourceCode.getRange({}) satisfies SourceRange; sourceCode.getText() satisfies string; @@ -146,6 +147,7 @@ sourceCodeWithOptions.getLocFromIndex(0) satisfies { line: number; column: number; }; +sourceCodeWithOptions.getIndexFromLoc({ line: 1, column: 0 }) satisfies number; sourceCodeWithOptions.getParent({ value: "" }) satisfies | { value: string } | undefined satisfies CustomOptions["SyntaxElementWithLoc"] | undefined; @@ -160,6 +162,8 @@ sourceCodeWithOptions.getLoc({}); // @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getLocFromIndex("foo"); // @ts-expect-error Wrong type should be caught +sourceCodeWithOptions.getIndexFromLoc({ line: "1", column: 0 }); +// @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getParent({}); // @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getRange({}); From 953a16c248150d2cf46cee4d7c29cccc90e4506b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 7 Jun 2025 18:24:28 +0900 Subject: [PATCH 10/55] wip: update `getIndexFromLoc` --- packages/plugin-kit/src/source-code.js | 33 +++++++++++++++++-- packages/plugin-kit/tests/source-code.test.js | 15 +++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index ed75c79f..710d05df 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -399,8 +399,37 @@ export class TextSourceCodeBase { * @public */ getIndexFromLoc(loc) { - // TODO - return loc.line; + if ( + typeof loc !== "object" || + typeof loc.line !== "number" || + typeof loc.column !== "number" + ) { + throw new TypeError( + "Expected `loc` to be an object with numeric `line` and `column` properties.", + ); + } + + if ( + loc.line < this.#lineStart || + this.#lineStartIndices.length - 1 + this.#lineStart < loc.line + ) { + throw new RangeError( + `Line number out of range (line ${loc.line} requested). Line numbers should be more than or equal to ${this.#lineStart} and less than or equal to ${this.#lineStartIndices.length - 1 + this.#lineStart}.`, + ); + } + + const lineStartIndex = + this.#lineStartIndices[loc.line - this.#lineStart]; + /* + const lineEndIndex = loc.line - this.#lineStart === this.#lineStartIndices.length - 1 + ? this.text.length + : this.#lineStartIndices[loc.line - this.#lineStart + 1]; + */ + const positionIndex = lineStartIndex + loc.column - this.#columnStart; + + // TODO: RangeError + + return positionIndex; } /** diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index b126ac88..77d6e5c7 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -498,6 +498,21 @@ describe("source-code", () => { column: 0, }); }); + + it("should symmetric with getIndexFromLoc()", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + for (let index = 0; index <= text.length; index++) { + assert.strictEqual( + index, + sourceCode.getIndexFromLoc( + sourceCode.getLocFromIndex(index), + ), + ); + } + }); }); describe("getRange()", () => { From e1df65c43a5366ba10cd80f99c2388cdc58be73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sun, 8 Jun 2025 13:56:59 +0900 Subject: [PATCH 11/55] wip: add more test cases for `getLocFromIndex` --- packages/plugin-kit/tests/source-code.test.js | 226 ++++++++++++++++-- 1 file changed, 212 insertions(+), 14 deletions(-) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 77d6e5c7..2f896630 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -226,10 +226,15 @@ describe("source-code", () => { ); }); - it("should handle the special case of `text.length`", () => { + it("should handle the special case of `text.length` when lineStart is 1 and columnStart is 0", () => { const ast = {}; const text = "foo\nbar"; - const sourceCode = new TextSourceCodeBase({ ast, text }); + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 0, + }); assert.deepStrictEqual( sourceCode.getLocFromIndex(text.length), @@ -240,6 +245,25 @@ describe("source-code", () => { ); }); + it("should handle the special case of `text.length` when lineStart is 0 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 1, + }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 1, + column: 4, + }, + ); + }); + it("should handle the special case of `text.length` when lineStart is 0 and columnStart is 0", () => { const ast = {}; const text = "foo\nbar"; @@ -278,10 +302,15 @@ describe("source-code", () => { ); }); - it("should convert index to location", () => { + it("should convert index to location when lineStart is 1 and columnStart is 0", () => { const ast = {}; const text = "foo\nbar\r\nbaz"; - const sourceCode = new TextSourceCodeBase({ ast, text }); + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 0, + }); assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { line: 1, @@ -337,6 +366,70 @@ describe("source-code", () => { }); }); + it("should convert index to location when lineStart is 0 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 1, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 0, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 0, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 0, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 0, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(4), { + line: 1, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(5), { + line: 1, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(6), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(7), { + line: 1, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(8), { + line: 1, + column: 5, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(10), { + line: 2, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(11), { + line: 2, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(12), { + line: 2, + column: 4, + }); + }); + it("should convert index to location when lineStart is 0 and columnStart is 0", () => { const ast = {}; const text = "foo\nbar\r\nbaz"; @@ -466,14 +559,54 @@ describe("source-code", () => { }); it("should handle empty text", () => { - const ast = {}; - const text = ""; - const sourceCode = new TextSourceCodeBase({ ast, text }); - - assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { - line: 1, - column: 0, - }); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: {}, + text: "", + lineStart: 1, + columnStart: 0, + }).getLocFromIndex(0), + { + line: 1, + column: 0, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: {}, + text: "", + lineStart: 0, + columnStart: 1, + }).getLocFromIndex(0), + { + line: 0, + column: 1, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: {}, + text: "", + lineStart: 0, + columnStart: 0, + }).getLocFromIndex(0), + { + line: 0, + column: 0, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: {}, + text: "", + lineStart: 1, + columnStart: 1, + }).getLocFromIndex(0), + { + line: 1, + column: 1, + }, + ); }); it("should handle text with only line breaks", () => { @@ -499,10 +632,75 @@ describe("source-code", () => { }); }); - it("should symmetric with getIndexFromLoc()", () => { + it("should symmetric with getIndexFromLoc() when lineStart is 1 and columnStart is 0", () => { const ast = {}; const text = "foo\nbar\r\nbaz"; - const sourceCode = new TextSourceCodeBase({ ast, text }); + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 0, + }); + + for (let index = 0; index <= text.length; index++) { + assert.strictEqual( + index, + sourceCode.getIndexFromLoc( + sourceCode.getLocFromIndex(index), + ), + ); + } + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 0 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 1, + }); + + for (let index = 0; index <= text.length; index++) { + assert.strictEqual( + index, + sourceCode.getIndexFromLoc( + sourceCode.getLocFromIndex(index), + ), + ); + } + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 0 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 0, + }); + + for (let index = 0; index <= text.length; index++) { + assert.strictEqual( + index, + sourceCode.getIndexFromLoc( + sourceCode.getLocFromIndex(index), + ), + ); + } + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 1 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 1, + }); for (let index = 0; index <= text.length; index++) { assert.strictEqual( From c31843fb664d2eee5a02c739ddd7fe914b9e0acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sun, 8 Jun 2025 14:12:47 +0900 Subject: [PATCH 12/55] wip: complete `getIndexFromLoc` --- packages/plugin-kit/src/source-code.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 710d05df..4abe5567 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -420,14 +420,22 @@ export class TextSourceCodeBase { const lineStartIndex = this.#lineStartIndices[loc.line - this.#lineStart]; - /* - const lineEndIndex = loc.line - this.#lineStart === this.#lineStartIndices.length - 1 - ? this.text.length - : this.#lineStartIndices[loc.line - this.#lineStart + 1]; - */ + const lineEndIndex = + loc.line - this.#lineStart === this.#lineStartIndices.length - 1 + ? this.text.length + : this.#lineStartIndices[loc.line - this.#lineStart + 1]; const positionIndex = lineStartIndex + loc.column - this.#columnStart; - // TODO: RangeError + if ( + (loc.line - this.#lineStart === this.#lineStartIndices.length - 1 && + positionIndex > lineEndIndex) || + (loc.line - this.#lineStart < this.#lineStartIndices.length - 1 && + positionIndex >= lineEndIndex) + ) { + throw new RangeError( + `Column number out of range (column ${loc.column} requested, but the length of line ${loc.line} is ${lineEndIndex - lineStartIndex}).`, + ); + } return positionIndex; } From a831324ea927230c22129add3e1e4edc664afc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sun, 8 Jun 2025 14:53:38 +0900 Subject: [PATCH 13/55] wip: add more test cases --- packages/plugin-kit/src/source-code.js | 3 +- packages/plugin-kit/tests/source-code.test.js | 310 ++++++++++++++++++ 2 files changed, 312 insertions(+), 1 deletion(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 4abe5567..d070b6cc 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -427,13 +427,14 @@ export class TextSourceCodeBase { const positionIndex = lineStartIndex + loc.column - this.#columnStart; if ( + loc.column < this.#columnStart || (loc.line - this.#lineStart === this.#lineStartIndices.length - 1 && positionIndex > lineEndIndex) || (loc.line - this.#lineStart < this.#lineStartIndices.length - 1 && positionIndex >= lineEndIndex) ) { throw new RangeError( - `Column number out of range (column ${loc.column} requested, but the length of line ${loc.line} is ${lineEndIndex - lineStartIndex}).`, + `Column number out of range (column ${loc.column} requested). Column number for line ${loc.line} should be more than or equal to ${this.#columnStart} and less than or equal to ${lineEndIndex - lineStartIndex}.`, ); } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 2f896630..79aca35a 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -713,6 +713,316 @@ describe("source-code", () => { }); }); + describe("getIndexFromLoc()", () => { + it("should throw an error for non-object loc", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc("invalid"); + }, + TypeError, + "Expected `loc` to be an object with numeric `line` and `column` properties.", + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc(null); + }, + TypeError, + "Expected `loc` to be an object with numeric `line` and `column` properties.", + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc(undefined); + }, + TypeError, + "Expected `loc` to be an object with numeric `line` and `column` properties.", + ); + }); + + it("should throw an error for missing or non-numeric line/column properties", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({}); + }, + TypeError, + "Expected `loc` to be an object with numeric `line` and `column` properties.", + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: "1", column: 0 }); + }, + TypeError, + "Expected `loc` to be an object with numeric `line` and `column` properties.", + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: "0" }); + }, + TypeError, + "Expected `loc` to be an object with numeric `line` and `column` properties.", + ); + }); + + it("should throw an error for line number out of range when lineStart is 1 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 0, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 0 }); + }, + RangeError, + /Line number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 3, column: 0 }); + }, + RangeError, + /Line number out of range/u, + ); + }); + + it("should throw an error for line number out of range when lineStart is 0 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 1, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: -1, column: 1 }); + }, + RangeError, + /Line number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 1 }); + }, + RangeError, + /Line number out of range/u, + ); + }); + + it("should throw an error for line number out of range when lineStart is 0 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 0, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: -1, column: 0 }); + }, + RangeError, + /Line number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 0 }); + }, + RangeError, + /Line number out of range/u, + ); + }); + + it("should throw an error for line number out of range when lineStart is 1 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 1, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 1 }); + }, + RangeError, + /Line number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 3, column: 1 }); + }, + RangeError, + /Line number out of range/u, + ); + }); + + it("should throw an error for column number out of range when lineStart is 1 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 0, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: -1 }); + }, + RangeError, + /Column number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 4 }); + }, + RangeError, + /Column number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 4 }); + }, + RangeError, + /Column number out of range/u, + ); + }); + + it("should throw an error for column number out of range when lineStart is 0 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 1, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 0 }); + }, + RangeError, + /Column number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 5 }); + }, + RangeError, + /Column number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 5 }); + }, + RangeError, + /Column number out of range/u, + ); + }); + + it("should throw an error for column number out of range when lineStart is 0 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 0, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: -1 }); + }, + RangeError, + /Column number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 4 }); + }, + RangeError, + /Column number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 4 }); + }, + RangeError, + /Column number out of range/u, + ); + }); + + it("should throw an error for column number out of range when lineStart is 1 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 1, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 0 }); + }, + RangeError, + /Column number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 5 }); + }, + RangeError, + /Column number out of range/u, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 5 }); + }, + RangeError, + /Column number out of range/u, + ); + }); + }); + describe("getRange()", () => { it("should return a range object when a range property is present", () => { const ast = { From eaa212d0a486c4f13ed8b254262a77535e550dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sun, 8 Jun 2025 17:09:57 +0900 Subject: [PATCH 14/55] wip: add more tests --- packages/plugin-kit/tests/source-code.test.js | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 79aca35a..1d863c96 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -1021,6 +1021,324 @@ describe("source-code", () => { /Column number out of range/u, ); }); + + it("should convert loc to index when lineStart is 1 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 0, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 0 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 2 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 0 }), + 4, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 5, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 2 }), + 6, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 3 }), + 7, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 4 }), + 8, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 0 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 1 }), + 10, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 2 }), + 11, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 3 }), + 12, + ); + }); + + it("should convert loc to index when lineStart is 0 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 1, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 1 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 2 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 3 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 4 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 4, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 2 }), + 5, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 6, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 4 }), + 7, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 5 }), + 8, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 2 }), + 10, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 3 }), + 11, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 4 }), + 12, + ); + }); + + it("should convert loc to index when lineStart is 0 and columnStart is 0", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 0, + columnStart: 0, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 0 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 1 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 2 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 3 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 0 }), + 4, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 5, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 2 }), + 6, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 7, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 4 }), + 8, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 0 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 10, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 2 }), + 11, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 3 }), + 12, + ); + }); + + it("should convert loc to index when lineStart is 1 and columnStart is 1", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineStart: 1, + columnStart: 1, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 2 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 4 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 4, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 2 }), + 5, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 3 }), + 6, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 4 }), + 7, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 5 }), + 8, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 1 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 2 }), + 10, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 3 }), + 11, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 4 }), + 12, + ); + }); + + it("should handle empty text", () => { + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: {}, + text: "", + lineStart: 1, + columnStart: 0, + }).getIndexFromLoc({ line: 1, column: 0 }), + 0, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: {}, + text: "", + lineStart: 0, + columnStart: 1, + }).getIndexFromLoc({ line: 0, column: 1 }), + 0, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: {}, + text: "", + lineStart: 0, + columnStart: 0, + }).getIndexFromLoc({ line: 0, column: 0 }), + 0, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: {}, + text: "", + lineStart: 1, + columnStart: 1, + }).getIndexFromLoc({ line: 1, column: 1 }), + 0, + ); + }); + + it("should handle text with only line breaks", () => { + const ast = {}; + const text = "\n\r\n"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 0 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 0 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 0 }), + 3, + ); + }); }); describe("getRange()", () => { From 478b8d58510da4c9974dee50e4886e61b305ee9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Mon, 16 Jun 2025 19:03:23 +0900 Subject: [PATCH 15/55] wip: update error message --- packages/plugin-kit/src/source-code.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index d070b6cc..94953c2a 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -414,7 +414,7 @@ export class TextSourceCodeBase { this.#lineStartIndices.length - 1 + this.#lineStart < loc.line ) { throw new RangeError( - `Line number out of range (line ${loc.line} requested). Line numbers should be more than or equal to ${this.#lineStart} and less than or equal to ${this.#lineStartIndices.length - 1 + this.#lineStart}.`, + `Line number out of range (line ${loc.line} requested). Valid range: ${this.#lineStart}-${this.#lineStartIndices.length - 1 + this.#lineStart}`, ); } @@ -434,7 +434,7 @@ export class TextSourceCodeBase { positionIndex >= lineEndIndex) ) { throw new RangeError( - `Column number out of range (column ${loc.column} requested). Column number for line ${loc.line} should be more than or equal to ${this.#columnStart} and less than or equal to ${lineEndIndex - lineStartIndex}.`, + `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${this.#columnStart}-${lineEndIndex - lineStartIndex}`, ); } From 498402f1fa5727919643ea9c5d7784bd5dba050a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Mon, 16 Jun 2025 19:37:06 +0900 Subject: [PATCH 16/55] wip: more detailed test cases --- packages/plugin-kit/src/source-code.js | 1 + packages/plugin-kit/tests/source-code.test.js | 142 ++++++++++++------ 2 files changed, 101 insertions(+), 42 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 94953c2a..41f81c83 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -400,6 +400,7 @@ export class TextSourceCodeBase { */ getIndexFromLoc(loc) { if ( + loc === null || typeof loc !== "object" || typeof loc.line !== "number" || typeof loc.column !== "number" diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 1d863c96..bf46d290 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -165,36 +165,46 @@ describe("source-code", () => { () => { sourceCode.getLocFromIndex("5"); }, - TypeError, - "Expected `index` to be a number.", + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, ); assert.throws( () => { sourceCode.getLocFromIndex(null); }, - TypeError, - "Expected `index` to be a number.", + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, ); assert.throws( () => { sourceCode.getLocFromIndex(undefined); }, - TypeError, - "Expected `index` to be a number.", + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, ); assert.throws( () => { sourceCode.getLocFromIndex(true); }, - TypeError, - "Expected `index` to be a number.", + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, ); assert.throws( () => { sourceCode.getLocFromIndex(false); }, - TypeError, - "Expected `index` to be a number.", + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, ); }); @@ -207,8 +217,11 @@ describe("source-code", () => { () => { sourceCode.getLocFromIndex(-1); }, - RangeError, - /Index out of range/u, + { + name: "RangeError", + message: + "Index out of range (requested index -1, but source text has length 7).", + }, ); }); @@ -221,8 +234,11 @@ describe("source-code", () => { () => { sourceCode.getLocFromIndex(text.length + 1); }, - RangeError, - /Index out of range/u, + { + name: "RangeError", + message: + "Index out of range (requested index 8, but source text has length 7).", + }, ); }); @@ -723,24 +739,33 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc("invalid"); }, - TypeError, - "Expected `loc` to be an object with numeric `line` and `column` properties.", + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc(null); }, - TypeError, - "Expected `loc` to be an object with numeric `line` and `column` properties.", + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc(undefined); }, - TypeError, - "Expected `loc` to be an object with numeric `line` and `column` properties.", + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, ); }); @@ -753,24 +778,33 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({}); }, - TypeError, - "Expected `loc` to be an object with numeric `line` and `column` properties.", + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: "1", column: 0 }); }, - TypeError, - "Expected `loc` to be an object with numeric `line` and `column` properties.", + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 1, column: "0" }); }, - TypeError, - "Expected `loc` to be an object with numeric `line` and `column` properties.", + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, ); }); @@ -788,16 +822,22 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({ line: 0, column: 0 }); }, - RangeError, - /Line number out of range/u, + { + name: "RangeError", + message: + "Line number out of range (line 0 requested). Valid range: 1-2", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 3, column: 0 }); }, - RangeError, - /Line number out of range/u, + { + name: "RangeError", + message: + "Line number out of range (line 3 requested). Valid range: 1-2", + }, ); }); @@ -815,16 +855,22 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({ line: -1, column: 1 }); }, - RangeError, - /Line number out of range/u, + { + name: "RangeError", + message: + "Line number out of range (line -1 requested). Valid range: 0-1", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 2, column: 1 }); }, - RangeError, - /Line number out of range/u, + { + name: "RangeError", + message: + "Line number out of range (line 2 requested). Valid range: 0-1", + }, ); }); @@ -842,16 +888,22 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({ line: -1, column: 0 }); }, - RangeError, - /Line number out of range/u, + { + name: "RangeError", + message: + "Line number out of range (line -1 requested). Valid range: 0-1", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 2, column: 0 }); }, - RangeError, - /Line number out of range/u, + { + name: "RangeError", + message: + "Line number out of range (line 2 requested). Valid range: 0-1", + }, ); }); @@ -869,16 +921,22 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({ line: 0, column: 1 }); }, - RangeError, - /Line number out of range/u, + { + name: "RangeError", + message: + "Line number out of range (line 0 requested). Valid range: 1-2", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 3, column: 1 }); }, - RangeError, - /Line number out of range/u, + { + name: "RangeError", + message: + "Line number out of range (line 3 requested). Valid range: 1-2", + }, ); }); From fbd565e41bfd6301037a9609f133ce077b520935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Mon, 16 Jun 2025 21:39:42 +0900 Subject: [PATCH 17/55] fix: improve error messages for column range validation --- packages/plugin-kit/src/source-code.js | 2 +- packages/plugin-kit/tests/source-code.test.js | 84 +++++++++++++------ 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 41f81c83..bf0b77af 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -435,7 +435,7 @@ export class TextSourceCodeBase { positionIndex >= lineEndIndex) ) { throw new RangeError( - `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${this.#columnStart}-${lineEndIndex - lineStartIndex}`, + `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${this.#columnStart}-${lineEndIndex - lineStartIndex - 1 + this.#columnStart}`, ); } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index bf46d290..cbcb9b34 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -954,24 +954,33 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({ line: 1, column: -1 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column -1 requested). Valid range for line 1: 0-3", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 1, column: 4 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 1: 0-3", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 2, column: 4 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 2: 0-2", + }, ); }); @@ -989,24 +998,33 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({ line: 0, column: 0 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 0 requested). Valid range for line 0: 1-4", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 0, column: 5 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 0: 1-4", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 1, column: 5 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 1: 1-3", + }, ); }); @@ -1024,24 +1042,33 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({ line: 0, column: -1 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column -1 requested). Valid range for line 0: 0-3", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 0, column: 4 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 0: 0-3", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 1, column: 4 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 1: 0-2", + }, ); }); @@ -1059,24 +1086,33 @@ describe("source-code", () => { () => { sourceCode.getIndexFromLoc({ line: 1, column: 0 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 0 requested). Valid range for line 1: 1-4", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 1, column: 5 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 1: 1-4", + }, ); assert.throws( () => { sourceCode.getIndexFromLoc({ line: 2, column: 5 }); }, - RangeError, - /Column number out of range/u, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 2: 1-3", + }, ); }); From e4b789bee186a0d8d9a9a353eb3a79ff7e2ed535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Mon, 16 Jun 2025 21:48:26 +0900 Subject: [PATCH 18/55] wip: remove `@ts-ignore` --- packages/plugin-kit/src/source-code.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index bf0b77af..a8eaf60f 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -361,8 +361,7 @@ export class TextSourceCodeBase { if (index === this.text.length) { return { line: this.#lines.length - 1 + this.#lineStart, - // @ts-ignore `this.#lines` is always non-empty here. See constructor. (Please avoid using `@ts-expect-error`, as it causes a build error.) - column: this.#lines.at(-1).length + this.#columnStart, + column: (this.#lines.at(-1)?.length ?? 0) + this.#columnStart, }; } @@ -371,8 +370,7 @@ export class TextSourceCodeBase { * be inserted into `lineStartIndices` to keep the list sorted. */ const lineNumber = - // @ts-ignore `this.#lineStartIndices` is always non-empty here. See constructor. (Please avoid using `@ts-expect-error`, as it causes a build error.) - (index >= this.#lineStartIndices.at(-1) + (index >= (this.#lineStartIndices.at(-1) ?? 0) ? this.#lineStartIndices.length : findLineNumberBinarySearch(this.#lineStartIndices, index)) - 1 + From 4844c96e7ea7b4ec0ee42f6728c3c9b39bbd13ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 19 Jun 2025 23:34:21 +0900 Subject: [PATCH 19/55] wip: retrieve `lineStart` and `columnStart` from AST --- packages/plugin-kit/src/source-code.js | 48 ++- packages/plugin-kit/tests/source-code.test.js | 352 +++++++++++++----- 2 files changed, 287 insertions(+), 113 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index a8eaf60f..0093cb71 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -242,7 +242,7 @@ export class Directive { /** * Source Code Base Object - * @template {SourceCodeBaseTypeOptions & {SyntaxElementWithLoc: object}} [Options=SourceCodeBaseTypeOptions & {SyntaxElementWithLoc: object}] + * @template {SourceCodeBaseTypeOptions & {RootNode: object, SyntaxElementWithLoc: object}} [Options=SourceCodeBaseTypeOptions & {RootNode: object, SyntaxElementWithLoc: object}] * @implements {TextSourceCode} */ export class TextSourceCodeBase { @@ -259,16 +259,16 @@ export class TextSourceCodeBase { #lineStartIndices = [0]; /** - * The line number at which the parser starts counting. + * The line number at which the parser starts counting. Defaults to `1` for ESTree compatibility. * @type {0|1} */ - #lineStart; + #lineStart = 1; /** - * The column number at which the parser starts counting. + * The column number at which the parser starts counting. Defaults to `0` for ESTree compatibility. * @type {0|1} */ - #columnStart; + #columnStart = 0; /** * The AST of the source code. @@ -288,20 +288,34 @@ export class TextSourceCodeBase { * @param {string} options.text The source code text. * @param {Options['RootNode']} options.ast The root AST node. * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. Defaults to `/\r?\n/gu`. - * @param {0|1} [options.lineStart] The line number at which the parser starts counting. Defaults to `1` for ESTree compatibility. - * @param {0|1} [options.columnStart] The column number at which the parser starts counting. Defaults to `0` for ESTree compatibility. - */ - constructor({ - text, - ast, - lineEndingPattern = /\r?\n/gu, - lineStart = 1, - columnStart = 0, - }) { + */ + constructor({ text, ast, lineEndingPattern = /\r?\n/gu }) { this.ast = ast; this.text = text; - this.#lineStart = lineStart; - this.#columnStart = columnStart; + + if (hasESTreeStyleLoc(ast)) { + if (ast.loc?.start?.line === 0 || ast.loc?.start?.line === 1) { + this.#lineStart = ast.loc.start.line; + } + + if (ast.loc?.start?.column === 0 || ast.loc?.start?.column === 1) { + this.#columnStart = ast.loc.start.column; + } + } else if (hasPosStyleLoc(ast)) { + if ( + ast.position?.start?.line === 0 || + ast.position?.start?.line === 1 + ) { + this.#lineStart = ast.position.start.line; + } + + if ( + ast.position?.start?.column === 0 || + ast.position?.start?.column === 1 + ) { + this.#columnStart = ast.position.start.column; + } + } let match; while ((match = lineEndingPattern.exec(text))) { diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index cbcb9b34..b361bad1 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -243,13 +243,18 @@ describe("source-code", () => { }); it("should handle the special case of `text.length` when lineStart is 1 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 0, }); assert.deepStrictEqual( @@ -262,13 +267,18 @@ describe("source-code", () => { }); it("should handle the special case of `text.length` when lineStart is 0 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 1, }); assert.deepStrictEqual( @@ -281,13 +291,18 @@ describe("source-code", () => { }); it("should handle the special case of `text.length` when lineStart is 0 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 0, }); assert.deepStrictEqual( @@ -300,13 +315,18 @@ describe("source-code", () => { }); it("should handle the special case of `text.length` when lineStart is 1 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 1, }); assert.deepStrictEqual( @@ -319,13 +339,18 @@ describe("source-code", () => { }); it("should convert index to location when lineStart is 1 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 0, }); assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { @@ -383,13 +408,18 @@ describe("source-code", () => { }); it("should convert index to location when lineStart is 0 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 1, }); assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { @@ -447,13 +477,18 @@ describe("source-code", () => { }); it("should convert index to location when lineStart is 0 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 0, }); assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { @@ -511,13 +546,18 @@ describe("source-code", () => { }); it("should convert index to location when lineStart is 1 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 1, }); assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { @@ -577,10 +617,15 @@ describe("source-code", () => { it("should handle empty text", () => { assert.deepStrictEqual( new TextSourceCodeBase({ - ast: {}, + ast: { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }, text: "", - lineStart: 1, - columnStart: 0, }).getLocFromIndex(0), { line: 1, @@ -589,10 +634,15 @@ describe("source-code", () => { ); assert.deepStrictEqual( new TextSourceCodeBase({ - ast: {}, + ast: { + loc: { + start: { + line: 0, + column: 1, + }, + }, + }, text: "", - lineStart: 0, - columnStart: 1, }).getLocFromIndex(0), { line: 0, @@ -601,10 +651,15 @@ describe("source-code", () => { ); assert.deepStrictEqual( new TextSourceCodeBase({ - ast: {}, + ast: { + loc: { + start: { + line: 0, + column: 0, + }, + }, + }, text: "", - lineStart: 0, - columnStart: 0, }).getLocFromIndex(0), { line: 0, @@ -613,10 +668,15 @@ describe("source-code", () => { ); assert.deepStrictEqual( new TextSourceCodeBase({ - ast: {}, + ast: { + loc: { + start: { + line: 1, + column: 1, + }, + }, + }, text: "", - lineStart: 1, - columnStart: 1, }).getLocFromIndex(0), { line: 1, @@ -649,13 +709,18 @@ describe("source-code", () => { }); it("should symmetric with getIndexFromLoc() when lineStart is 1 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 0, }); for (let index = 0; index <= text.length; index++) { @@ -669,13 +734,18 @@ describe("source-code", () => { }); it("should symmetric with getIndexFromLoc() when lineStart is 0 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 1, }); for (let index = 0; index <= text.length; index++) { @@ -689,13 +759,18 @@ describe("source-code", () => { }); it("should symmetric with getIndexFromLoc() when lineStart is 0 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 0, }); for (let index = 0; index <= text.length; index++) { @@ -709,13 +784,18 @@ describe("source-code", () => { }); it("should symmetric with getIndexFromLoc() when lineStart is 1 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 1, }); for (let index = 0; index <= text.length; index++) { @@ -809,13 +889,18 @@ describe("source-code", () => { }); it("should throw an error for line number out of range when lineStart is 1 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 0, }); assert.throws( @@ -842,13 +927,18 @@ describe("source-code", () => { }); it("should throw an error for line number out of range when lineStart is 0 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 1, }); assert.throws( @@ -875,13 +965,18 @@ describe("source-code", () => { }); it("should throw an error for line number out of range when lineStart is 0 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 0, }); assert.throws( @@ -908,13 +1003,18 @@ describe("source-code", () => { }); it("should throw an error for line number out of range when lineStart is 1 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 1, }); assert.throws( @@ -941,13 +1041,18 @@ describe("source-code", () => { }); it("should throw an error for column number out of range when lineStart is 1 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 0, }); assert.throws( @@ -985,13 +1090,18 @@ describe("source-code", () => { }); it("should throw an error for column number out of range when lineStart is 0 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 1, }); assert.throws( @@ -1029,13 +1139,18 @@ describe("source-code", () => { }); it("should throw an error for column number out of range when lineStart is 0 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 0, }); assert.throws( @@ -1073,13 +1188,18 @@ describe("source-code", () => { }); it("should throw an error for column number out of range when lineStart is 1 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + }, + }; const text = "foo\nbar"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 1, }); assert.throws( @@ -1117,13 +1237,18 @@ describe("source-code", () => { }); it("should convert loc to index when lineStart is 1 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 0, }); assert.strictEqual( @@ -1181,13 +1306,18 @@ describe("source-code", () => { }); it("should convert loc to index when lineStart is 0 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 1, }); assert.strictEqual( @@ -1245,13 +1375,18 @@ describe("source-code", () => { }); it("should convert loc to index when lineStart is 0 and columnStart is 0", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 0, - columnStart: 0, }); assert.strictEqual( @@ -1309,13 +1444,18 @@ describe("source-code", () => { }); it("should convert loc to index when lineStart is 1 and columnStart is 1", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + }, + }; const text = "foo\nbar\r\nbaz"; const sourceCode = new TextSourceCodeBase({ ast, text, - lineStart: 1, - columnStart: 1, }); assert.strictEqual( @@ -1375,37 +1515,57 @@ describe("source-code", () => { it("should handle empty text", () => { assert.deepStrictEqual( new TextSourceCodeBase({ - ast: {}, + ast: { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }, text: "", - lineStart: 1, - columnStart: 0, }).getIndexFromLoc({ line: 1, column: 0 }), 0, ); assert.deepStrictEqual( new TextSourceCodeBase({ - ast: {}, + ast: { + loc: { + start: { + line: 0, + column: 1, + }, + }, + }, text: "", - lineStart: 0, - columnStart: 1, }).getIndexFromLoc({ line: 0, column: 1 }), 0, ); assert.deepStrictEqual( new TextSourceCodeBase({ - ast: {}, + ast: { + loc: { + start: { + line: 0, + column: 0, + }, + }, + }, text: "", - lineStart: 0, - columnStart: 0, }).getIndexFromLoc({ line: 0, column: 0 }), 0, ); assert.deepStrictEqual( new TextSourceCodeBase({ - ast: {}, + ast: { + loc: { + start: { + line: 1, + column: 1, + }, + }, + }, text: "", - lineStart: 1, - columnStart: 1, }).getIndexFromLoc({ line: 1, column: 1 }), 0, ); From bd212837df821116d998697c0eace74d9c156b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Wed, 2 Jul 2025 23:19:55 +0900 Subject: [PATCH 20/55] wip: lazy caculation --- packages/plugin-kit/src/source-code.js | 79 +++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 0093cb71..35886403 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -270,6 +270,12 @@ export class TextSourceCodeBase { */ #columnStart = 0; + /** + * The pattern to match line endings in the source code. + * @type {RegExp} + */ + #lineEndingPattern; + /** * The AST of the source code. * @type {Options['RootNode']} @@ -292,6 +298,8 @@ export class TextSourceCodeBase { constructor({ text, ast, lineEndingPattern = /\r?\n/gu }) { this.ast = ast; this.text = text; + this.#lineEndingPattern = lineEndingPattern; + this.#lines = text.split(this.#lineEndingPattern); if (hasESTreeStyleLoc(ast)) { if (ast.loc?.start?.line === 0 || ast.loc?.start?.line === 1) { @@ -316,15 +324,68 @@ export class TextSourceCodeBase { this.#columnStart = ast.position.start.column; } } + } + + /** + * Ensures `#lineStartIndices` information is calculated up to the specified index. + * @param {number} index The index of a character in a file. + * @returns {void} + */ + #ensureLineStartIndicesFromIndex(index) { + const lastIndex = this.#lineStartIndices.at(-1) ?? 0; + + // If we've already parsed up to or beyond this index, do nothing + if (index <= lastIndex) { + return; + } + + // Create a new RegExp instance to avoid lastIndex issues + const lineEndingPattern = structuredClone(this.#lineEndingPattern); + + // Start parsing from where we left off + const text = this.text.slice(lastIndex, index + 1); let match; while ((match = lineEndingPattern.exec(text))) { - this.#lines.push( - text.slice(this.#lineStartIndices.at(-1), match.index), + this.#lineStartIndices.push( + lastIndex + match.index + match[0].length, + ); + } + } + + /** + * Ensures `#lineStartIndices` information is calculated up to the specified loc. + * @param {Object} loc A line/column location. + * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.) + * @param {number} loc.column The column number of the location. (0 or 1-indexed based on language.) + * @returns {void} + */ + #ensureLineStartIndicesFromLoc(loc) { + // Calculate line indices up to `locLineIndex + 1` (current line + potentially next line) + const nextLocLineIndex = loc.line - this.#lineStart + 1; + + if (nextLocLineIndex <= this.#lineStartIndices.length - 1) { + return; + } + + const lastIndex = this.#lineStartIndices.at(-1) ?? 0; + const lineEndingPattern = structuredClone(this.#lineEndingPattern); + const text = this.text.slice(lastIndex); + + let match; + let linesFound = 0; + const additionalLinesNeeded = + nextLocLineIndex - (this.#lineStartIndices.length - 1); + + while ( + linesFound < additionalLinesNeeded && + (match = lineEndingPattern.exec(text)) + ) { + this.#lineStartIndices.push( + lastIndex + match.index + match[0].length, ); - this.#lineStartIndices.push(match.index + match[0].length); + linesFound++; } - this.#lines.push(text.slice(this.#lineStartIndices.at(-1))); } /** @@ -365,6 +426,8 @@ export class TextSourceCodeBase { ); } + this.#ensureLineStartIndicesFromIndex(index); + /* * For an argument of `this.text.length`, return the location one "spot" past the last character * of the file. If the last character is a linebreak, the location will be column 0 of the next @@ -424,17 +487,19 @@ export class TextSourceCodeBase { if ( loc.line < this.#lineStart || - this.#lineStartIndices.length - 1 + this.#lineStart < loc.line + this.#lines.length - 1 + this.#lineStart < loc.line ) { throw new RangeError( - `Line number out of range (line ${loc.line} requested). Valid range: ${this.#lineStart}-${this.#lineStartIndices.length - 1 + this.#lineStart}`, + `Line number out of range (line ${loc.line} requested). Valid range: ${this.#lineStart}-${this.#lines.length - 1 + this.#lineStart}`, ); } + this.#ensureLineStartIndicesFromLoc(loc); + const lineStartIndex = this.#lineStartIndices[loc.line - this.#lineStart]; const lineEndIndex = - loc.line - this.#lineStart === this.#lineStartIndices.length - 1 + loc.line - this.#lineStart === this.#lines.length - 1 ? this.text.length : this.#lineStartIndices[loc.line - this.#lineStart + 1]; const positionIndex = lineStartIndex + loc.column - this.#columnStart; From b7a4abfdddf6380050f19fe257be106c14807ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 3 Jul 2025 20:59:15 +0900 Subject: [PATCH 21/55] wip: refactor `findLineNumberBinarySearch` --- packages/plugin-kit/src/source-code.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 35886403..8ec1f294 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -69,17 +69,17 @@ function hasPosStyleRange(node) { * **Please note that the `lineStartIndices` should be sorted in ascending order**. * - Time Complexity: O(log n) - Significantly faster than linear search for large files. * @param {number[]} lineStartIndices Sorted array of line start indices. - * @param {number} target The character index to find the line number for. - * @returns {number} The 1-based line number for the target index. + * @param {number} targetIndex The target index to find the line number for. + * @returns {number} The line number for the target index. */ -function findLineNumberBinarySearch(lineStartIndices, target) { +function findLineNumberBinarySearch(lineStartIndices, targetIndex) { let low = 0; - let high = lineStartIndices.length; + let high = lineStartIndices.length - 1; while (low < high) { const mid = ((low + high) / 2) | 0; // Use bitwise OR to floor the division. - if (target < lineStartIndices[mid]) { + if (targetIndex < lineStartIndices[mid]) { high = mid; } else { low = mid + 1; From 3ef01a95d5a9c22eff46f61932509c43e38045c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 3 Jul 2025 21:03:29 +0900 Subject: [PATCH 22/55] wip: refactor `#lines` --- packages/plugin-kit/src/source-code.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 8ec1f294..eda0898c 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -250,7 +250,7 @@ export class TextSourceCodeBase { * The lines of text in the source code. * @type {Array} */ - #lines = []; + #lines; /** * The indices of the start of each line in the source code. From 2098f9156800b85a3d33b4fa7869ff82cff124b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 3 Jul 2025 21:21:47 +0900 Subject: [PATCH 23/55] wip: refactor `#setLineColumnStart` --- packages/plugin-kit/src/source-code.js | 41 ++++++++++++-------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index eda0898c..f8cbd79c 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -301,28 +301,25 @@ export class TextSourceCodeBase { this.#lineEndingPattern = lineEndingPattern; this.#lines = text.split(this.#lineEndingPattern); - if (hasESTreeStyleLoc(ast)) { - if (ast.loc?.start?.line === 0 || ast.loc?.start?.line === 1) { - this.#lineStart = ast.loc.start.line; - } - - if (ast.loc?.start?.column === 0 || ast.loc?.start?.column === 1) { - this.#columnStart = ast.loc.start.column; - } - } else if (hasPosStyleLoc(ast)) { - if ( - ast.position?.start?.line === 0 || - ast.position?.start?.line === 1 - ) { - this.#lineStart = ast.position.start.line; - } - - if ( - ast.position?.start?.column === 0 || - ast.position?.start?.column === 1 - ) { - this.#columnStart = ast.position.start.column; - } + if (hasESTreeStyleLoc(this.ast)) { + this.#setLineColumnStart(this.ast.loc); + } else if (hasPosStyleLoc(this.ast)) { + this.#setLineColumnStart(this.ast.position); + } + } + + /** + * Sets the `#lineStart` and `#columnStart` based on a `loc` or `position` object. + * @param {SourceLocation} loc The `loc` or `position` object to use. + * @returns {void} + */ + #setLineColumnStart(loc) { + if (loc?.start?.line === 0 || loc?.start?.line === 1) { + this.#lineStart = loc.start.line; + } + + if (loc?.start?.column === 0 || loc?.start?.column === 1) { + this.#columnStart = loc.start.column; } } From c4e80ddf527b8eb4554d518c8637627f04cee205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 3 Jul 2025 22:57:39 +0900 Subject: [PATCH 24/55] wip: freeze `#lines` and add test cases --- packages/plugin-kit/src/source-code.js | 4 ++ packages/plugin-kit/tests/source-code.test.js | 70 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index f8cbd79c..33e5f9a5 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -306,6 +306,10 @@ export class TextSourceCodeBase { } else if (hasPosStyleLoc(this.ast)) { this.#setLineColumnStart(this.ast.position); } + + // don't allow further modification of this object + Object.freeze(this); + Object.freeze(this.lines); } /** diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index b361bad1..0cc00abf 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -1689,5 +1689,75 @@ describe("source-code", () => { ]); }); }); + + describe("lines", () => { + it("should return an array of lines", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.lines, ["foo", "bar", "baz"]); + }); + + it("should return an array of lines when line ending pattern is specified", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineEndingPattern: /\n/u, + }); + + assert.deepStrictEqual(sourceCode.lines, [ + "foo", + "bar\r", + "baz", + ]); + }); + + it("should return an array of lines when no line endings are present", () => { + const ast = {}; + const text = "foo"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.lines, ["foo"]); + }); + + it("should return an empty array when text is empty", () => { + const ast = {}; + const text = ""; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.lines, [""]); + }); + + it("should throw an error when writing to lines", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.lines = ["bar"]; + }, + { + name: "TypeError", + message: + "Cannot set property lines of # which has only a getter", + }, + ); + + assert.throws( + () => { + sourceCode.lines.push("qux"); + }, + { + name: "TypeError", + message: + "Cannot add property 3, object is not extensible", + }, + ); + }); + }); }); }); From 82591a6c81291ae6d432bd2d926002fe918a7780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 3 Jul 2025 23:04:59 +0900 Subject: [PATCH 25/55] wip: fix CI --- packages/plugin-kit/tests/source-code.test.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 0cc00abf..edccad4f 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -1741,9 +1741,7 @@ describe("source-code", () => { sourceCode.lines = ["bar"]; }, { - name: "TypeError", - message: - "Cannot set property lines of # which has only a getter", + name: "TypeError", // Cannot use `message` here because it behaves differently in other runtimes, such as Bun. }, ); @@ -1752,9 +1750,7 @@ describe("source-code", () => { sourceCode.lines.push("qux"); }, { - name: "TypeError", - message: - "Cannot add property 3, object is not extensible", + name: "TypeError", // Cannot use `message` here because it behaves differently in other runtimes, such as Bun. }, ); }); From 1a5022ea86255d129832892ae64ced8a30df250f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 3 Jul 2025 23:56:20 +0900 Subject: [PATCH 26/55] wip: complete refactor (maybe?) --- packages/plugin-kit/src/source-code.js | 46 ++++++++++++++------------ 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 33e5f9a5..526416d3 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -333,23 +333,23 @@ export class TextSourceCodeBase { * @returns {void} */ #ensureLineStartIndicesFromIndex(index) { - const lastIndex = this.#lineStartIndices.at(-1) ?? 0; + const lastCalculatedIndex = this.#lineStartIndices.at(-1) ?? 0; - // If we've already parsed up to or beyond this index, do nothing - if (index <= lastIndex) { + // If we've already parsed up to or beyond this index, do nothing. + if (index <= lastCalculatedIndex) { return; } - // Create a new RegExp instance to avoid lastIndex issues + // Create a new RegExp instance to avoid lastIndex issues. const lineEndingPattern = structuredClone(this.#lineEndingPattern); - // Start parsing from where we left off - const text = this.text.slice(lastIndex, index + 1); + // Start parsing from where we left off. + const text = this.text.slice(lastCalculatedIndex, index + 1); let match; while ((match = lineEndingPattern.exec(text))) { this.#lineStartIndices.push( - lastIndex + match.index + match[0].length, + lastCalculatedIndex + match.index + match[0].length, ); } } @@ -362,30 +362,32 @@ export class TextSourceCodeBase { * @returns {void} */ #ensureLineStartIndicesFromLoc(loc) { - // Calculate line indices up to `locLineIndex + 1` (current line + potentially next line) + // Calculate line indices up to the potentially next line, as it is needed for the follow‑up calculation. const nextLocLineIndex = loc.line - this.#lineStart + 1; + const lastCalculatedLineIndex = this.#lineStartIndices.length - 1; + let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex; - if (nextLocLineIndex <= this.#lineStartIndices.length - 1) { + // If we've already parsed up to or beyond this line, do nothing. + if (additionalLinesNeeded <= 0) { return; } - const lastIndex = this.#lineStartIndices.at(-1) ?? 0; + const lastCalculatedIndex = this.#lineStartIndices.at(-1) ?? 0; + + // Create a new RegExp instance to avoid lastIndex issues. const lineEndingPattern = structuredClone(this.#lineEndingPattern); - const text = this.text.slice(lastIndex); - let match; - let linesFound = 0; - const additionalLinesNeeded = - nextLocLineIndex - (this.#lineStartIndices.length - 1); + // Start parsing from where we left off. + const text = this.text.slice(lastCalculatedIndex); + let match; while ( - linesFound < additionalLinesNeeded && + Boolean(additionalLinesNeeded--) && (match = lineEndingPattern.exec(text)) ) { this.#lineStartIndices.push( - lastIndex + match.index + match[0].length, + lastCalculatedIndex + match.index + match[0].length, ); - linesFound++; } } @@ -427,11 +429,9 @@ export class TextSourceCodeBase { ); } - this.#ensureLineStartIndicesFromIndex(index); - /* * For an argument of `this.text.length`, return the location one "spot" past the last character - * of the file. If the last character is a linebreak, the location will be column 0 of the next + * of the file. If the last character is a linebreak, the location will be `#columnStart` of the next * line; otherwise, the location will be in the next column on the same line. * * See `getIndexFromLoc` for the motivation for this special case. @@ -443,6 +443,9 @@ export class TextSourceCodeBase { }; } + // Ensure `#lineStartIndices` are lazily calculated. + this.#ensureLineStartIndicesFromIndex(index); + /* * To figure out which line `index` is on, determine the last place at which index could * be inserted into `lineStartIndices` to keep the list sorted. @@ -495,6 +498,7 @@ export class TextSourceCodeBase { ); } + // Ensure `#lineStartIndices` are lazily calculated. this.#ensureLineStartIndicesFromLoc(loc); const lineStartIndex = From 2d9d12b3ea604e4abbf4c45180c619a4acc63f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 4 Jul 2025 15:39:08 +0900 Subject: [PATCH 27/55] wip: add more test cases --- packages/plugin-kit/tests/source-code.test.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index edccad4f..8771a629 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -115,6 +115,44 @@ describe("source-code", () => { assert.deepStrictEqual(sourceCode.lines, ["foo", "bar", "baz"]); }); + + it("should handle loc (ESTree) style location", () => { + const ast = { + loc: { + start: { line: 1, column: 1 }, + }, + }; + const text = "foo"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 1, + }); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 0, + ); + }); + + it("should handle position style location", () => { + const ast = { + position: { + start: { line: 1, column: 1 }, + }, + }; + const text = "foo"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 1, + }); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 0, + ); + }); }); describe("getLoc()", () => { From c70890a43a9f9e0497d86ea5e4b3204431f8246f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 4 Jul 2025 15:56:40 +0900 Subject: [PATCH 28/55] wip: add more test cases --- packages/plugin-kit/tests/source-code.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 8771a629..504f2086 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -1115,6 +1115,10 @@ describe("source-code", () => { }, ); + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 2, column: 3 }); + }); + assert.throws( () => { sourceCode.getIndexFromLoc({ line: 2, column: 4 }); @@ -1164,6 +1168,10 @@ describe("source-code", () => { }, ); + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 1, column: 4 }); + }); + assert.throws( () => { sourceCode.getIndexFromLoc({ line: 1, column: 5 }); @@ -1213,6 +1221,10 @@ describe("source-code", () => { }, ); + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 1, column: 3 }); + }); + assert.throws( () => { sourceCode.getIndexFromLoc({ line: 1, column: 4 }); @@ -1262,6 +1274,10 @@ describe("source-code", () => { }, ); + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 2, column: 4 }); + }); + assert.throws( () => { sourceCode.getIndexFromLoc({ line: 2, column: 5 }); From 59b3f2e9443f9f175717f0a4b042480339883622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 4 Jul 2025 16:15:05 +0900 Subject: [PATCH 29/55] fix: wrong console logging --- packages/plugin-kit/src/source-code.js | 17 ++++++++--------- packages/plugin-kit/tests/source-code.test.js | 8 ++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 526416d3..3c3ec95c 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -501,23 +501,22 @@ export class TextSourceCodeBase { // Ensure `#lineStartIndices` are lazily calculated. this.#ensureLineStartIndicesFromLoc(loc); + const isLastLine = + loc.line - this.#lineStart === this.#lines.length - 1; const lineStartIndex = this.#lineStartIndices[loc.line - this.#lineStart]; - const lineEndIndex = - loc.line - this.#lineStart === this.#lines.length - 1 - ? this.text.length - : this.#lineStartIndices[loc.line - this.#lineStart + 1]; + const lineEndIndex = isLastLine + ? this.text.length + : this.#lineStartIndices[loc.line - this.#lineStart + 1]; const positionIndex = lineStartIndex + loc.column - this.#columnStart; if ( loc.column < this.#columnStart || - (loc.line - this.#lineStart === this.#lineStartIndices.length - 1 && - positionIndex > lineEndIndex) || - (loc.line - this.#lineStart < this.#lineStartIndices.length - 1 && - positionIndex >= lineEndIndex) + (isLastLine && positionIndex > lineEndIndex) || + (!isLastLine && positionIndex >= lineEndIndex) ) { throw new RangeError( - `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${this.#columnStart}-${lineEndIndex - lineStartIndex - 1 + this.#columnStart}`, + `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${this.#columnStart}-${lineEndIndex - lineStartIndex + this.#columnStart + (isLastLine ? 0 : -1)}`, ); } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 504f2086..0e95fcde 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -1126,7 +1126,7 @@ describe("source-code", () => { { name: "RangeError", message: - "Column number out of range (column 4 requested). Valid range for line 2: 0-2", + "Column number out of range (column 4 requested). Valid range for line 2: 0-3", }, ); }); @@ -1179,7 +1179,7 @@ describe("source-code", () => { { name: "RangeError", message: - "Column number out of range (column 5 requested). Valid range for line 1: 1-3", + "Column number out of range (column 5 requested). Valid range for line 1: 1-4", }, ); }); @@ -1232,7 +1232,7 @@ describe("source-code", () => { { name: "RangeError", message: - "Column number out of range (column 4 requested). Valid range for line 1: 0-2", + "Column number out of range (column 4 requested). Valid range for line 1: 0-3", }, ); }); @@ -1285,7 +1285,7 @@ describe("source-code", () => { { name: "RangeError", message: - "Column number out of range (column 5 requested). Valid range for line 2: 1-3", + "Column number out of range (column 5 requested). Valid range for line 2: 1-4", }, ); }); From bb13197a489a4b52aa1b88b25e2afb5b5faa1740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 4 Jul 2025 22:39:56 +0900 Subject: [PATCH 30/55] wip: cleanup --- packages/plugin-kit/src/source-code.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 3c3ec95c..960590cc 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -302,13 +302,11 @@ export class TextSourceCodeBase { this.#lines = text.split(this.#lineEndingPattern); if (hasESTreeStyleLoc(this.ast)) { - this.#setLineColumnStart(this.ast.loc); + this.#setLineColumn(this.ast.loc); } else if (hasPosStyleLoc(this.ast)) { - this.#setLineColumnStart(this.ast.position); + this.#setLineColumn(this.ast.position); } - // don't allow further modification of this object - Object.freeze(this); Object.freeze(this.lines); } @@ -317,7 +315,7 @@ export class TextSourceCodeBase { * @param {SourceLocation} loc The `loc` or `position` object to use. * @returns {void} */ - #setLineColumnStart(loc) { + #setLineColumn(loc) { if (loc?.start?.line === 0 || loc?.start?.line === 1) { this.#lineStart = loc.start.line; } From 17509602964ada4ec654b6993ff61d6775ff11a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 4 Jul 2025 23:03:10 +0900 Subject: [PATCH 31/55] wip: lazily caculate `#lines` --- packages/plugin-kit/src/source-code.js | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 960590cc..9da78be4 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -250,7 +250,7 @@ export class TextSourceCodeBase { * The lines of text in the source code. * @type {Array} */ - #lines; + #lines = []; /** * The indices of the start of each line in the source code. @@ -299,15 +299,12 @@ export class TextSourceCodeBase { this.ast = ast; this.text = text; this.#lineEndingPattern = lineEndingPattern; - this.#lines = text.split(this.#lineEndingPattern); if (hasESTreeStyleLoc(this.ast)) { this.#setLineColumn(this.ast.loc); } else if (hasPosStyleLoc(this.ast)) { this.#setLineColumn(this.ast.position); } - - Object.freeze(this.lines); } /** @@ -325,6 +322,20 @@ export class TextSourceCodeBase { } } + /** + * Ensures that `#lines` is lazily calculated from the source text. + * @returns {void} + */ + #ensureLines() { + // If `#lines` has already been calculated, do nothing. + if (this.#lines.length > 0) { + return; + } + + this.#lines = this.text.split(this.#lineEndingPattern); + Object.freeze(this.#lines); + } + /** * Ensures `#lineStartIndices` information is calculated up to the specified index. * @param {number} index The index of a character in a file. @@ -436,8 +447,8 @@ export class TextSourceCodeBase { */ if (index === this.text.length) { return { - line: this.#lines.length - 1 + this.#lineStart, - column: (this.#lines.at(-1)?.length ?? 0) + this.#columnStart, + line: this.lines.length - 1 + this.#lineStart, + column: (this.lines.at(-1)?.length ?? 0) + this.#columnStart, }; } @@ -489,18 +500,17 @@ export class TextSourceCodeBase { if ( loc.line < this.#lineStart || - this.#lines.length - 1 + this.#lineStart < loc.line + this.lines.length - 1 + this.#lineStart < loc.line ) { throw new RangeError( - `Line number out of range (line ${loc.line} requested). Valid range: ${this.#lineStart}-${this.#lines.length - 1 + this.#lineStart}`, + `Line number out of range (line ${loc.line} requested). Valid range: ${this.#lineStart}-${this.lines.length - 1 + this.#lineStart}`, ); } // Ensure `#lineStartIndices` are lazily calculated. this.#ensureLineStartIndicesFromLoc(loc); - const isLastLine = - loc.line - this.#lineStart === this.#lines.length - 1; + const isLastLine = loc.line - this.#lineStart === this.lines.length - 1; const lineStartIndex = this.#lineStartIndices[loc.line - this.#lineStart]; const lineEndIndex = isLastLine @@ -606,6 +616,8 @@ export class TextSourceCodeBase { * @public */ get lines() { + this.#ensureLines(); // Ensure `#lines` is lazily calculated. + return this.#lines; } From 2cef1890c9c261ca6d394fea3e9879e5af3b0d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 4 Jul 2025 23:40:21 +0900 Subject: [PATCH 32/55] wip: create `#rootNodeLoc` --- packages/plugin-kit/src/source-code.js | 47 +++++++++++++++++++------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 9da78be4..b8b2b471 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -276,6 +276,13 @@ export class TextSourceCodeBase { */ #lineEndingPattern; + /** + * The location of the root node in the source code. + * Used to determine the starting and ending line/column numbers. + * @type {SourceLocation} + */ // @ts-expect-error TODO + #rootNodeLoc; + /** * The AST of the source code. * @type {Options['RootNode']} @@ -301,25 +308,41 @@ export class TextSourceCodeBase { this.#lineEndingPattern = lineEndingPattern; if (hasESTreeStyleLoc(this.ast)) { - this.#setLineColumn(this.ast.loc); + this.#setRootNodeLoc(this.ast.loc); } else if (hasPosStyleLoc(this.ast)) { - this.#setLineColumn(this.ast.position); + this.#setRootNodeLoc(this.ast.position); } } /** - * Sets the `#lineStart` and `#columnStart` based on a `loc` or `position` object. - * @param {SourceLocation} loc The `loc` or `position` object to use. + * Sets the root node loc based on a `loc` or `position` object. + * @param {SourceLocation} rootNodeLoc The `loc` or `position` object to use. * @returns {void} */ - #setLineColumn(loc) { - if (loc?.start?.line === 0 || loc?.start?.line === 1) { - this.#lineStart = loc.start.line; - } - - if (loc?.start?.column === 0 || loc?.start?.column === 1) { - this.#columnStart = loc.start.column; - } + #setRootNodeLoc(rootNodeLoc) { + // const zeroOrOne = new Set([0, 1]); + + // if(rootNodeLoc === null || typeof rootNodeLoc !== "object" || !("start" in rootNodeLoc) || !("end" in rootNodeLoc)) { + // throw new TypeError("Expected root node `loc` or `position` to be an object with `start` and `end` properties."); + // } + + // if (!zeroOrOne.has(rootNodeLoc.start.line) || !zeroOrOne.has(rootNodeLoc.start.column)) { + // throw new TypeError( + // "Expected root node `loc` or `position` to have `start.line` and `start.column` properties with values of 0 or 1.", + // ); + // } + + // if (typeof rootNodeLoc.end.line !== "number" || typeof rootNodeLoc.end.column !== "number") { + // throw new TypeError( + // "Expected root node `loc` or `position` to have `end.line` and `end.column` properties with numeric values.", + // ); + // } + + this.#rootNodeLoc = rootNodeLoc; + // @ts-expect-error TODO + this.#lineStart = this.#rootNodeLoc.start.line; + // @ts-expect-error TODO + this.#columnStart = this.#rootNodeLoc.start.column; } /** From 1d1022a654f575c95904961197fb06dd655de61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 4 Jul 2025 23:47:11 +0900 Subject: [PATCH 33/55] wip: remove `#lineStart` --- packages/plugin-kit/src/source-code.js | 33 +++++++++---------- packages/plugin-kit/tests/source-code.test.js | 18 ++++++++-- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index b8b2b471..f6205765 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -258,12 +258,6 @@ export class TextSourceCodeBase { */ #lineStartIndices = [0]; - /** - * The line number at which the parser starts counting. Defaults to `1` for ESTree compatibility. - * @type {0|1} - */ - #lineStart = 1; - /** * The column number at which the parser starts counting. Defaults to `0` for ESTree compatibility. * @type {0|1} @@ -340,8 +334,6 @@ export class TextSourceCodeBase { this.#rootNodeLoc = rootNodeLoc; // @ts-expect-error TODO - this.#lineStart = this.#rootNodeLoc.start.line; - // @ts-expect-error TODO this.#columnStart = this.#rootNodeLoc.start.column; } @@ -395,7 +387,7 @@ export class TextSourceCodeBase { */ #ensureLineStartIndicesFromLoc(loc) { // Calculate line indices up to the potentially next line, as it is needed for the follow‑up calculation. - const nextLocLineIndex = loc.line - this.#lineStart + 1; + const nextLocLineIndex = loc.line - this.#rootNodeLoc.start.line + 1; const lastCalculatedLineIndex = this.#lineStartIndices.length - 1; let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex; @@ -470,7 +462,7 @@ export class TextSourceCodeBase { */ if (index === this.text.length) { return { - line: this.lines.length - 1 + this.#lineStart, + line: this.lines.length - 1 + this.#rootNodeLoc.start.line, column: (this.lines.at(-1)?.length ?? 0) + this.#columnStart, }; } @@ -487,13 +479,15 @@ export class TextSourceCodeBase { ? this.#lineStartIndices.length : findLineNumberBinarySearch(this.#lineStartIndices, index)) - 1 + - this.#lineStart; + this.#rootNodeLoc.start.line; return { line: lineNumber, column: index - - this.#lineStartIndices[lineNumber - this.#lineStart] + + this.#lineStartIndices[ + lineNumber - this.#rootNodeLoc.start.line + ] + this.#columnStart, }; } @@ -522,23 +516,26 @@ export class TextSourceCodeBase { } if ( - loc.line < this.#lineStart || - this.lines.length - 1 + this.#lineStart < loc.line + loc.line < this.#rootNodeLoc.start.line || + this.lines.length - 1 + this.#rootNodeLoc.start.line < loc.line ) { throw new RangeError( - `Line number out of range (line ${loc.line} requested). Valid range: ${this.#lineStart}-${this.lines.length - 1 + this.#lineStart}`, + `Line number out of range (line ${loc.line} requested). Valid range: ${this.#rootNodeLoc.start.line}-${this.lines.length - 1 + this.#rootNodeLoc.start.line}`, ); } // Ensure `#lineStartIndices` are lazily calculated. this.#ensureLineStartIndicesFromLoc(loc); - const isLastLine = loc.line - this.#lineStart === this.lines.length - 1; + const isLastLine = + loc.line - this.#rootNodeLoc.start.line === this.lines.length - 1; const lineStartIndex = - this.#lineStartIndices[loc.line - this.#lineStart]; + this.#lineStartIndices[loc.line - this.#rootNodeLoc.start.line]; const lineEndIndex = isLastLine ? this.text.length - : this.#lineStartIndices[loc.line - this.#lineStart + 1]; + : this.#lineStartIndices[ + loc.line - this.#rootNodeLoc.start.line + 1 + ]; const positionIndex = lineStartIndex + loc.column - this.#columnStart; if ( diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 0e95fcde..89f484cf 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -724,7 +724,14 @@ describe("source-code", () => { }); it("should handle text with only line breaks", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "\n\r\n"; const sourceCode = new TextSourceCodeBase({ ast, text }); @@ -1626,7 +1633,14 @@ describe("source-code", () => { }); it("should handle text with only line breaks", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "\n\r\n"; const sourceCode = new TextSourceCodeBase({ ast, text }); From 0e4cac0755811f07d6617bf5ce153a9d67bc591a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 4 Jul 2025 23:54:25 +0900 Subject: [PATCH 34/55] wip: cleanup --- packages/plugin-kit/src/source-code.js | 52 +++++--------------------- 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index f6205765..e20ab3ac 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -258,12 +258,6 @@ export class TextSourceCodeBase { */ #lineStartIndices = [0]; - /** - * The column number at which the parser starts counting. Defaults to `0` for ESTree compatibility. - * @type {0|1} - */ - #columnStart = 0; - /** * The pattern to match line endings in the source code. * @type {RegExp} @@ -302,41 +296,12 @@ export class TextSourceCodeBase { this.#lineEndingPattern = lineEndingPattern; if (hasESTreeStyleLoc(this.ast)) { - this.#setRootNodeLoc(this.ast.loc); + this.#rootNodeLoc = this.ast.loc; } else if (hasPosStyleLoc(this.ast)) { - this.#setRootNodeLoc(this.ast.position); + this.#rootNodeLoc = this.ast.position; } } - /** - * Sets the root node loc based on a `loc` or `position` object. - * @param {SourceLocation} rootNodeLoc The `loc` or `position` object to use. - * @returns {void} - */ - #setRootNodeLoc(rootNodeLoc) { - // const zeroOrOne = new Set([0, 1]); - - // if(rootNodeLoc === null || typeof rootNodeLoc !== "object" || !("start" in rootNodeLoc) || !("end" in rootNodeLoc)) { - // throw new TypeError("Expected root node `loc` or `position` to be an object with `start` and `end` properties."); - // } - - // if (!zeroOrOne.has(rootNodeLoc.start.line) || !zeroOrOne.has(rootNodeLoc.start.column)) { - // throw new TypeError( - // "Expected root node `loc` or `position` to have `start.line` and `start.column` properties with values of 0 or 1.", - // ); - // } - - // if (typeof rootNodeLoc.end.line !== "number" || typeof rootNodeLoc.end.column !== "number") { - // throw new TypeError( - // "Expected root node `loc` or `position` to have `end.line` and `end.column` properties with numeric values.", - // ); - // } - - this.#rootNodeLoc = rootNodeLoc; - // @ts-expect-error TODO - this.#columnStart = this.#rootNodeLoc.start.column; - } - /** * Ensures that `#lines` is lazily calculated from the source text. * @returns {void} @@ -463,7 +428,9 @@ export class TextSourceCodeBase { if (index === this.text.length) { return { line: this.lines.length - 1 + this.#rootNodeLoc.start.line, - column: (this.lines.at(-1)?.length ?? 0) + this.#columnStart, + column: + (this.lines.at(-1)?.length ?? 0) + + this.#rootNodeLoc.start.column, }; } @@ -488,7 +455,7 @@ export class TextSourceCodeBase { this.#lineStartIndices[ lineNumber - this.#rootNodeLoc.start.line ] + - this.#columnStart, + this.#rootNodeLoc.start.column, }; } @@ -536,15 +503,16 @@ export class TextSourceCodeBase { : this.#lineStartIndices[ loc.line - this.#rootNodeLoc.start.line + 1 ]; - const positionIndex = lineStartIndex + loc.column - this.#columnStart; + const positionIndex = + lineStartIndex + loc.column - this.#rootNodeLoc.start.column; if ( - loc.column < this.#columnStart || + loc.column < this.#rootNodeLoc.start.column || (isLastLine && positionIndex > lineEndIndex) || (!isLastLine && positionIndex >= lineEndIndex) ) { throw new RangeError( - `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${this.#columnStart}-${lineEndIndex - lineStartIndex + this.#columnStart + (isLastLine ? 0 : -1)}`, + `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${this.#rootNodeLoc.start.column}-${lineEndIndex - lineStartIndex + this.#rootNodeLoc.start.column + (isLastLine ? 0 : -1)}`, ); } From 1c0531cf97ffca470a0d3ac3e9e5f5e70ab6933a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 00:39:21 +0900 Subject: [PATCH 35/55] wip: remove `@ts-expect-error` --- packages/plugin-kit/src/source-code.js | 9 +++++++-- packages/plugin-kit/tests/source-code.test.js | 18 ++---------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index e20ab3ac..47599bf1 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -267,9 +267,14 @@ export class TextSourceCodeBase { /** * The location of the root node in the source code. * Used to determine the starting and ending line/column numbers. + * - `start.line`: Defaults to `1` for ESTree compatibility. + * - `start.column`: Defaults to `0` for ESTree compatibility. * @type {SourceLocation} - */ // @ts-expect-error TODO - #rootNodeLoc; + */ + #rootNodeLoc = { + start: { line: 1, column: 0 }, + end: { line: Infinity, column: Infinity }, + }; /** * The AST of the source code. diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 89f484cf..0e95fcde 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -724,14 +724,7 @@ describe("source-code", () => { }); it("should handle text with only line breaks", () => { - const ast = { - loc: { - start: { - line: 1, - column: 0, - }, - }, - }; + const ast = {}; const text = "\n\r\n"; const sourceCode = new TextSourceCodeBase({ ast, text }); @@ -1633,14 +1626,7 @@ describe("source-code", () => { }); it("should handle text with only line breaks", () => { - const ast = { - loc: { - start: { - line: 1, - column: 0, - }, - }, - }; + const ast = {}; const text = "\n\r\n"; const sourceCode = new TextSourceCodeBase({ ast, text }); From 1e3ba0653082e3366989f6ca643b3acd3c5b6704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 14:11:24 +0900 Subject: [PATCH 36/55] wip: refactor `rootNodeLoc` --- packages/plugin-kit/src/source-code.js | 67 +++++++------------ packages/plugin-kit/tests/source-code.test.js | 18 ++++- 2 files changed, 41 insertions(+), 44 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 47599bf1..98eac662 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -64,13 +64,13 @@ function hasPosStyleRange(node) { } /** - * Performs binary search to find the line number containing a given character index. + * Performs binary search to find the line number index containing a given target index. * Returns the lower bound - the index of the first element greater than the target. * **Please note that the `lineStartIndices` should be sorted in ascending order**. * - Time Complexity: O(log n) - Significantly faster than linear search for large files. * @param {number[]} lineStartIndices Sorted array of line start indices. - * @param {number} targetIndex The target index to find the line number for. - * @returns {number} The line number for the target index. + * @param {number} targetIndex The target index to find the line number index for. + * @returns {number} The line number index for the target index. */ function findLineNumberBinarySearch(lineStartIndices, targetIndex) { let low = 0; @@ -259,23 +259,11 @@ export class TextSourceCodeBase { #lineStartIndices = [0]; /** - * The pattern to match line endings in the source code. + * The pattern to match lineEndings in the source code. * @type {RegExp} */ #lineEndingPattern; - /** - * The location of the root node in the source code. - * Used to determine the starting and ending line/column numbers. - * - `start.line`: Defaults to `1` for ESTree compatibility. - * - `start.column`: Defaults to `0` for ESTree compatibility. - * @type {SourceLocation} - */ - #rootNodeLoc = { - start: { line: 1, column: 0 }, - end: { line: Infinity, column: Infinity }, - }; - /** * The AST of the source code. * @type {Options['RootNode']} @@ -299,12 +287,6 @@ export class TextSourceCodeBase { this.ast = ast; this.text = text; this.#lineEndingPattern = lineEndingPattern; - - if (hasESTreeStyleLoc(this.ast)) { - this.#rootNodeLoc = this.ast.loc; - } else if (hasPosStyleLoc(this.ast)) { - this.#rootNodeLoc = this.ast.position; - } } /** @@ -356,8 +338,10 @@ export class TextSourceCodeBase { * @returns {void} */ #ensureLineStartIndicesFromLoc(loc) { + const rootNodeLoc = this.getLoc(this.ast); + // Calculate line indices up to the potentially next line, as it is needed for the follow‑up calculation. - const nextLocLineIndex = loc.line - this.#rootNodeLoc.start.line + 1; + const nextLocLineIndex = loc.line - rootNodeLoc.start.line + 1; const lastCalculatedLineIndex = this.#lineStartIndices.length - 1; let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex; @@ -423,6 +407,8 @@ export class TextSourceCodeBase { ); } + const rootNodeLoc = this.getLoc(this.ast); + /* * For an argument of `this.text.length`, return the location one "spot" past the last character * of the file. If the last character is a linebreak, the location will be `#columnStart` of the next @@ -432,10 +418,9 @@ export class TextSourceCodeBase { */ if (index === this.text.length) { return { - line: this.lines.length - 1 + this.#rootNodeLoc.start.line, + line: this.lines.length - 1 + rootNodeLoc.start.line, column: - (this.lines.at(-1)?.length ?? 0) + - this.#rootNodeLoc.start.column, + (this.lines.at(-1)?.length ?? 0) + rootNodeLoc.start.column, }; } @@ -451,16 +436,14 @@ export class TextSourceCodeBase { ? this.#lineStartIndices.length : findLineNumberBinarySearch(this.#lineStartIndices, index)) - 1 + - this.#rootNodeLoc.start.line; + rootNodeLoc.start.line; return { line: lineNumber, column: index - - this.#lineStartIndices[ - lineNumber - this.#rootNodeLoc.start.line - ] + - this.#rootNodeLoc.start.column, + this.#lineStartIndices[lineNumber - rootNodeLoc.start.line] + + rootNodeLoc.start.column, }; } @@ -487,12 +470,14 @@ export class TextSourceCodeBase { ); } + const rootNodeLoc = this.getLoc(this.ast); + if ( - loc.line < this.#rootNodeLoc.start.line || - this.lines.length - 1 + this.#rootNodeLoc.start.line < loc.line + loc.line < rootNodeLoc.start.line || + this.lines.length - 1 + rootNodeLoc.start.line < loc.line ) { throw new RangeError( - `Line number out of range (line ${loc.line} requested). Valid range: ${this.#rootNodeLoc.start.line}-${this.lines.length - 1 + this.#rootNodeLoc.start.line}`, + `Line number out of range (line ${loc.line} requested). Valid range: ${rootNodeLoc.start.line}-${this.lines.length - 1 + rootNodeLoc.start.line}`, ); } @@ -500,24 +485,22 @@ export class TextSourceCodeBase { this.#ensureLineStartIndicesFromLoc(loc); const isLastLine = - loc.line - this.#rootNodeLoc.start.line === this.lines.length - 1; + loc.line - rootNodeLoc.start.line === this.lines.length - 1; const lineStartIndex = - this.#lineStartIndices[loc.line - this.#rootNodeLoc.start.line]; + this.#lineStartIndices[loc.line - rootNodeLoc.start.line]; const lineEndIndex = isLastLine ? this.text.length - : this.#lineStartIndices[ - loc.line - this.#rootNodeLoc.start.line + 1 - ]; + : this.#lineStartIndices[loc.line - rootNodeLoc.start.line + 1]; const positionIndex = - lineStartIndex + loc.column - this.#rootNodeLoc.start.column; + lineStartIndex + loc.column - rootNodeLoc.start.column; if ( - loc.column < this.#rootNodeLoc.start.column || + loc.column < rootNodeLoc.start.column || (isLastLine && positionIndex > lineEndIndex) || (!isLastLine && positionIndex >= lineEndIndex) ) { throw new RangeError( - `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${this.#rootNodeLoc.start.column}-${lineEndIndex - lineStartIndex + this.#rootNodeLoc.start.column + (isLastLine ? 0 : -1)}`, + `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${rootNodeLoc.start.column}-${lineEndIndex - lineStartIndex + rootNodeLoc.start.column + (isLastLine ? 0 : -1)}`, ); } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 0e95fcde..89f484cf 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -724,7 +724,14 @@ describe("source-code", () => { }); it("should handle text with only line breaks", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "\n\r\n"; const sourceCode = new TextSourceCodeBase({ ast, text }); @@ -1626,7 +1633,14 @@ describe("source-code", () => { }); it("should handle text with only line breaks", () => { - const ast = {}; + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; const text = "\n\r\n"; const sourceCode = new TextSourceCodeBase({ ast, text }); From bf020095228538041c28d6d9bcaaf2cf29b6ccbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 14:57:04 +0900 Subject: [PATCH 37/55] wip: refactor `getLocFromIndex` and add update test cases --- packages/plugin-kit/src/source-code.js | 21 ++-- packages/plugin-kit/tests/source-code.test.js | 102 ++++++++++++++++++ 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 98eac662..138ae082 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -409,18 +409,19 @@ export class TextSourceCodeBase { const rootNodeLoc = this.getLoc(this.ast); - /* - * For an argument of `this.text.length`, return the location one "spot" past the last character - * of the file. If the last character is a linebreak, the location will be `#columnStart` of the next - * line; otherwise, the location will be in the next column on the same line. - * - * See `getIndexFromLoc` for the motivation for this special case. - */ + // If the index is at the start, return the start location of the root node. + if (index === 0) { + return { + line: rootNodeLoc.start.line, + column: rootNodeLoc.start.column, + }; + } + + // If the index is `this.text.length`, return the location one "spot" past the last character of the file. if (index === this.text.length) { return { - line: this.lines.length - 1 + rootNodeLoc.start.line, - column: - (this.lines.at(-1)?.length ?? 0) + rootNodeLoc.start.column, + line: rootNodeLoc.end.line, + column: rootNodeLoc.end.column, }; } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 89f484cf..bce4624f 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -280,6 +280,23 @@ describe("source-code", () => { ); }); + it("should throw an error when `ast.loc` or `ast.position` is not defined", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getLocFromIndex(0); + }, + { + name: "Error", + message: + "Custom getLoc() method must be implemented in the subclass.", + }, + ); + }); + it("should handle the special case of `text.length` when lineStart is 1 and columnStart is 0", () => { const ast = { loc: { @@ -287,6 +304,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 2, + column: 3, + }, }, }; const text = "foo\nbar"; @@ -311,6 +332,10 @@ describe("source-code", () => { line: 0, column: 1, }, + end: { + line: 1, + column: 4, + }, }, }; const text = "foo\nbar"; @@ -335,6 +360,10 @@ describe("source-code", () => { line: 0, column: 0, }, + end: { + line: 1, + column: 3, + }, }, }; const text = "foo\nbar"; @@ -359,6 +388,10 @@ describe("source-code", () => { line: 1, column: 1, }, + end: { + line: 2, + column: 4, + }, }, }; const text = "foo\nbar"; @@ -383,6 +416,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 3, + column: 3, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -452,6 +489,10 @@ describe("source-code", () => { line: 0, column: 1, }, + end: { + line: 2, + column: 4, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -521,6 +562,10 @@ describe("source-code", () => { line: 0, column: 0, }, + end: { + line: 2, + column: 3, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -590,6 +635,10 @@ describe("source-code", () => { line: 1, column: 1, }, + end: { + line: 3, + column: 4, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -661,6 +710,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 1, + column: 0, + }, }, }, text: "", @@ -678,6 +731,10 @@ describe("source-code", () => { line: 0, column: 1, }, + end: { + line: 0, + column: 1, + }, }, }, text: "", @@ -695,6 +752,10 @@ describe("source-code", () => { line: 0, column: 0, }, + end: { + line: 0, + column: 0, + }, }, }, text: "", @@ -712,6 +773,10 @@ describe("source-code", () => { line: 1, column: 1, }, + end: { + line: 1, + column: 1, + }, }, }, text: "", @@ -730,6 +795,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 3, + column: 0, + }, }, }; const text = "\n\r\n"; @@ -760,6 +829,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 3, + column: 3, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -785,6 +858,10 @@ describe("source-code", () => { line: 0, column: 1, }, + end: { + line: 2, + column: 4, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -810,6 +887,10 @@ describe("source-code", () => { line: 0, column: 0, }, + end: { + line: 2, + column: 3, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -835,6 +916,10 @@ describe("source-code", () => { line: 1, column: 1, }, + end: { + line: 3, + column: 4, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -933,6 +1018,23 @@ describe("source-code", () => { ); }); + it("should throw an error when `ast.loc` or `ast.position` is not defined", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 0 }); + }, + { + name: "Error", + message: + "Custom getLoc() method must be implemented in the subclass.", + }, + ); + }); + it("should throw an error for line number out of range when lineStart is 1 and columnStart is 0", () => { const ast = { loc: { From aebf8d2d244c8ebec981ee2b207c6f5fe4c83d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 15:04:45 +0900 Subject: [PATCH 38/55] wip: add more test cases --- packages/plugin-kit/tests/source-code.test.js | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index bce4624f..5ba8fbc5 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -409,6 +409,39 @@ describe("source-code", () => { ); }); + it("should convert index to location when random index is given", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 3, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 1, + column: 1, + }); // Please do not change the order of these tests. It's for checking lazy calculation. + }); + it("should convert index to location when lineStart is 1 and columnStart is 0", () => { const ast = { loc: { @@ -1399,6 +1432,35 @@ describe("source-code", () => { ); }); + it("should convert loc to index when random loc is given", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 0 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 1, + ); // Please do not change the order of these tests. It's for checking lazy calculation. + }); + it("should convert loc to index when lineStart is 1 and columnStart is 0", () => { const ast = { loc: { From 710f945a62c37a0fab14da164b716d19ec48e54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 15:21:17 +0900 Subject: [PATCH 39/55] wip: complete `getLocFromIndex` --- packages/plugin-kit/src/source-code.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 138ae082..5b4841cc 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -64,13 +64,13 @@ function hasPosStyleRange(node) { } /** - * Performs binary search to find the line number index containing a given target index. + * Performs binary search to find the line number containing a given target index. * Returns the lower bound - the index of the first element greater than the target. * **Please note that the `lineStartIndices` should be sorted in ascending order**. * - Time Complexity: O(log n) - Significantly faster than linear search for large files. * @param {number[]} lineStartIndices Sorted array of line start indices. - * @param {number} targetIndex The target index to find the line number index for. - * @returns {number} The line number index for the target index. + * @param {number} targetIndex The target index to find the line number for. + * @returns {number} The line number for the target index. */ function findLineNumberBinarySearch(lineStartIndices, targetIndex) { let low = 0; @@ -430,7 +430,7 @@ export class TextSourceCodeBase { /* * To figure out which line `index` is on, determine the last place at which index could - * be inserted into `lineStartIndices` to keep the list sorted. + * be inserted into `#lineStartIndices` to keep the list sorted. */ const lineNumber = (index >= (this.#lineStartIndices.at(-1) ?? 0) From 23510e0b2b409dbaf136aa53b7033a62a6d29dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 15:38:19 +0900 Subject: [PATCH 40/55] wip: modify comments --- packages/plugin-kit/src/source-code.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 5b4841cc..74c2767b 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -290,7 +290,7 @@ export class TextSourceCodeBase { } /** - * Ensures that `#lines` is lazily calculated from the source text. + * Ensures `#lines` is lazily calculated from the source text. * @returns {void} */ #ensureLines() { @@ -304,7 +304,7 @@ export class TextSourceCodeBase { } /** - * Ensures `#lineStartIndices` information is calculated up to the specified index. + * Ensures `#lineStartIndices` is lazily calculated up to the specified index. * @param {number} index The index of a character in a file. * @returns {void} */ @@ -331,7 +331,7 @@ export class TextSourceCodeBase { } /** - * Ensures `#lineStartIndices` information is calculated up to the specified loc. + * Ensures `#lineStartIndices` is lazily calculated up to the specified loc. * @param {Object} loc A line/column location. * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.) * @param {number} loc.column The column number of the location. (0 or 1-indexed based on language.) From 7a6e57f5946fea1cef0c0c9e211646ad5941fa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 22:15:13 +0900 Subject: [PATCH 41/55] wip: update test cases --- packages/plugin-kit/tests/source-code.test.js | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 5ba8fbc5..e19f6d3f 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -1075,6 +1075,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 2, + column: 3, + }, }, }; const text = "foo\nbar"; @@ -1113,6 +1117,10 @@ describe("source-code", () => { line: 0, column: 1, }, + end: { + line: 1, + column: 4, + }, }, }; const text = "foo\nbar"; @@ -1151,6 +1159,10 @@ describe("source-code", () => { line: 0, column: 0, }, + end: { + line: 1, + column: 3, + }, }, }; const text = "foo\nbar"; @@ -1189,6 +1201,10 @@ describe("source-code", () => { line: 1, column: 1, }, + end: { + line: 2, + column: 4, + }, }, }; const text = "foo\nbar"; @@ -1227,6 +1243,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 2, + column: 3, + }, }, }; const text = "foo\nbar"; @@ -1280,6 +1300,10 @@ describe("source-code", () => { line: 0, column: 1, }, + end: { + line: 1, + column: 4, + }, }, }; const text = "foo\nbar"; @@ -1333,6 +1357,10 @@ describe("source-code", () => { line: 0, column: 0, }, + end: { + line: 1, + column: 3, + }, }, }; const text = "foo\nbar"; @@ -1386,6 +1414,10 @@ describe("source-code", () => { line: 1, column: 1, }, + end: { + line: 2, + column: 4, + }, }, }; const text = "foo\nbar"; @@ -1439,6 +1471,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 3, + column: 3, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -1468,6 +1504,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 3, + column: 3, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -1537,6 +1577,10 @@ describe("source-code", () => { line: 0, column: 1, }, + end: { + line: 2, + column: 4, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -1606,6 +1650,10 @@ describe("source-code", () => { line: 0, column: 0, }, + end: { + line: 2, + column: 3, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -1675,6 +1723,10 @@ describe("source-code", () => { line: 1, column: 1, }, + end: { + line: 3, + column: 4, + }, }, }; const text = "foo\nbar\r\nbaz"; @@ -1746,6 +1798,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 1, + column: 0, + }, }, }, text: "", @@ -1760,6 +1816,10 @@ describe("source-code", () => { line: 0, column: 1, }, + end: { + line: 0, + column: 1, + }, }, }, text: "", @@ -1774,6 +1834,10 @@ describe("source-code", () => { line: 0, column: 0, }, + end: { + line: 0, + column: 0, + }, }, }, text: "", @@ -1788,6 +1852,10 @@ describe("source-code", () => { line: 1, column: 1, }, + end: { + line: 1, + column: 1, + }, }, }, text: "", @@ -1803,6 +1871,10 @@ describe("source-code", () => { line: 1, column: 0, }, + end: { + line: 3, + column: 0, + }, }, }; const text = "\n\r\n"; From 0d98c6bb9382821e3903c9a484551419894e4f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 22:18:49 +0900 Subject: [PATCH 42/55] wip: remove unnecessary test --- packages/plugin-kit/tests/source-code.test.js | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index e19f6d3f..83a36f38 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -115,44 +115,6 @@ describe("source-code", () => { assert.deepStrictEqual(sourceCode.lines, ["foo", "bar", "baz"]); }); - - it("should handle loc (ESTree) style location", () => { - const ast = { - loc: { - start: { line: 1, column: 1 }, - }, - }; - const text = "foo"; - const sourceCode = new TextSourceCodeBase({ ast, text }); - - assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { - line: 1, - column: 1, - }); - assert.strictEqual( - sourceCode.getIndexFromLoc({ line: 1, column: 1 }), - 0, - ); - }); - - it("should handle position style location", () => { - const ast = { - position: { - start: { line: 1, column: 1 }, - }, - }; - const text = "foo"; - const sourceCode = new TextSourceCodeBase({ ast, text }); - - assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { - line: 1, - column: 1, - }); - assert.strictEqual( - sourceCode.getIndexFromLoc({ line: 1, column: 1 }), - 0, - ); - }); }); describe("getLoc()", () => { From 2fe8cd586c7f030164de608034e7509fcd3512cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 22:22:42 +0900 Subject: [PATCH 43/55] wip: refactor line ending calculation --- packages/plugin-kit/src/source-code.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 74c2767b..50509d4b 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -475,18 +475,17 @@ export class TextSourceCodeBase { if ( loc.line < rootNodeLoc.start.line || - this.lines.length - 1 + rootNodeLoc.start.line < loc.line + rootNodeLoc.end.line < loc.line ) { throw new RangeError( - `Line number out of range (line ${loc.line} requested). Valid range: ${rootNodeLoc.start.line}-${this.lines.length - 1 + rootNodeLoc.start.line}`, + `Line number out of range (line ${loc.line} requested). Valid range: ${rootNodeLoc.start.line}-${rootNodeLoc.end.line}`, ); } // Ensure `#lineStartIndices` are lazily calculated. this.#ensureLineStartIndicesFromLoc(loc); - const isLastLine = - loc.line - rootNodeLoc.start.line === this.lines.length - 1; + const isLastLine = loc.line === rootNodeLoc.end.line; const lineStartIndex = this.#lineStartIndices[loc.line - rootNodeLoc.start.line]; const lineEndIndex = isLastLine From 75e156fd99575e8346ef452d512f7bad08f03194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 22:26:41 +0900 Subject: [PATCH 44/55] wip: add early return logic --- packages/plugin-kit/src/source-code.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 50509d4b..79abab4d 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -482,6 +482,22 @@ export class TextSourceCodeBase { ); } + // If the loc is at the start, return the start index of the root node. + if ( + loc.line === rootNodeLoc.start.line && + loc.column === rootNodeLoc.start.column + ) { + return 0; + } + + // If the loc is at the end, return the index one "spot" past the last character of the file. + if ( + loc.line === rootNodeLoc.end.line && + loc.column === rootNodeLoc.end.column + ) { + return this.text.length; + } + // Ensure `#lineStartIndices` are lazily calculated. this.#ensureLineStartIndicesFromLoc(loc); From b1e52499aebd69bc77478c9767960c2476200088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Sat, 5 Jul 2025 22:34:23 +0900 Subject: [PATCH 45/55] wip: refactor `#ensureLineStartIndicesFromLoc` --- packages/plugin-kit/src/source-code.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 79abab4d..b2b6faf0 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -334,14 +334,12 @@ export class TextSourceCodeBase { * Ensures `#lineStartIndices` is lazily calculated up to the specified loc. * @param {Object} loc A line/column location. * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.) - * @param {number} loc.column The column number of the location. (0 or 1-indexed based on language.) * @returns {void} */ #ensureLineStartIndicesFromLoc(loc) { - const rootNodeLoc = this.getLoc(this.ast); - // Calculate line indices up to the potentially next line, as it is needed for the follow‑up calculation. - const nextLocLineIndex = loc.line - rootNodeLoc.start.line + 1; + const nextLocLineIndex = + loc.line - this.getLoc(this.ast).start.line + 1; const lastCalculatedLineIndex = this.#lineStartIndices.length - 1; let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex; From 9d1f5419dfd2d1c27c9bd2324ddf70b946df2586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Wed, 9 Jul 2025 21:27:00 +0900 Subject: [PATCH 46/55] wip: cleanup using destructuring --- packages/plugin-kit/src/source-code.js | 55 ++++++++++++-------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index b2b6faf0..4910b7df 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -405,21 +405,24 @@ export class TextSourceCodeBase { ); } - const rootNodeLoc = this.getLoc(this.ast); + const { + start: { line: startLine, column: startColumn }, + end: { line: endLine, column: endColumn }, + } = this.getLoc(this.ast); // If the index is at the start, return the start location of the root node. if (index === 0) { return { - line: rootNodeLoc.start.line, - column: rootNodeLoc.start.column, + line: startLine, + column: startColumn, }; } // If the index is `this.text.length`, return the location one "spot" past the last character of the file. if (index === this.text.length) { return { - line: rootNodeLoc.end.line, - column: rootNodeLoc.end.column, + line: endLine, + column: endColumn, }; } @@ -435,14 +438,14 @@ export class TextSourceCodeBase { ? this.#lineStartIndices.length : findLineNumberBinarySearch(this.#lineStartIndices, index)) - 1 + - rootNodeLoc.start.line; + startLine; return { line: lineNumber, column: index - - this.#lineStartIndices[lineNumber - rootNodeLoc.start.line] + - rootNodeLoc.start.column, + this.#lineStartIndices[lineNumber - startLine] + + startColumn, }; } @@ -469,52 +472,44 @@ export class TextSourceCodeBase { ); } - const rootNodeLoc = this.getLoc(this.ast); + const { + start: { line: startLine, column: startColumn }, + end: { line: endLine, column: endColumn }, + } = this.getLoc(this.ast); - if ( - loc.line < rootNodeLoc.start.line || - rootNodeLoc.end.line < loc.line - ) { + if (loc.line < startLine || endLine < loc.line) { throw new RangeError( - `Line number out of range (line ${loc.line} requested). Valid range: ${rootNodeLoc.start.line}-${rootNodeLoc.end.line}`, + `Line number out of range (line ${loc.line} requested). Valid range: ${startLine}-${endLine}`, ); } // If the loc is at the start, return the start index of the root node. - if ( - loc.line === rootNodeLoc.start.line && - loc.column === rootNodeLoc.start.column - ) { + if (loc.line === startLine && loc.column === startColumn) { return 0; } // If the loc is at the end, return the index one "spot" past the last character of the file. - if ( - loc.line === rootNodeLoc.end.line && - loc.column === rootNodeLoc.end.column - ) { + if (loc.line === endLine && loc.column === endColumn) { return this.text.length; } // Ensure `#lineStartIndices` are lazily calculated. this.#ensureLineStartIndicesFromLoc(loc); - const isLastLine = loc.line === rootNodeLoc.end.line; - const lineStartIndex = - this.#lineStartIndices[loc.line - rootNodeLoc.start.line]; + const isLastLine = loc.line === endLine; + const lineStartIndex = this.#lineStartIndices[loc.line - startLine]; const lineEndIndex = isLastLine ? this.text.length - : this.#lineStartIndices[loc.line - rootNodeLoc.start.line + 1]; - const positionIndex = - lineStartIndex + loc.column - rootNodeLoc.start.column; + : this.#lineStartIndices[loc.line - startLine + 1]; + const positionIndex = lineStartIndex + loc.column - startColumn; if ( - loc.column < rootNodeLoc.start.column || + loc.column < startColumn || (isLastLine && positionIndex > lineEndIndex) || (!isLastLine && positionIndex >= lineEndIndex) ) { throw new RangeError( - `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${rootNodeLoc.start.column}-${lineEndIndex - lineStartIndex + rootNodeLoc.start.column + (isLastLine ? 0 : -1)}`, + `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${startColumn}-${lineEndIndex - lineStartIndex + startColumn + (isLastLine ? 0 : -1)}`, ); } From 123118c4328e0f712d8a94dde783f5e500cb64a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Wed, 9 Jul 2025 23:29:02 +0900 Subject: [PATCH 47/55] wip: calculate lines together --- packages/plugin-kit/src/source-code.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 4910b7df..97c85bf2 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -322,11 +322,14 @@ export class TextSourceCodeBase { // Start parsing from where we left off. const text = this.text.slice(lastCalculatedIndex, index + 1); + let lastSliceIndex = 0; let match; while ((match = lineEndingPattern.exec(text))) { + this.#lines.push(text.slice(lastSliceIndex, match.index)); this.#lineStartIndices.push( lastCalculatedIndex + match.index + match[0].length, ); + lastSliceIndex = match.index + match[0].length; } } @@ -356,14 +359,17 @@ export class TextSourceCodeBase { // Start parsing from where we left off. const text = this.text.slice(lastCalculatedIndex); + let lastSliceIndex = 0; let match; while ( Boolean(additionalLinesNeeded--) && (match = lineEndingPattern.exec(text)) ) { + this.#lines.push(text.slice(lastSliceIndex, match.index)); this.#lineStartIndices.push( lastCalculatedIndex + match.index + match[0].length, ); + lastSliceIndex = match.index + match[0].length; } } From 91f3b8e50bf258b14b419d84feb1c68f4775f488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Wed, 9 Jul 2025 23:55:34 +0900 Subject: [PATCH 48/55] wip: calculate lines together --- packages/plugin-kit/src/source-code.js | 22 +++++++++++++++++-- packages/plugin-kit/tests/source-code.test.js | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 97c85bf2..ddaeb342 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -295,11 +295,29 @@ export class TextSourceCodeBase { */ #ensureLines() { // If `#lines` has already been calculated, do nothing. - if (this.#lines.length > 0) { + if (this.#lines.length === this.#lineStartIndices.length) { return; } - this.#lines = this.text.split(this.#lineEndingPattern); + const lastCalculatedIndex = this.#lineStartIndices.at(-1) ?? 0; + + // Create a new RegExp instance to avoid lastIndex issues. + const lineEndingPattern = structuredClone(this.#lineEndingPattern); + + // Start parsing from where we left off. + const text = this.text.slice(lastCalculatedIndex); + + let lastSliceIndex = 0; + let match; + while ((match = lineEndingPattern.exec(text))) { + this.#lines.push(text.slice(lastSliceIndex, match.index)); + this.#lineStartIndices.push( + lastCalculatedIndex + match.index + match[0].length, + ); + lastSliceIndex = match.index + match[0].length; + } + this.#lines.push(text.slice(lastSliceIndex)); + Object.freeze(this.#lines); } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 83a36f38..37904a5f 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -1971,7 +1971,7 @@ describe("source-code", () => { const sourceCode = new TextSourceCodeBase({ ast, text, - lineEndingPattern: /\n/u, + lineEndingPattern: /\n/gu, }); assert.deepStrictEqual(sourceCode.lines, [ From 98691ad33557a4fc0ff6693ce50d820e4e9d5577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 10 Jul 2025 01:16:29 +0900 Subject: [PATCH 49/55] wip: refactor code to reduce redundency --- packages/plugin-kit/src/source-code.js | 74 ++++++++++--------- packages/plugin-kit/tests/source-code.test.js | 2 +- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index ddaeb342..5b4a26b8 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -281,14 +281,37 @@ export class TextSourceCodeBase { * @param {Object} options The options for the instance. * @param {string} options.text The source code text. * @param {Options['RootNode']} options.ast The root AST node. - * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. Defaults to `/\r?\n/gu`. + * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. Defaults to `/\r?\n/u`. */ - constructor({ text, ast, lineEndingPattern = /\r?\n/gu }) { + constructor({ text, ast, lineEndingPattern = /\r?\n/u }) { this.ast = ast; this.text = text; this.#lineEndingPattern = lineEndingPattern; } + /** + * Parses the source text into lines and updates the `#lines` and `#lineStartIndices` properties. + * @param {string} text The source text to parse into lines. + * @returns {boolean} `true` if the text was successfully parsed into lines, `false` otherwise. + */ + #parseText(text) { + // Create a new RegExp instance to avoid lastIndex issues. + const match = structuredClone(this.#lineEndingPattern).exec(text); + + if (!match) { + return false; + } + + this.#lines.push(text.slice(0, match.index)); + this.#lineStartIndices.push( + (this.#lineStartIndices.at(-1) ?? 0) + + match.index + + match[0].length, + ); + + return true; + } + /** * Ensures `#lines` is lazily calculated from the source text. * @returns {void} @@ -299,24 +322,13 @@ export class TextSourceCodeBase { return; } - const lastCalculatedIndex = this.#lineStartIndices.at(-1) ?? 0; - - // Create a new RegExp instance to avoid lastIndex issues. - const lineEndingPattern = structuredClone(this.#lineEndingPattern); - - // Start parsing from where we left off. - const text = this.text.slice(lastCalculatedIndex); - - let lastSliceIndex = 0; - let match; - while ((match = lineEndingPattern.exec(text))) { - this.#lines.push(text.slice(lastSliceIndex, match.index)); - this.#lineStartIndices.push( - lastCalculatedIndex + match.index + match[0].length, - ); - lastSliceIndex = match.index + match[0].length; + while ( + this.#parseText(this.text.slice(this.#lineStartIndices.at(-1))) + ) { + // Continue parsing until no more matches are found. } - this.#lines.push(text.slice(lastSliceIndex)); + + this.#lines.push(this.text.slice(this.#lineStartIndices.at(-1))); Object.freeze(this.#lines); } @@ -327,27 +339,17 @@ export class TextSourceCodeBase { * @returns {void} */ #ensureLineStartIndicesFromIndex(index) { - const lastCalculatedIndex = this.#lineStartIndices.at(-1) ?? 0; - // If we've already parsed up to or beyond this index, do nothing. - if (index <= lastCalculatedIndex) { + if (index <= (this.#lineStartIndices.at(-1) ?? 0)) { return; } - // Create a new RegExp instance to avoid lastIndex issues. - const lineEndingPattern = structuredClone(this.#lineEndingPattern); - - // Start parsing from where we left off. - const text = this.text.slice(lastCalculatedIndex, index + 1); - - let lastSliceIndex = 0; - let match; - while ((match = lineEndingPattern.exec(text))) { - this.#lines.push(text.slice(lastSliceIndex, match.index)); - this.#lineStartIndices.push( - lastCalculatedIndex + match.index + match[0].length, - ); - lastSliceIndex = match.index + match[0].length; + while ( + this.#parseText( + this.text.slice(this.#lineStartIndices.at(-1), index + 1), + ) + ) { + // Continue parsing until no more matches are found. } } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index 37904a5f..c10cd825 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -1971,7 +1971,7 @@ describe("source-code", () => { const sourceCode = new TextSourceCodeBase({ ast, text, - lineEndingPattern: /\n/gu, + lineEndingPattern: /\n/u, // Avoid using the `g` flag here, as this test is meant to run without it. }); assert.deepStrictEqual(sourceCode.lines, [ From ae465b6d22cbd104ce56f356ddd99fb97a64ba4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 10 Jul 2025 15:07:28 +0900 Subject: [PATCH 50/55] wip: refactor `#ensureLineStartIndicesFromLoc` --- packages/plugin-kit/src/source-code.js | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 5b4a26b8..ad3457e2 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -371,25 +371,11 @@ export class TextSourceCodeBase { return; } - const lastCalculatedIndex = this.#lineStartIndices.at(-1) ?? 0; - - // Create a new RegExp instance to avoid lastIndex issues. - const lineEndingPattern = structuredClone(this.#lineEndingPattern); - - // Start parsing from where we left off. - const text = this.text.slice(lastCalculatedIndex); - - let lastSliceIndex = 0; - let match; while ( - Boolean(additionalLinesNeeded--) && - (match = lineEndingPattern.exec(text)) + this.#parseText(this.text.slice(this.#lineStartIndices.at(-1))) && + Boolean(additionalLinesNeeded--) ) { - this.#lines.push(text.slice(lastSliceIndex, match.index)); - this.#lineStartIndices.push( - lastCalculatedIndex + match.index + match[0].length, - ); - lastSliceIndex = match.index + match[0].length; + // Continue parsing until no more matches are found. } } From 8afa6acbecffdf135280bf9feeed71f79c489b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 10 Jul 2025 15:22:39 +0900 Subject: [PATCH 51/55] wip: refactor `#ensureLineStartIndicesFromLoc` --- packages/plugin-kit/src/source-code.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index ad3457e2..34e827c2 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -357,12 +357,12 @@ export class TextSourceCodeBase { * Ensures `#lineStartIndices` is lazily calculated up to the specified loc. * @param {Object} loc A line/column location. * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.) + * @param {number} lineStart The line number at which the parser starts counting. * @returns {void} */ - #ensureLineStartIndicesFromLoc(loc) { + #ensureLineStartIndicesFromLoc(loc, lineStart) { // Calculate line indices up to the potentially next line, as it is needed for the follow‑up calculation. - const nextLocLineIndex = - loc.line - this.getLoc(this.ast).start.line + 1; + const nextLocLineIndex = loc.line - lineStart + 1; const lastCalculatedLineIndex = this.#lineStartIndices.length - 1; let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex; @@ -506,7 +506,7 @@ export class TextSourceCodeBase { } // Ensure `#lineStartIndices` are lazily calculated. - this.#ensureLineStartIndicesFromLoc(loc); + this.#ensureLineStartIndicesFromLoc(loc, startLine); const isLastLine = loc.line === endLine; const lineStartIndex = this.#lineStartIndices[loc.line - startLine]; From ffb9b54f28b17c52e66a323762c899dc8ab5f7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Thu, 10 Jul 2025 15:24:37 +0900 Subject: [PATCH 52/55] wip: rename var --- packages/plugin-kit/src/source-code.js | 44 +++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 34e827c2..647f9e84 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -418,23 +418,23 @@ export class TextSourceCodeBase { } const { - start: { line: startLine, column: startColumn }, - end: { line: endLine, column: endColumn }, + start: { line: lineStart, column: columnStart }, + end: { line: lineEnd, column: columnEnd }, } = this.getLoc(this.ast); // If the index is at the start, return the start location of the root node. if (index === 0) { return { - line: startLine, - column: startColumn, + line: lineStart, + column: columnStart, }; } // If the index is `this.text.length`, return the location one "spot" past the last character of the file. if (index === this.text.length) { return { - line: endLine, - column: endColumn, + line: lineEnd, + column: columnEnd, }; } @@ -450,14 +450,14 @@ export class TextSourceCodeBase { ? this.#lineStartIndices.length : findLineNumberBinarySearch(this.#lineStartIndices, index)) - 1 + - startLine; + lineStart; return { line: lineNumber, column: index - - this.#lineStartIndices[lineNumber - startLine] + - startColumn, + this.#lineStartIndices[lineNumber - lineStart] + + columnStart, }; } @@ -485,43 +485,43 @@ export class TextSourceCodeBase { } const { - start: { line: startLine, column: startColumn }, - end: { line: endLine, column: endColumn }, + start: { line: lineStart, column: columnStart }, + end: { line: lineEnd, column: columnEnd }, } = this.getLoc(this.ast); - if (loc.line < startLine || endLine < loc.line) { + if (loc.line < lineStart || lineEnd < loc.line) { throw new RangeError( - `Line number out of range (line ${loc.line} requested). Valid range: ${startLine}-${endLine}`, + `Line number out of range (line ${loc.line} requested). Valid range: ${lineStart}-${lineEnd}`, ); } // If the loc is at the start, return the start index of the root node. - if (loc.line === startLine && loc.column === startColumn) { + if (loc.line === lineStart && loc.column === columnStart) { return 0; } // If the loc is at the end, return the index one "spot" past the last character of the file. - if (loc.line === endLine && loc.column === endColumn) { + if (loc.line === lineEnd && loc.column === columnEnd) { return this.text.length; } // Ensure `#lineStartIndices` are lazily calculated. - this.#ensureLineStartIndicesFromLoc(loc, startLine); + this.#ensureLineStartIndicesFromLoc(loc, lineStart); - const isLastLine = loc.line === endLine; - const lineStartIndex = this.#lineStartIndices[loc.line - startLine]; + const isLastLine = loc.line === lineEnd; + const lineStartIndex = this.#lineStartIndices[loc.line - lineStart]; const lineEndIndex = isLastLine ? this.text.length - : this.#lineStartIndices[loc.line - startLine + 1]; - const positionIndex = lineStartIndex + loc.column - startColumn; + : this.#lineStartIndices[loc.line - lineStart + 1]; + const positionIndex = lineStartIndex + loc.column - columnStart; if ( - loc.column < startColumn || + loc.column < columnStart || (isLastLine && positionIndex > lineEndIndex) || (!isLastLine && positionIndex >= lineEndIndex) ) { throw new RangeError( - `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${startColumn}-${lineEndIndex - lineStartIndex + startColumn + (isLastLine ? 0 : -1)}`, + `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${columnStart}-${lineEndIndex - lineStartIndex + columnStart + (isLastLine ? 0 : -1)}`, ); } From 13014b45cfd5341eff789343d2d5d6fbbbca3ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 11 Jul 2025 22:42:53 +0900 Subject: [PATCH 53/55] wip: rename to `#fineNextLine` --- packages/plugin-kit/src/source-code.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 647f9e84..8e2c3d6f 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -290,11 +290,11 @@ export class TextSourceCodeBase { } /** - * Parses the source text into lines and updates the `#lines` and `#lineStartIndices` properties. - * @param {string} text The source text to parse into lines. - * @returns {boolean} `true` if the text was successfully parsed into lines, `false` otherwise. + * Finds the next line in the source text and updates `#lines` and `#lineStartIndices`. + * @param {string} text The text to search for the next line. + * @returns {boolean} `true` if a next line was found, `false` otherwise. */ - #parseText(text) { + #findNextLine(text) { // Create a new RegExp instance to avoid lastIndex issues. const match = structuredClone(this.#lineEndingPattern).exec(text); @@ -323,7 +323,7 @@ export class TextSourceCodeBase { } while ( - this.#parseText(this.text.slice(this.#lineStartIndices.at(-1))) + this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1))) ) { // Continue parsing until no more matches are found. } @@ -345,7 +345,7 @@ export class TextSourceCodeBase { } while ( - this.#parseText( + this.#findNextLine( this.text.slice(this.#lineStartIndices.at(-1), index + 1), ) ) { @@ -372,7 +372,9 @@ export class TextSourceCodeBase { } while ( - this.#parseText(this.text.slice(this.#lineStartIndices.at(-1))) && + this.#findNextLine( + this.text.slice(this.#lineStartIndices.at(-1)), + ) && Boolean(additionalLinesNeeded--) ) { // Continue parsing until no more matches are found. From 3d3448ac5a60f982448a78abb3712cb69081aca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 11 Jul 2025 22:46:46 +0900 Subject: [PATCH 54/55] wip: refactor `additonalLinesNeeded` --- packages/plugin-kit/src/source-code.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 8e2c3d6f..9e1d3a1e 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -375,9 +375,10 @@ export class TextSourceCodeBase { this.#findNextLine( this.text.slice(this.#lineStartIndices.at(-1)), ) && - Boolean(additionalLinesNeeded--) + additionalLinesNeeded > 0 ) { - // Continue parsing until no more matches are found. + // Continue parsing until no more matches are found or we have enough lines. + additionalLinesNeeded -= 1; } } From 1296ace4827db492ec0b0e25a85bb8f16fe12697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 11 Jul 2025 22:58:02 +0900 Subject: [PATCH 55/55] wip: replace `structuredClone` with `RegExp` --- packages/plugin-kit/src/source-code.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index 9e1d3a1e..e2d07bce 100644 --- a/packages/plugin-kit/src/source-code.js +++ b/packages/plugin-kit/src/source-code.js @@ -286,7 +286,11 @@ export class TextSourceCodeBase { constructor({ text, ast, lineEndingPattern = /\r?\n/u }) { this.ast = ast; this.text = text; - this.#lineEndingPattern = lineEndingPattern; + // Remove the global(`g`) flag from the `lineEndingPattern` to avoid issues with lastIndex. + this.#lineEndingPattern = new RegExp( + lineEndingPattern.source, + lineEndingPattern.flags.replace("g", ""), + ); } /** @@ -295,8 +299,7 @@ export class TextSourceCodeBase { * @returns {boolean} `true` if a next line was found, `false` otherwise. */ #findNextLine(text) { - // Create a new RegExp instance to avoid lastIndex issues. - const match = structuredClone(this.#lineEndingPattern).exec(text); + const match = this.#lineEndingPattern.exec(text); if (!match) { return false;