From 2225f630284b601d4cfc4ecc19148121d6e11a3f Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 22 Jul 2024 13:40:38 -0400 Subject: [PATCH] feat: Add getLoc and getRange to JSONSourceCode (#13) * feat: Add getLoc and getRange to JSONSourceCode * Add tests --- package.json | 2 +- src/languages/json-language.js | 9 ++-- src/languages/json-source-code.js | 46 ++++++++++++++------ tests/languages/json-source-code.test.js | 55 ++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 0e0125b..9265dbc 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@humanwhocodes/momoa": "^3.1.1" }, "devDependencies": { - "@eslint/core": "^0.1.0", + "@eslint/core": "^0.3.0", "@types/eslint": "^8.56.10", "c8": "^9.1.0", "eslint": "^9.6.0", diff --git a/src/languages/json-language.js b/src/languages/json-language.js index 5cb87bc..9e95d48 100644 --- a/src/languages/json-language.js +++ b/src/languages/json-language.js @@ -16,10 +16,11 @@ import { visitorKeys } from "@humanwhocodes/momoa"; //----------------------------------------------------------------------------- /** @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode */ +/** @typedef {import("@humanwhocodes/momoa").Node} JSONNode */ /** @typedef {import("@eslint/core").Language} Language */ +/** @typedef {import("@eslint/core").OkParseResult} OkParseResult */ +/** @typedef {import("@eslint/core").ParseResult} ParseResult */ /** @typedef {import("@eslint/core").File} File */ -/** @typedef {import("@eslint/core").ParseResult} ParseResult */ -/** @typedef {import("@eslint/core").SyntaxElement} SyntaxElement */ //----------------------------------------------------------------------------- // Exports @@ -111,7 +112,7 @@ export class JSONLanguage { return { ok: true, - ast: /** @type {DocumentNode & SyntaxElement} */ (root), + ast: root, }; } catch (ex) { // error messages end with (line:column) so we strip that off for ESLint @@ -135,7 +136,7 @@ export class JSONLanguage { /** * Creates a new `JSONSourceCode` object from the given information. * @param {File} file The virtual file to create a `JSONSourceCode` object from. - * @param {ParseResult} parseResult The result returned from `parse()`. + * @param {OkParseResult} parseResult The result returned from `parse()`. * @returns {JSONSourceCode} The new `JSONSourceCode` object. */ createSourceCode(file, parseResult) { diff --git a/src/languages/json-source-code.js b/src/languages/json-source-code.js index d207815..bff8553 100644 --- a/src/languages/json-source-code.js +++ b/src/languages/json-source-code.js @@ -16,13 +16,12 @@ import { iterator } from "@humanwhocodes/momoa"; /** @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode */ /** @typedef {import("@humanwhocodes/momoa").Node} JSONNode */ /** @typedef {import("@humanwhocodes/momoa").Token} JSONToken */ -/** @typedef {import("@eslint/core").SyntaxElement} SyntaxElement */ -/** @typedef {import("@eslint/core").Language} Language */ +/** @typedef {import("@eslint/core").SourceRange} SourceRange */ +/** @typedef {import("@eslint/core").SourceLocation} SourceLocation */ /** @typedef {import("@eslint/core").File} File */ /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */ -/** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */ /** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */ -/** @typedef {import("@eslint/core").ParseResult} ParseResult */ +/** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */ //----------------------------------------------------------------------------- // Helpers @@ -50,7 +49,7 @@ class JSONTraversalStep { /** * The target of the step. - * @type {JSONNode & SyntaxElement} + * @type {JSONNode} */ target; @@ -69,7 +68,7 @@ class JSONTraversalStep { /** * Creates a new instance. * @param {Object} options The options for the step. - * @param {JSONNode & SyntaxElement} options.target The target of the step. + * @param {JSONNode} options.target The target of the step. * @param {1|2} options.phase The phase of the step. * @param {Array} options.args The arguments of the step. */ @@ -109,7 +108,7 @@ export class JSONSourceCode { /** * The AST of the source code. - * @type {DocumentNode & SyntaxElement} + * @type {DocumentNode} */ ast; @@ -129,7 +128,7 @@ export class JSONSourceCode { * Creates a new instance. * @param {Object} options The options for the instance. * @param {string} options.text The source code text. - * @param {DocumentNode & SyntaxElement} options.ast The root AST node. + * @param {DocumentNode} options.ast The root AST node. */ constructor({ text, ast }) { this.ast = ast; @@ -139,6 +138,28 @@ export class JSONSourceCode { ); } + /* eslint-disable class-methods-use-this -- Required to complete interface. */ + + /** + * Returns the loc information for the given node or token. + * @param {JSONNode|JSONToken} nodeOrToken The node or token to get the loc information for. + * @returns {SourceLocation} The loc information for the node or token. + */ + getLoc(nodeOrToken) { + return nodeOrToken.loc; + } + + /** + * Returns the range information for the given node or token. + * @param {JSONNode|JSONToken} nodeOrToken The node or token to get the range information for. + * @returns {SourceRange} The range information for the node or token. + */ + getRange(nodeOrToken) { + return nodeOrToken.range; + } + + /* eslint-enable class-methods-use-this -- Required to complete interface. */ + /** * Returns the parent of the given node. * @param {JSONNode} node The node to get the parent of. @@ -205,7 +226,7 @@ export class JSONSourceCode { /** * Traverse the source code and return the steps that were taken. - * @returns {Iterable} The steps that were taken while traversing the source code. + * @returns {Iterable} The steps that were taken while traversing the source code. */ traverse() { // Because the AST doesn't mutate, we can cache the steps @@ -213,15 +234,14 @@ export class JSONSourceCode { return this.#steps.values(); } + /** @type {Array} */ const steps = (this.#steps = []); - for (const { node, parent, phase } of iterator( - /** @type {DocumentNode} */ (this.ast), - )) { + for (const { node, parent, phase } of iterator(this.ast)) { this.#parents.set(node, parent); steps.push( new JSONTraversalStep({ - target: /** @type {JSONNode & SyntaxElement} */ (node), + target: node, phase: phase === "enter" ? 1 : 2, args: [node, parent], }), diff --git a/tests/languages/json-source-code.test.js b/tests/languages/json-source-code.test.js index 261bb08..a739cff 100644 --- a/tests/languages/json-source-code.test.js +++ b/tests/languages/json-source-code.test.js @@ -52,6 +52,61 @@ describe("JSONSourceCode", () => { }); }); + describe("getLoc()", () => { + it("should return the loc property of a node", () => { + const loc = { + start: { + line: 1, + column: 1, + offset: 0, + }, + end: { + line: 1, + column: 2, + offset: 1, + }, + }; + const ast = { + type: "Document", + body: { + type: "Object", + properties: [], + }, + tokens: [], + loc, + }; + const text = "{}"; + const sourceCode = new JSONSourceCode({ + text, + ast, + }); + + assert.strictEqual(sourceCode.getLoc(ast), loc); + }); + }); + + describe("getRange()", () => { + it("should return the range property of a node", () => { + const range = [0, 1]; + const ast = { + type: "Document", + body: { + type: "Object", + properties: [], + }, + tokens: [], + range, + }; + const text = "{}"; + const sourceCode = new JSONSourceCode({ + text, + ast, + }); + + assert.strictEqual(sourceCode.getRange(ast), range); + }); + }); + describe("comments", () => { it("should contain an empty array when parsing JSON", () => { const file = { body: "{}", path: "test.json" };