diff --git a/packages/plugin-kit/README.md b/packages/plugin-kit/README.md index 09f19801..4f14210a 100644 --- a/packages/plugin-kit/README.md +++ b/packages/plugin-kit/README.md @@ -205,6 +205,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. diff --git a/packages/plugin-kit/src/source-code.js b/packages/plugin-kit/src/source-code.js index a3221af5..b2b6faf0 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 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. + */ +function findLineNumberBinarySearch(lineStartIndices, targetIndex) { + let low = 0; + let high = lineStartIndices.length - 1; + + while (low < high) { + const mid = ((low + high) / 2) | 0; // Use bitwise OR to floor the division. + + if (targetIndex < lineStartIndices[mid]) { + high = mid; + } else { + low = mid + 1; + } + } + + return low; +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- @@ -216,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 { @@ -224,7 +250,19 @@ 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 pattern to match lineEndings in the source code. + * @type {RegExp} + */ + #lineEndingPattern; /** * The AST of the source code. @@ -243,12 +281,90 @@ 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`. */ - constructor({ text, ast, lineEndingPattern = /\r?\n/u }) { + constructor({ text, ast, lineEndingPattern = /\r?\n/gu }) { this.ast = ast; this.text = text; - this.#lines = text.split(lineEndingPattern); + this.#lineEndingPattern = lineEndingPattern; + } + + /** + * Ensures `#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` is lazily calculated up to the specified index. + * @param {number} index The index of a character in a file. + * @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) { + 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 match; + while ((match = lineEndingPattern.exec(text))) { + this.#lineStartIndices.push( + lastCalculatedIndex + match.index + match[0].length, + ); + } + } + + /** + * 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.) + * @returns {void} + */ + #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.getLoc(this.ast).start.line + 1; + const lastCalculatedLineIndex = this.#lineStartIndices.length - 1; + let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex; + + // If we've already parsed up to or beyond this line, do nothing. + if (additionalLinesNeeded <= 0) { + 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 match; + while ( + Boolean(additionalLinesNeeded--) && + (match = lineEndingPattern.exec(text)) + ) { + this.#lineStartIndices.push( + lastCalculatedIndex + match.index + match[0].length, + ); + } } /** @@ -271,6 +387,140 @@ export class TextSourceCodeBase { ); } + /** + * 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. + * @public + */ + 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}).`, + ); + } + + const rootNodeLoc = 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, + }; + } + + // 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, + }; + } + + // 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. + */ + const lineNumber = + (index >= (this.#lineStartIndices.at(-1) ?? 0) + ? this.#lineStartIndices.length + : findLineNumberBinarySearch(this.#lineStartIndices, index)) - + 1 + + rootNodeLoc.start.line; + + return { + line: lineNumber, + column: + index - + this.#lineStartIndices[lineNumber - rootNodeLoc.start.line] + + rootNodeLoc.start.column, + }; + } + + /** + * 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. + * @public + */ + getIndexFromLoc(loc) { + if ( + loc === null || + 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.", + ); + } + + const rootNodeLoc = this.getLoc(this.ast); + + if ( + loc.line < rootNodeLoc.start.line || + rootNodeLoc.end.line < loc.line + ) { + throw new RangeError( + `Line number out of range (line ${loc.line} requested). Valid range: ${rootNodeLoc.start.line}-${rootNodeLoc.end.line}`, + ); + } + + // 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); + + const isLastLine = loc.line === rootNodeLoc.end.line; + const lineStartIndex = + this.#lineStartIndices[loc.line - rootNodeLoc.start.line]; + const lineEndIndex = isLastLine + ? this.text.length + : this.#lineStartIndices[loc.line - rootNodeLoc.start.line + 1]; + const positionIndex = + lineStartIndex + loc.column - rootNodeLoc.start.column; + + if ( + 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}: ${rootNodeLoc.start.column}-${lineEndIndex - lineStartIndex + rootNodeLoc.start.column + (isLastLine ? 0 : -1)}`, + ); + } + + return positionIndex; + } + /** * Returns the range information for the given node or token. * @param {Options['SyntaxElementWithLoc']} nodeOrToken The node or token to get the range information for. @@ -356,6 +606,8 @@ export class TextSourceCodeBase { * @public */ get lines() { + this.#ensureLines(); // Ensure `#lines` is lazily calculated. + return this.#lines; } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index dcb90842..83a36f38 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -155,6 +155,1712 @@ 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"); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(null); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(undefined); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(true); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(false); + }, + { + name: "TypeError", + message: "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); + }, + { + name: "RangeError", + message: + "Index out of range (requested index -1, but source text has length 7).", + }, + ); + }); + + 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); + }, + { + name: "RangeError", + message: + "Index out of range (requested index 8, but source text has length 7).", + }, + ); + }); + + 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: { + start: { + line: 1, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + 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 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 1, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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 = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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 = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 2, + column: 4, + }, + ); + }); + + 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: { + 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(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 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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 = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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 = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 3, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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", () => { + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 0, + }, + }, + }, + text: "", + }).getLocFromIndex(0), + { + line: 1, + column: 0, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 0, + column: 1, + }, + }, + }, + text: "", + }).getLocFromIndex(0), + { + line: 0, + column: 1, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 0, + column: 0, + }, + }, + }, + text: "", + }).getLocFromIndex(0), + { + line: 0, + column: 0, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + text: "", + }).getLocFromIndex(0), + { + line: 1, + column: 1, + }, + ); + }); + + it("should handle text with only line breaks", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 0, + }, + }, + }; + 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, + }); + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 1 and columnStart is 0", () => { + 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, + }); + + 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 = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + 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), + ), + ); + } + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + 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), + ), + ); + } + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 3, + column: 4, + }, + }, + }; + 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("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"); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc(null); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc(undefined); + }, + { + name: "TypeError", + message: + "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({}); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: "1", column: 0 }); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: "0" }); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + }); + + 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: { + start: { + line: 1, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 0 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 0 requested). Valid range: 1-2", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 3, column: 0 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 3 requested). Valid range: 1-2", + }, + ); + }); + + it("should throw an error for line number out of range when lineStart is 0 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 1, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: -1, column: 1 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line -1 requested). Valid range: 0-1", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 1 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 2 requested). Valid range: 0-1", + }, + ); + }); + + it("should throw an error for line number out of range when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: -1, column: 0 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line -1 requested). Valid range: 0-1", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 0 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 2 requested). Valid range: 0-1", + }, + ); + }); + + it("should throw an error for line number out of range when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 1 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 0 requested). Valid range: 1-2", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 3, column: 1 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 3 requested). Valid range: 1-2", + }, + ); + }); + + it("should throw an error for column number out of range when lineStart is 1 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: -1 }); + }, + { + 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 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 1: 0-3", + }, + ); + + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 2, column: 3 }); + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 4 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 2: 0-3", + }, + ); + }); + + it("should throw an error for column number out of range when lineStart is 0 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 1, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 0 }); + }, + { + 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 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 0: 1-4", + }, + ); + + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 1, column: 4 }); + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 5 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 1: 1-4", + }, + ); + }); + + it("should throw an error for column number out of range when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: -1 }); + }, + { + 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 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 0: 0-3", + }, + ); + + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 1, column: 3 }); + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 4 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 1: 0-3", + }, + ); + }); + + it("should throw an error for column number out of range when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 0 }); + }, + { + 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 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 1: 1-4", + }, + ); + + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 2, column: 4 }); + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 5 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 2: 1-4", + }, + ); + }); + + it("should convert loc to index when random loc 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.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: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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 = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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 = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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 = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 3, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + 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: { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 0, + }, + }, + }, + text: "", + }).getIndexFromLoc({ line: 1, column: 0 }), + 0, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 0, + column: 1, + }, + }, + }, + text: "", + }).getIndexFromLoc({ line: 0, column: 1 }), + 0, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 0, + column: 0, + }, + }, + }, + text: "", + }).getIndexFromLoc({ line: 0, column: 0 }), + 0, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + text: "", + }).getIndexFromLoc({ line: 1, column: 1 }), + 0, + ); + }); + + it("should handle text with only line breaks", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 0, + }, + }, + }; + 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()", () => { it("should return a range object when a range property is present", () => { const ast = { @@ -249,5 +1955,71 @@ 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", // Cannot use `message` here because it behaves differently in other runtimes, such as Bun. + }, + ); + + assert.throws( + () => { + sourceCode.lines.push("qux"); + }, + { + name: "TypeError", // Cannot use `message` here because it behaves differently in other runtimes, such as Bun. + }, + ); + }); + }); }); }); diff --git a/packages/plugin-kit/tests/types/types.test.ts b/packages/plugin-kit/tests/types/types.test.ts index 4ca94fd2..0f2eaa0d 100644 --- a/packages/plugin-kit/tests/types/types.test.ts +++ b/packages/plugin-kit/tests/types/types.test.ts @@ -87,6 +87,8 @@ 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.getIndexFromLoc({ line: 1, column: 0 }) satisfies number; sourceCode.getParent({}) satisfies object | undefined; sourceCode.getRange({}) satisfies SourceRange; sourceCode.getText() satisfies string; @@ -141,6 +143,11 @@ sourceCodeWithOptions.getAncestors({ value: "" }) satisfies { value: string; }[] satisfies CustomOptions["SyntaxElementWithLoc"][]; sourceCodeWithOptions.getLoc({ value: "" }) satisfies SourceLocation; +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; @@ -153,6 +160,10 @@ 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.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({});