diff --git a/package.json b/package.json index 9e37f55..86f0128 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ ], "license": "Apache-2.0", "dependencies": { + "@eslint/plugin-kit": "^0.1.0", "@humanwhocodes/momoa": "^3.2.0" }, "devDependencies": { diff --git a/src/languages/json-source-code.js b/src/languages/json-source-code.js index bff8553..5503698 100644 --- a/src/languages/json-source-code.js +++ b/src/languages/json-source-code.js @@ -8,6 +8,7 @@ //----------------------------------------------------------------------------- import { iterator } from "@humanwhocodes/momoa"; +import { VisitNodeStep, TextSourceCodeBase } from "@eslint/plugin-kit"; //----------------------------------------------------------------------------- // Types @@ -29,41 +30,13 @@ import { iterator } from "@humanwhocodes/momoa"; /** * A class to represent a step in the traversal process. - * @implements {VisitTraversalStep} */ -class JSONTraversalStep { - /** - * The type of the step. - * @type {"visit"} - * @readonly - */ - type = "visit"; - - /** - * The kind of the step. Represents the same data as the `type` property - * but it's a number for performance. - * @type {1} - * @readonly - */ - kind = 1; - +class JSONTraversalStep extends VisitNodeStep { /** * The target of the step. * @type {JSONNode} */ - target; - - /** - * The phase of the step. - * @type {1|2} - */ - phase; - - /** - * The arguments of the step. - * @type {Array} - */ - args; + target = undefined; /** * Creates a new instance. @@ -73,9 +46,9 @@ class JSONTraversalStep { * @param {Array} options.args The arguments of the step. */ constructor({ target, phase, args }) { + super({ target, phase, args }); + this.target = target; - this.phase = phase; - this.args = args; } } @@ -85,9 +58,8 @@ class JSONTraversalStep { /** * JSON Source Code Object - * @implements {TextSourceCode} */ -export class JSONSourceCode { +export class JSONSourceCode extends TextSourceCodeBase { /** * Cached traversal steps. * @type {Array|undefined} @@ -100,23 +72,11 @@ export class JSONSourceCode { */ #parents = new WeakMap(); - /** - * The lines of text in the source code. - * @type {Array} - */ - #lines; - /** * The AST of the source code. * @type {DocumentNode} */ - ast; - - /** - * The text of the source code. - * @type {string} - */ - text; + ast = undefined; /** * The comment node in the source code. @@ -131,35 +91,13 @@ export class JSONSourceCode { * @param {DocumentNode} options.ast The root AST node. */ constructor({ text, ast }) { + super({ text, ast }); this.ast = ast; - this.text = text; - this.comments = ast.tokens.filter(token => - token.type.endsWith("Comment"), - ); - } - - /* 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; + this.comments = ast.tokens + ? ast.tokens.filter(token => token.type.endsWith("Comment")) + : []; } - /* 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. @@ -169,61 +107,6 @@ export class JSONSourceCode { return this.#parents.get(node); } - /** - * Gets all the ancestors of a given node - * @param {JSONNode} node The node - * @returns {Array} All the ancestor nodes in the AST, not including the provided node, starting - * from the root node at index 0 and going inwards to the parent node. - * @throws {TypeError} When `node` is missing. - */ - getAncestors(node) { - if (!node) { - throw new TypeError("Missing required argument: node."); - } - - const ancestorsStartingAtParent = []; - - for ( - let ancestor = this.#parents.get(node); - ancestor; - ancestor = this.#parents.get(ancestor) - ) { - ancestorsStartingAtParent.push(ancestor); - } - - return ancestorsStartingAtParent.reverse(); - } - - /** - * Gets the source code for the given node. - * @param {JSONNode} [node] The AST node to get the text for. - * @param {number} [beforeCount] The number of characters before the node to retrieve. - * @param {number} [afterCount] The number of characters after the node to retrieve. - * @returns {string} The text representing the AST node. - * @public - */ - getText(node, beforeCount, afterCount) { - if (node) { - return this.text.slice( - Math.max(node.range[0] - (beforeCount || 0), 0), - node.range[1] + (afterCount || 0), - ); - } - return this.text; - } - - /** - * Gets the entire source text split into an array of lines. - * @returns {Array} The source text as an array of lines. - * @public - */ - get lines() { - if (!this.#lines) { - this.#lines = this.text.split(/\r?\n/gu); - } - return this.#lines; - } - /** * Traverse the source code and return the steps that were taken. * @returns {Iterable} The steps that were taken while traversing the source code. @@ -238,7 +121,10 @@ export class JSONSourceCode { const steps = (this.#steps = []); for (const { node, parent, phase } of iterator(this.ast)) { - this.#parents.set(node, parent); + if (parent) { + this.#parents.set(node, parent); + } + steps.push( new JSONTraversalStep({ target: node, diff --git a/tests/languages/json-source-code.test.js b/tests/languages/json-source-code.test.js index a739cff..ea87d1b 100644 --- a/tests/languages/json-source-code.test.js +++ b/tests/languages/json-source-code.test.js @@ -142,7 +142,7 @@ describe("JSONSourceCode", () => { }); }); - describe("get lines", () => { + describe("lines", () => { it("should return an array of lines", () => { const file = { body: "{\n//test\n}", path: "test.jsonc" }; const language = new JSONLanguage({ mode: "jsonc" }); @@ -155,4 +155,124 @@ describe("JSONSourceCode", () => { assert.deepStrictEqual(sourceCode.lines, ["{", "//test", "}"]); }); }); + + describe("getParent()", () => { + it("should return the parent node for a given node", () => { + const ast = { + type: "Document", + body: { + type: "Object", + properties: [], + }, + tokens: [], + }; + const text = "{}"; + const sourceCode = new JSONSourceCode({ + text, + ast, + }); + const node = ast.body; + + // call traverse to initialize the parent map + sourceCode.traverse(); + + assert.strictEqual(sourceCode.getParent(node), ast); + }); + + it("should return the parent node for a deeply nested node", () => { + const ast = { + type: "Document", + body: { + type: "Object", + members: [ + { + type: "Member", + name: { + type: "Identifier", + name: "foo", + }, + value: { + type: "Object", + properties: [], + }, + }, + ], + }, + tokens: [], + }; + const text = '{"foo":{}}'; + const sourceCode = new JSONSourceCode({ + text, + ast, + }); + const node = ast.body.members[0].value; + + // call traverse to initialize the parent map + sourceCode.traverse(); + + assert.strictEqual(sourceCode.getParent(node), ast.body.members[0]); + }); + }); + + describe("getAncestors()", () => { + it("should return an array of ancestors for a given node", () => { + const ast = { + type: "Document", + body: { + type: "Object", + members: [], + }, + tokens: [], + }; + const text = "{}"; + const sourceCode = new JSONSourceCode({ + text, + ast, + }); + const node = ast.body; + + // call traverse to initialize the parent map + sourceCode.traverse(); + + assert.deepStrictEqual(sourceCode.getAncestors(node), [ast]); + }); + + it("should return an array of ancestors for a deeply nested node", () => { + const ast = { + type: "Document", + body: { + type: "Object", + members: [ + { + type: "Member", + name: { + type: "Identifier", + name: "foo", + }, + value: { + type: "Object", + members: [], + }, + }, + ], + }, + tokens: [], + }; + const text = '{"foo":{}}'; + const sourceCode = new JSONSourceCode({ + text, + ast, + }); + const node = ast.body.members[0].value; + + // call traverse to initialize the parent map + sourceCode.traverse(); + + assert.deepStrictEqual(sourceCode.getAncestors(node), [ + ast, + ast.body, + ast.body.members[0], + ]); + }); + }); });