From 24b6100f98cf632d507c61d36fe2e362808a7153 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 22 May 2024 18:09:32 +0530 Subject: [PATCH 1/8] feat: add transform for v9 migration --- lib/v9-migration/v9-migration.js | 159 +++++++++++++++++++++++++ package.json | 1 + tests/lib/v9-migration/v9-migration.js | 100 ++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 lib/v9-migration/v9-migration.js create mode 100644 tests/lib/v9-migration/v9-migration.js diff --git a/lib/v9-migration/v9-migration.js b/lib/v9-migration/v9-migration.js new file mode 100644 index 0000000..2ba99d1 --- /dev/null +++ b/lib/v9-migration/v9-migration.js @@ -0,0 +1,159 @@ +/** + * @fileoverview Transform that migrates an ESLint API from v8 to v9 + * Refer to https://github.com/eslint/eslint-transforms/issues/25 for more information + * + * @author Nitin Kumar + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Formats a message string with ANSI escape codes to display it in yellow with bold styling in the terminal. + * @param {string} message The message to be formatted. + * @returns {string} The formatted message string. + */ +function formatBoldYellow(message) { + return `\u001b[1m\u001b[33m${message}\u001b[39m\u001b[22m`; +} + +const contextMethodsToPropertyMapping = { + getSourceCode: "sourceCode", + getFilename: "filename", + getPhysicalFilename: "physicalFilename", + getCwd: "cwd" +}; + +const contextToSourceCodeMapping = { + getSource: "getText", + getSourceLines: "getLines", + getAllComments: "getAllComments", + getNodeByRangeIndex: "getNodeByRangeIndex", + getComments: "getComments", + getCommentsBefore: "getCommentsBefore", + getCommentsAfter: "getCommentsAfter", + getCommentsInside: "getCommentsInside", + getJSDocComment: "getJSDocComment", + getFirstToken: "getFirstToken", + getFirstTokens: "getFirstTokens", + getLastToken: "getLastToken", + getLastTokens: "getLastTokens", + getTokenAfter: "getTokenAfter", + getTokenBefore: "getTokenBefore", + getTokenByRangeStart: "getTokenByRangeStart", + getTokens: "getTokens", + getTokensAfter: "getTokensAfter", + getTokensBefore: "getTokensBefore", + getTokensBetween: "getTokensBetween", + parserServices: "parserServices", + getDeclaredVariables: "getDeclaVariables" +}; + +//------------------------------------------------------------------------------ +// Transform Definition +//------------------------------------------------------------------------------ + +/** + * Transforms an ESLint rule from the old format to the new format. + * @param {Object} fileInfo holds information about the currently processed file. + * * @param {Object} api holds the jscodeshift API + * @returns {string} the new source code, after being transformed. + */ + +module.exports = function(fileInfo, api) { + const j = api.jscodeshift; + const root = j(fileInfo.source); + + // Update context methods + // context.getSourceCode() -> context.sourceCode ?? context.getSourceCode() + root.find(j.CallExpression, { + callee: { + object: { + type: "Identifier", + name: "context" + }, + property: { + type: "Identifier", + name: name => + Object.keys(contextMethodsToPropertyMapping).includes(name) + } + } + }).replaceWith(({ node }) => { + const method = node.callee.property.name; + const args = node.arguments; + + return j.logicalExpression( + "??", + j.memberExpression( + j.identifier("context"), + j.identifier(contextMethodsToPropertyMapping[method]) + ), + j.callExpression( + j.memberExpression( + j.identifier("context"), + j.identifier(method) + ), + [...args] + ) + ); + }); + + // Move context methods to SourceCode + // context.getSource() -> context.sourceCode.getText() + root.find(j.MemberExpression, { + object: { + type: "Identifier", + name: "context" + }, + property: { + type: "Identifier", + name: name => + Object.keys(contextToSourceCodeMapping).includes(name) + } + }).replaceWith(({ node }) => { + const method = node.property.name; + + if (method === "getComments") { + // eslint-disable-next-line no-console -- This is an intentional warning message + console.warn( + formatBoldYellow( + `${fileInfo.path}:${node.loc.start.line}:${node.loc.start.column} The "getComments()" method has been removed. Please use "getCommentsBefore()", "getCommentsAfter()", or "getCommentsInside()" instead. https://eslint.org/docs/latest/use/migrate-to-9.0.0#-removed-sourcecodegetcomments` + ) + ); + return node; + } + + node.property.name = contextToSourceCodeMapping[method]; + node.object.name = "context.sourceCode"; + + return node; + }); + + // Warn for codePath.currentSegments + root.find(j.Property, { + key: { + type: "Identifier", + name: name => + name === "onCodePathStart" || name === "onCodePathEnd" + } + }) + .find(j.MemberExpression, { + property: { + type: "Identifier", + name: "currentSegments" + } + }) + .forEach(({ node }) => { + // eslint-disable-next-line no-console -- This is an intentional warning message + console.warn( + formatBoldYellow( + `${fileInfo.path}:${node.loc.start.line}:${node.loc.start.column} The "CodePath#currentSegments" property has been removed and it can't be migrated automatically.\nPlease read https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/#codepath%23currentsegments for more information.\n` + ) + ); + }); + + return root.toSource(); +}; diff --git a/package.json b/package.json index 5bb5cdd..5aea0d7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "test": "mocha ./tests/lib/**/*.js" }, "devDependencies": { + "@hypermod/utils": "^0.4.2", "eslint": "^9.2.0", "eslint-config-eslint": "^10.0.0", "eslint-release": "^1.0.0", diff --git a/tests/lib/v9-migration/v9-migration.js b/tests/lib/v9-migration/v9-migration.js new file mode 100644 index 0000000..43d58a7 --- /dev/null +++ b/tests/lib/v9-migration/v9-migration.js @@ -0,0 +1,100 @@ +/** + * @fileoverview Tests for v9-migration transform. + * @author Nitin Kumar + * MIT License + */ + +"use strict"; + +const { applyTransform } = require("@hypermod/utils"); +const assert = require("assert"); + +const v9MigrationTransform = require("../../../lib/v9-migration/v9-migration"); + +describe("v9 migration transform", () => { + it("should migrate deprecated context methods to new properties", async () => { + const result = await applyTransform( + v9MigrationTransform, + ` + const sourceCode = context.getSourceCode(); + const cwd = context.getCwd(); + const filename = context.getFilename(); + const physicalFilename = context.getPhysicalFilename(); + ` + ); + + assert.strictEqual( + result, + ` + const sourceCode = context.sourceCode ?? context.getSourceCode(); + const cwd = context.cwd ?? context.getCwd(); + const filename = context.filename ?? context.getFilename(); + const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename(); + `.trim() + ); + }); + + it("should migrate deprecated context methods to SourceCode", async () => { + const result = await applyTransform( + v9MigrationTransform, + ` + const sourceCode = context.getSource(); + const sourceLines = context.getSourceLines(); + const allComments = context.getAllComments(); + const nodeByRangeIndex = context.getNodeByRangeIndex(); + const commentsBefore = context.getCommentsBefore(nodeOrToken); + const commentsAfter = context.getCommentsAfter(nodeOrToken); + const commentsInside = context.getCommentsInside(nodeOrToken); + ` + ); + + assert.strictEqual( + result, + ` + const sourceCode = context.sourceCode.getText(); + const sourceLines = context.sourceCode.getLines(); + const allComments = context.sourceCode.getAllComments(); + const nodeByRangeIndex = context.sourceCode.getNodeByRangeIndex(); + const commentsBefore = context.sourceCode.getCommentsBefore(nodeOrToken); + const commentsAfter = context.sourceCode.getCommentsAfter(nodeOrToken); + `.trim() + ); + }); + + it.only("should warn about codePath.currentSegments", async () => { + await applyTransform( + v9MigrationTransform, + ` + module.exports = { + meta: { + docs: {}, + schema: [] + }, + create(context) { + return { + onCodePathStart(codePath, node) { + const currentSegments = codePath.currentSegments(); + }, + + onCodePathEnd(endCodePath, node) { + const currentSegments = endCodePath.currentSegments(); + }, + }; + } + } + ` + ); + + // assert.strictEqual( + // result, + // ` + // const sourceCode = context.sourceCode.getText(); + // const sourceLines = context.sourceCode.getLines(); + // const allComments = context.sourceCode.getAllComments(); + // const nodeByRangeIndex = context.sourceCode.getNodeByRangeIndex(); + // const commentsBefore = context.sourceCode.getCommentsBefore(nodeOrToken); + // const commentsAfter = context.sourceCode.getCommentsAfter(nodeOrToken); + // `.trim() + // ); + }); +}); From b16fd613bf258eae061eabc984b9926aa4e46b97 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 22 May 2024 19:44:44 +0530 Subject: [PATCH 2/8] chore: add test cases --- lib/v9-migration/v9-migration.js | 11 +++- package.json | 3 +- tests/lib/v9-migration/v9-migration.js | 88 +++++++++++++++++--------- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/lib/v9-migration/v9-migration.js b/lib/v9-migration/v9-migration.js index 2ba99d1..6ee7a5f 100644 --- a/lib/v9-migration/v9-migration.js +++ b/lib/v9-migration/v9-migration.js @@ -7,6 +7,11 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ +const path = require("path"); + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -120,7 +125,7 @@ module.exports = function(fileInfo, api) { // eslint-disable-next-line no-console -- This is an intentional warning message console.warn( formatBoldYellow( - `${fileInfo.path}:${node.loc.start.line}:${node.loc.start.column} The "getComments()" method has been removed. Please use "getCommentsBefore()", "getCommentsAfter()", or "getCommentsInside()" instead. https://eslint.org/docs/latest/use/migrate-to-9.0.0#-removed-sourcecodegetcomments` + `${path.relative(process.cwd(), fileInfo.path)}:${node.loc.start.line}:${node.loc.start.column} The "getComments()" method has been removed. Please use "getCommentsBefore()", "getCommentsAfter()", or "getCommentsInside()" instead. https://eslint.org/docs/latest/use/migrate-to-9.0.0#-removed-sourcecodegetcomments` ) ); return node; @@ -150,10 +155,12 @@ module.exports = function(fileInfo, api) { // eslint-disable-next-line no-console -- This is an intentional warning message console.warn( formatBoldYellow( - `${fileInfo.path}:${node.loc.start.line}:${node.loc.start.column} The "CodePath#currentSegments" property has been removed and it can't be migrated automatically.\nPlease read https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/#codepath%23currentsegments for more information.\n` + `${path.relative(process.cwd(), fileInfo.path)}:${node.loc.start.line}:${node.loc.start.column} The "CodePath#currentSegments" property has been removed and it can't be migrated automatically.\nPlease read https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/#codepath%23currentsegments for more information.\n` ) ); }); return root.toSource(); }; + +module.exports.formatBoldYellow = formatBoldYellow; diff --git a/package.json b/package.json index 5aea0d7..4aabdcc 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "eslint-config-eslint": "^10.0.0", "eslint-release": "^1.0.0", "globals": "^15.2.0", - "mocha": "^10.4.0" + "mocha": "^10.4.0", + "sinon": "^18.0.0" }, "keywords": [ "javascript", diff --git a/tests/lib/v9-migration/v9-migration.js b/tests/lib/v9-migration/v9-migration.js index 43d58a7..4da5f11 100644 --- a/tests/lib/v9-migration/v9-migration.js +++ b/tests/lib/v9-migration/v9-migration.js @@ -6,9 +6,10 @@ "use strict"; +const path = require("path"); const { applyTransform } = require("@hypermod/utils"); const assert = require("assert"); - +const sinon = require("sinon"); const v9MigrationTransform = require("../../../lib/v9-migration/v9-migration"); describe("v9 migration transform", () => { @@ -57,44 +58,71 @@ describe("v9 migration transform", () => { const nodeByRangeIndex = context.sourceCode.getNodeByRangeIndex(); const commentsBefore = context.sourceCode.getCommentsBefore(nodeOrToken); const commentsAfter = context.sourceCode.getCommentsAfter(nodeOrToken); + const commentsInside = context.sourceCode.getCommentsInside(nodeOrToken); `.trim() ); }); - it.only("should warn about codePath.currentSegments", async () => { + it("should warn about context.getComments()", async () => { + const spy = sinon.spy(console, "warn"); + await applyTransform( v9MigrationTransform, - ` - module.exports = { - meta: { - docs: {}, - schema: [] - }, - create(context) { - return { - onCodePathStart(codePath, node) { - const currentSegments = codePath.currentSegments(); - }, - - onCodePathEnd(endCodePath, node) { - const currentSegments = endCodePath.currentSegments(); - }, - }; + { + source: "const comments = context.getComments();", + path: path.resolve(__dirname, __filename) + } + ); + + assert.strictEqual(spy.callCount, 1); + assert.match( + spy.args[0][0], + /1:17 The "getComments\(\)" method has been removed. Please use "getCommentsBefore\(\)", "getCommentsAfter\(\)", or "getCommentsInside\(\)" instead/u + ); + + spy.restore(); + }); + + it("should warn about codePath.currentSegments", async () => { + const spy = sinon.spy(console, "warn"); + const filePath = path.resolve(__dirname, __filename); + + await applyTransform( + v9MigrationTransform, + { + path: filePath, + source: ` + module.exports = { + meta: { + docs: {}, + schema: [] + }, + create(context) { + return { + onCodePathStart(codePath, node) { + const currentSegments = codePath.currentSegments(); + }, + + onCodePathEnd(endCodePath, node) { + const currentSegments = endCodePath.currentSegments(); + }, + }; + } } + ` } - ` ); - // assert.strictEqual( - // result, - // ` - // const sourceCode = context.sourceCode.getText(); - // const sourceLines = context.sourceCode.getLines(); - // const allComments = context.sourceCode.getAllComments(); - // const nodeByRangeIndex = context.sourceCode.getNodeByRangeIndex(); - // const commentsBefore = context.sourceCode.getCommentsBefore(nodeOrToken); - // const commentsAfter = context.sourceCode.getCommentsAfter(nodeOrToken); - // `.trim() - // ); + assert.strictEqual(spy.callCount, 2); + assert.match( + spy.args[0][0], + /10:56 The "CodePath#currentSegments" property has been removed and it can't be migrated automatically/u + ); + assert.match( + spy.args[1][0], + /14:56 The "CodePath#currentSegments" property has been removed and it can't be migrated automatically/u + ); + + spy.restore(); }); }); From c61b72ffef167b94177d5484880e237288cde33a Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 22 May 2024 19:46:03 +0530 Subject: [PATCH 3/8] chore: remove unwanted changes --- lib/v9-migration/v9-migration.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/v9-migration/v9-migration.js b/lib/v9-migration/v9-migration.js index 6ee7a5f..5d6fa1b 100644 --- a/lib/v9-migration/v9-migration.js +++ b/lib/v9-migration/v9-migration.js @@ -162,5 +162,3 @@ module.exports = function(fileInfo, api) { return root.toSource(); }; - -module.exports.formatBoldYellow = formatBoldYellow; From 06417880c5dcbfcf560dcbc04625c44f30021c86 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Thu, 23 May 2024 06:45:18 +0530 Subject: [PATCH 4/8] chore: fix tests on windows --- tests/lib/new-rule-format/new-rule-format.js | 14 +------------- tests/lib/v9-migration/v9-migration.js | 13 +++++++------ tests/utils/index.js | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 tests/utils/index.js diff --git a/tests/lib/new-rule-format/new-rule-format.js b/tests/lib/new-rule-format/new-rule-format.js index dd6af7d..7e62cd4 100644 --- a/tests/lib/new-rule-format/new-rule-format.js +++ b/tests/lib/new-rule-format/new-rule-format.js @@ -9,23 +9,11 @@ const jscodeshift = require("jscodeshift"); const fs = require("fs"); -const os = require("os"); const path = require("path"); const assert = require("assert"); const newRuleFormatTransform = require("../../../lib/new-rule-format/new-rule-format"); - -/** - * Returns a new string with all the EOL markers from the string passed in - * replaced with the Operating System specific EOL marker. - * Useful for guaranteeing two transform outputs have the same EOL marker format. - * @param {string} input the string which will have its EOL markers replaced - * @returns {string} a new string with all EOL markers replaced - * @private - */ -function normalizeLineEndngs(input) { - return input.replace(/(\r\n|\n|\r)/gmu, os.EOL); -} +const { normalizeLineEndngs } = require("../../utils"); /** * Run a transform against a fixture file and compare results with expected output. diff --git a/tests/lib/v9-migration/v9-migration.js b/tests/lib/v9-migration/v9-migration.js index 4da5f11..95401b3 100644 --- a/tests/lib/v9-migration/v9-migration.js +++ b/tests/lib/v9-migration/v9-migration.js @@ -10,6 +10,7 @@ const path = require("path"); const { applyTransform } = require("@hypermod/utils"); const assert = require("assert"); const sinon = require("sinon"); +const { normalizeLineEndngs } = require("../../utils"); const v9MigrationTransform = require("../../../lib/v9-migration/v9-migration"); describe("v9 migration transform", () => { @@ -25,13 +26,13 @@ describe("v9 migration transform", () => { ); assert.strictEqual( - result, - ` + normalizeLineEndngs(result), + normalizeLineEndngs(` const sourceCode = context.sourceCode ?? context.getSourceCode(); const cwd = context.cwd ?? context.getCwd(); const filename = context.filename ?? context.getFilename(); const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename(); - `.trim() + `.trim()) ); }); @@ -50,8 +51,8 @@ describe("v9 migration transform", () => { ); assert.strictEqual( - result, - ` + normalizeLineEndngs(result), + normalizeLineEndngs(` const sourceCode = context.sourceCode.getText(); const sourceLines = context.sourceCode.getLines(); const allComments = context.sourceCode.getAllComments(); @@ -59,7 +60,7 @@ describe("v9 migration transform", () => { const commentsBefore = context.sourceCode.getCommentsBefore(nodeOrToken); const commentsAfter = context.sourceCode.getCommentsAfter(nodeOrToken); const commentsInside = context.sourceCode.getCommentsInside(nodeOrToken); - `.trim() + `.trim()) ); }); diff --git a/tests/utils/index.js b/tests/utils/index.js new file mode 100644 index 0000000..7fa993c --- /dev/null +++ b/tests/utils/index.js @@ -0,0 +1,19 @@ +"use strict"; + +const os = require("os"); + +/** + * Returns a new string with all the EOL markers from the string passed in + * replaced with the Operating System specific EOL marker. + * Useful for guaranteeing two transform outputs have the same EOL marker format. + * @param {string} input the string which will have its EOL markers replaced + * @returns {string} a new string with all EOL markers replaced + * @private + */ +function normalizeLineEndngs(input) { + return input.replace(/(\r\n|\n|\r)/gmu, os.EOL); +} + +module.exports = { + normalizeLineEndngs +}; \ No newline at end of file From f412c0b8d47eb6c11c0c871865764ab4cc25c06a Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 25 May 2024 15:10:31 +0530 Subject: [PATCH 5/8] feat: cover more cases --- lib/v9-migration/v9-migration.js | 164 ---------- lib/v9-rule-migration/v9-rule-migration.js | 280 +++++++++++++++++ tests/lib/v9-migration/v9-migration.js | 129 -------- .../v9-rule-migration/v9-rule-migration.js | 286 ++++++++++++++++++ tests/utils/index.js | 2 +- 5 files changed, 567 insertions(+), 294 deletions(-) delete mode 100644 lib/v9-migration/v9-migration.js create mode 100644 lib/v9-rule-migration/v9-rule-migration.js delete mode 100644 tests/lib/v9-migration/v9-migration.js create mode 100644 tests/lib/v9-rule-migration/v9-rule-migration.js diff --git a/lib/v9-migration/v9-migration.js b/lib/v9-migration/v9-migration.js deleted file mode 100644 index 5d6fa1b..0000000 --- a/lib/v9-migration/v9-migration.js +++ /dev/null @@ -1,164 +0,0 @@ -/** - * @fileoverview Transform that migrates an ESLint API from v8 to v9 - * Refer to https://github.com/eslint/eslint-transforms/issues/25 for more information - * - * @author Nitin Kumar - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ -const path = require("path"); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Formats a message string with ANSI escape codes to display it in yellow with bold styling in the terminal. - * @param {string} message The message to be formatted. - * @returns {string} The formatted message string. - */ -function formatBoldYellow(message) { - return `\u001b[1m\u001b[33m${message}\u001b[39m\u001b[22m`; -} - -const contextMethodsToPropertyMapping = { - getSourceCode: "sourceCode", - getFilename: "filename", - getPhysicalFilename: "physicalFilename", - getCwd: "cwd" -}; - -const contextToSourceCodeMapping = { - getSource: "getText", - getSourceLines: "getLines", - getAllComments: "getAllComments", - getNodeByRangeIndex: "getNodeByRangeIndex", - getComments: "getComments", - getCommentsBefore: "getCommentsBefore", - getCommentsAfter: "getCommentsAfter", - getCommentsInside: "getCommentsInside", - getJSDocComment: "getJSDocComment", - getFirstToken: "getFirstToken", - getFirstTokens: "getFirstTokens", - getLastToken: "getLastToken", - getLastTokens: "getLastTokens", - getTokenAfter: "getTokenAfter", - getTokenBefore: "getTokenBefore", - getTokenByRangeStart: "getTokenByRangeStart", - getTokens: "getTokens", - getTokensAfter: "getTokensAfter", - getTokensBefore: "getTokensBefore", - getTokensBetween: "getTokensBetween", - parserServices: "parserServices", - getDeclaredVariables: "getDeclaVariables" -}; - -//------------------------------------------------------------------------------ -// Transform Definition -//------------------------------------------------------------------------------ - -/** - * Transforms an ESLint rule from the old format to the new format. - * @param {Object} fileInfo holds information about the currently processed file. - * * @param {Object} api holds the jscodeshift API - * @returns {string} the new source code, after being transformed. - */ - -module.exports = function(fileInfo, api) { - const j = api.jscodeshift; - const root = j(fileInfo.source); - - // Update context methods - // context.getSourceCode() -> context.sourceCode ?? context.getSourceCode() - root.find(j.CallExpression, { - callee: { - object: { - type: "Identifier", - name: "context" - }, - property: { - type: "Identifier", - name: name => - Object.keys(contextMethodsToPropertyMapping).includes(name) - } - } - }).replaceWith(({ node }) => { - const method = node.callee.property.name; - const args = node.arguments; - - return j.logicalExpression( - "??", - j.memberExpression( - j.identifier("context"), - j.identifier(contextMethodsToPropertyMapping[method]) - ), - j.callExpression( - j.memberExpression( - j.identifier("context"), - j.identifier(method) - ), - [...args] - ) - ); - }); - - // Move context methods to SourceCode - // context.getSource() -> context.sourceCode.getText() - root.find(j.MemberExpression, { - object: { - type: "Identifier", - name: "context" - }, - property: { - type: "Identifier", - name: name => - Object.keys(contextToSourceCodeMapping).includes(name) - } - }).replaceWith(({ node }) => { - const method = node.property.name; - - if (method === "getComments") { - // eslint-disable-next-line no-console -- This is an intentional warning message - console.warn( - formatBoldYellow( - `${path.relative(process.cwd(), fileInfo.path)}:${node.loc.start.line}:${node.loc.start.column} The "getComments()" method has been removed. Please use "getCommentsBefore()", "getCommentsAfter()", or "getCommentsInside()" instead. https://eslint.org/docs/latest/use/migrate-to-9.0.0#-removed-sourcecodegetcomments` - ) - ); - return node; - } - - node.property.name = contextToSourceCodeMapping[method]; - node.object.name = "context.sourceCode"; - - return node; - }); - - // Warn for codePath.currentSegments - root.find(j.Property, { - key: { - type: "Identifier", - name: name => - name === "onCodePathStart" || name === "onCodePathEnd" - } - }) - .find(j.MemberExpression, { - property: { - type: "Identifier", - name: "currentSegments" - } - }) - .forEach(({ node }) => { - // eslint-disable-next-line no-console -- This is an intentional warning message - console.warn( - formatBoldYellow( - `${path.relative(process.cwd(), fileInfo.path)}:${node.loc.start.line}:${node.loc.start.column} The "CodePath#currentSegments" property has been removed and it can't be migrated automatically.\nPlease read https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/#codepath%23currentsegments for more information.\n` - ) - ); - }); - - return root.toSource(); -}; diff --git a/lib/v9-rule-migration/v9-rule-migration.js b/lib/v9-rule-migration/v9-rule-migration.js new file mode 100644 index 0000000..7b922fb --- /dev/null +++ b/lib/v9-rule-migration/v9-rule-migration.js @@ -0,0 +1,280 @@ +/** + * @fileoverview Transform that migrates an ESLint API from v8 to v9 + * Refer to https://github.com/eslint/eslint-transforms/issues/25 for more information + * + * @author Nitin Kumar + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ +const path = require("path"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Formats a message string with ANSI escape codes to display it in yellow with bold styling in the terminal. + * @param {string} message The message to be formatted. + * @returns {string} The formatted message string. + */ +function formatBoldYellow(message) { + return `\u001b[1m\u001b[33m${message}\u001b[39m\u001b[22m`; +} + +const contextMethodsToPropertyMapping = { + getSourceCode: "sourceCode", + getFilename: "filename", + getPhysicalFilename: "physicalFilename", + getCwd: "cwd" +}; + +const contextToSourceCodeMapping = { + getScope: "getScope", + getAncestors: "getAncestors", + getDeclaredVariables: "getDeclaredVariables", + markVariableAsUsed: "markVariableAsUsed", + getSource: "getText", + getSourceLines: "getLines", + getAllComments: "getAllComments", + getNodeByRangeIndex: "getNodeByRangeIndex", + getComments: "getComments", + getCommentsBefore: "getCommentsBefore", + getCommentsAfter: "getCommentsAfter", + getCommentsInside: "getCommentsInside", + getJSDocComment: "getJSDocComment", + getFirstToken: "getFirstToken", + getFirstTokens: "getFirstTokens", + getLastToken: "getLastToken", + getLastTokens: "getLastTokens", + getTokenAfter: "getTokenAfter", + getTokenBefore: "getTokenBefore", + getTokenByRangeStart: "getTokenByRangeStart", + getTokens: "getTokens", + getTokensAfter: "getTokensAfter", + getTokensBefore: "getTokensBefore", + getTokensBetween: "getTokensBetween", + parserServices: "parserServices" +}; + +const METHODS_WITH_SIGNATURE_CHANGE = new Set([ + "getScope", + "getAncestors", + "markVariableAsUsed", + "getDeclaredVariables" +]); + +/** + * Returns the parent ObjectMethod node + * @param {Node} nodePath The nodePath of the current node + * @returns {Node} The parent ObjectMethod node + */ +function getParentObjectMethod(nodePath) { + const node = nodePath.node; + + if (node.type && node.type === "Property" && node.method) { + return node; + } + return getParentObjectMethod(nodePath.parentPath); +} + +//------------------------------------------------------------------------------ +// Transform Definition +//------------------------------------------------------------------------------ + +/** + * Transforms an ESLint rule from the old format to the new format. + * @param {Object} fileInfo holds information about the currently processed file. + * * @param {Object} api holds the jscodeshift API + * @returns {string} the new source code, after being transformed. + */ + +module.exports = function(fileInfo, api) { + const j = api.jscodeshift; + const root = j(fileInfo.source); + + // Update context methods + // context.getSourceCode() -> context.sourceCode ?? context.getSourceCode() + root.find(j.CallExpression, { + callee: { + object: { + type: "Identifier", + name: "context" + }, + property: { + type: "Identifier", + name: name => + Object.keys(contextMethodsToPropertyMapping).includes(name) + } + } + }).replaceWith(({ node }) => { + const method = node.callee.property.name; + const args = node.arguments; + + // Replace all instances of context.getSourceCode() with sourceCode variable + if (method === "getSourceCode") { + return j.identifier("sourceCode"); + } + + return j.logicalExpression( + "??", + j.memberExpression( + j.identifier("context"), + j.identifier(contextMethodsToPropertyMapping[method]) + ), + j.callExpression( + j.memberExpression( + j.identifier("context"), + j.identifier(method) + ), + [...args] + ) + ); + }); + + // Add a variable declaration for `const sourceCode = context.sourceCode ?? context.getSourceCode()` immediately inside the create() method + root.find(j.Property, { + key: { name: "create" } + }).replaceWith(({ node }) => { + const sourceCodeDeclaration = j.variableDeclaration("const", [ + j.variableDeclarator( + j.identifier("sourceCode"), + j.logicalExpression( + "??", + j.memberExpression( + j.identifier("context"), + j.identifier("sourceCode") + ), + j.callExpression( + j.memberExpression( + j.identifier("context"), + j.identifier("getSourceCode") + ), + [] + ) + ) + ) + ]); + + // Insert the sourceCodeDeclaration at the beginning of the create() method + node.value.body.body.unshift(sourceCodeDeclaration); + + return node; + }); + + // Move context methods to SourceCode + // context.getSource() -> sourceCode.getText() + root.find(j.CallExpression, { + callee: { + type: "MemberExpression", + object: { + type: "Identifier", + name: "context" + }, + property: { + type: "Identifier", + name: name => + Object.keys(contextToSourceCodeMapping).includes(name) + } + } + }).replaceWith(nodePath => { + const node = nodePath.node; + const method = node.callee.property.name; + const args = node.arguments; + + if (method === "getComments") { + // eslint-disable-next-line no-console -- This is an intentional warning message + console.warn( + formatBoldYellow( + `${path.relative(process.cwd(), fileInfo.path)}:${ + node.loc.start.line + }:${ + node.loc.start.column + } The "getComments()" method has been removed. Please use "getCommentsBefore()", "getCommentsAfter()", or "getCommentsInside()" instead. https://eslint.org/docs/latest/use/migrate-to-9.0.0#-removed-sourcecodegetcomments` + ) + ); + return node; + } + + if (METHODS_WITH_SIGNATURE_CHANGE.has(method)) { + const parentObjectMethodNode = getParentObjectMethod(nodePath); + const parentObjectMethodParamName = + parentObjectMethodNode.value.params[0].name; + + // Return the node as is if the method is called with an argument + // context.getScope(node) -> sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); + return j.conditionalExpression( + j.memberExpression( + j.identifier("sourceCode"), + j.identifier(contextToSourceCodeMapping[method]) + ), + j.callExpression( + j.memberExpression( + j.identifier("sourceCode"), + j.identifier(contextToSourceCodeMapping[method]) + ), + [...args, j.identifier(parentObjectMethodParamName)] + ), + j.callExpression( + j.memberExpression( + j.identifier("context"), + j.identifier(method) + ), + [] + ) + ); + } + + node.callee.property.name = contextToSourceCodeMapping[method]; + node.callee.object.name = "sourceCode"; + + return node; + }); + + // Migrate context.parserServices to sourceCode.parserServices + root.find(j.MemberExpression, { + object: { + type: "Identifier", + name: "context" + }, + property: { + type: "Identifier", + name: "parserServices" + } + }).replaceWith(({ node }) => { + node.object.name = "sourceCode"; + return node; + }); + + // Warn for codePath.currentSegments + root.find(j.Property, { + key: { + type: "Identifier", + name: name => + name === "onCodePathStart" || name === "onCodePathEnd" + } + }) + .find(j.MemberExpression, { + property: { + type: "Identifier", + name: "currentSegments" + } + }) + .forEach(({ node }) => { + // eslint-disable-next-line no-console -- This is an intentional warning message + console.warn( + formatBoldYellow( + `${path.relative(process.cwd(), fileInfo.path)}:${ + node.loc.start.line + }:${ + node.loc.start.column + } The "CodePath#currentSegments" property has been removed and it can't be migrated automatically.\nPlease read https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/#codepath%23currentsegments for more information.\n` + ) + ); + }); + + return root.toSource(); +}; diff --git a/tests/lib/v9-migration/v9-migration.js b/tests/lib/v9-migration/v9-migration.js deleted file mode 100644 index 95401b3..0000000 --- a/tests/lib/v9-migration/v9-migration.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @fileoverview Tests for v9-migration transform. - * @author Nitin Kumar - * MIT License - */ - -"use strict"; - -const path = require("path"); -const { applyTransform } = require("@hypermod/utils"); -const assert = require("assert"); -const sinon = require("sinon"); -const { normalizeLineEndngs } = require("../../utils"); -const v9MigrationTransform = require("../../../lib/v9-migration/v9-migration"); - -describe("v9 migration transform", () => { - it("should migrate deprecated context methods to new properties", async () => { - const result = await applyTransform( - v9MigrationTransform, - ` - const sourceCode = context.getSourceCode(); - const cwd = context.getCwd(); - const filename = context.getFilename(); - const physicalFilename = context.getPhysicalFilename(); - ` - ); - - assert.strictEqual( - normalizeLineEndngs(result), - normalizeLineEndngs(` - const sourceCode = context.sourceCode ?? context.getSourceCode(); - const cwd = context.cwd ?? context.getCwd(); - const filename = context.filename ?? context.getFilename(); - const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename(); - `.trim()) - ); - }); - - it("should migrate deprecated context methods to SourceCode", async () => { - const result = await applyTransform( - v9MigrationTransform, - ` - const sourceCode = context.getSource(); - const sourceLines = context.getSourceLines(); - const allComments = context.getAllComments(); - const nodeByRangeIndex = context.getNodeByRangeIndex(); - const commentsBefore = context.getCommentsBefore(nodeOrToken); - const commentsAfter = context.getCommentsAfter(nodeOrToken); - const commentsInside = context.getCommentsInside(nodeOrToken); - ` - ); - - assert.strictEqual( - normalizeLineEndngs(result), - normalizeLineEndngs(` - const sourceCode = context.sourceCode.getText(); - const sourceLines = context.sourceCode.getLines(); - const allComments = context.sourceCode.getAllComments(); - const nodeByRangeIndex = context.sourceCode.getNodeByRangeIndex(); - const commentsBefore = context.sourceCode.getCommentsBefore(nodeOrToken); - const commentsAfter = context.sourceCode.getCommentsAfter(nodeOrToken); - const commentsInside = context.sourceCode.getCommentsInside(nodeOrToken); - `.trim()) - ); - }); - - it("should warn about context.getComments()", async () => { - const spy = sinon.spy(console, "warn"); - - await applyTransform( - v9MigrationTransform, - { - source: "const comments = context.getComments();", - path: path.resolve(__dirname, __filename) - } - ); - - assert.strictEqual(spy.callCount, 1); - assert.match( - spy.args[0][0], - /1:17 The "getComments\(\)" method has been removed. Please use "getCommentsBefore\(\)", "getCommentsAfter\(\)", or "getCommentsInside\(\)" instead/u - ); - - spy.restore(); - }); - - it("should warn about codePath.currentSegments", async () => { - const spy = sinon.spy(console, "warn"); - const filePath = path.resolve(__dirname, __filename); - - await applyTransform( - v9MigrationTransform, - { - path: filePath, - source: ` - module.exports = { - meta: { - docs: {}, - schema: [] - }, - create(context) { - return { - onCodePathStart(codePath, node) { - const currentSegments = codePath.currentSegments(); - }, - - onCodePathEnd(endCodePath, node) { - const currentSegments = endCodePath.currentSegments(); - }, - }; - } - } - ` - } - ); - - assert.strictEqual(spy.callCount, 2); - assert.match( - spy.args[0][0], - /10:56 The "CodePath#currentSegments" property has been removed and it can't be migrated automatically/u - ); - assert.match( - spy.args[1][0], - /14:56 The "CodePath#currentSegments" property has been removed and it can't be migrated automatically/u - ); - - spy.restore(); - }); -}); diff --git a/tests/lib/v9-rule-migration/v9-rule-migration.js b/tests/lib/v9-rule-migration/v9-rule-migration.js new file mode 100644 index 0000000..a37a1e4 --- /dev/null +++ b/tests/lib/v9-rule-migration/v9-rule-migration.js @@ -0,0 +1,286 @@ +/** + * @fileoverview Tests for v9-rule-migration transform. + * @author Nitin Kumar + * MIT License + */ + +"use strict"; + +const path = require("path"); +const { applyTransform } = require("@hypermod/utils"); +const assert = require("assert"); +const sinon = require("sinon"); +const { normalizeLineEndngs } = require("../../utils"); +const v9MigrationTransform = require("../../../lib/v9-rule-migration/v9-rule-migration"); + +describe("v9 migration transform", () => { + it("should migrate deprecated context methods to new properties", async () => { + const result = await applyTransform( + v9MigrationTransform, + ` + module.exports = { + create(context) { + return { + Program(node) { + const sourceCode = context.getSourceCode(); + const cwd = context.getCwd(); + const filename = context.getFilename(); + const physicalFilename = context.getPhysicalFilename(); + }, + }; + } + }; + ` + ); + + assert.strictEqual( + normalizeLineEndngs(result), + normalizeLineEndngs( + ` + module.exports = { + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + return { + Program(node) { + const sourceCode = sourceCode; + const cwd = context.cwd ?? context.getCwd(); + const filename = context.filename ?? context.getFilename(); + const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename(); + }, + }; + } + }; + `.trim() + ) + ); + }); + + it("should migrate deprecated context methods to SourceCode", async () => { + const result = await applyTransform( + v9MigrationTransform, + ` + module.exports = { + create(context) { + return { + Program(node) { + const sourceCodeText = context.getSource(); + const sourceLines = context.getSourceLines(); + const allComments = context.getAllComments(); + const nodeByRangeIndex = context.getNodeByRangeIndex(); + const commentsBefore = context.getCommentsBefore(nodeOrToken); + const commentsAfter = context.getCommentsAfter(nodeOrToken); + const commentsInside = context.getCommentsInside(nodeOrToken); + const jsDocComment = context.getJSDocComment(); + const firstToken = context.getFirstToken(node); + const firstTokens = context.getFirstTokens(node); + const lastToken = context.getLastToken(node); + const lastTokens = context.getLastTokens(node); + const tokenAfter = context.getTokenAfter(node); + const tokenBefore = context.getTokenBefore(node); + const tokenByRangeStart = context.getTokenByRangeStart(node); + const getTokens = context.getTokens(node); + const tokensAfter = context.getTokensAfter(node); + const tokensBefore = context.getTokensBefore(node); + const tokensBetween = context.getTokensBetween(node); + const parserServices = context.parserServices; + }, + + FunctionDeclaration(node) { + const sourceCodeText = context.getSourceCode().getText(); + const sourceLines = context.getSourceCode().getLines(); + const allComments = context.getSourceCode().getAllComments(); + const nodeByRangeIndex = context.getSourceCode().getNodeByRangeIndex(); + const commentsBefore = context.getSourceCode().getCommentsBefore(node); + const commentsAfter = context.getSourceCode().getCommentsAfter(node); + const commentsInside = context.getSourceCode().getCommentsInside(node); + const jsDocComment = context.getSourceCode().getJSDocComment(); + const firstToken = context.getSourceCode().getFirstToken(node); + const firstTokens = context.getSourceCode().getFirstTokens(node); + const lastToken = context.getSourceCode().getLastToken(node); + const lastTokens = context.getSourceCode().getLastTokens(node); + const tokenAfter = context.getSourceCode().getTokenAfter(node); + const tokenBefore = context.getSourceCode().getTokenBefore(node); + const tokenByRangeStart = context.getSourceCode().getTokenByRangeStart(node); + const getTokens = context.getSourceCode().getTokens(node); + const tokensAfter = context.getSourceCode().getTokensAfter(node); + const tokensBefore = context.getSourceCode().getTokensBefore(node); + const tokensBetween = context.getSourceCode().getTokensBetween(node); + const parserServices = context.getSourceCode().parserServices; + }, + }; + } + }; + ` + ); + + assert.strictEqual( + normalizeLineEndngs(result), + normalizeLineEndngs( + ` + module.exports = { + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + return { + Program(node) { + const sourceCodeText = sourceCode.getText(); + const sourceLines = sourceCode.getLines(); + const allComments = sourceCode.getAllComments(); + const nodeByRangeIndex = sourceCode.getNodeByRangeIndex(); + const commentsBefore = sourceCode.getCommentsBefore(nodeOrToken); + const commentsAfter = sourceCode.getCommentsAfter(nodeOrToken); + const commentsInside = sourceCode.getCommentsInside(nodeOrToken); + const jsDocComment = sourceCode.getJSDocComment(); + const firstToken = sourceCode.getFirstToken(node); + const firstTokens = sourceCode.getFirstTokens(node); + const lastToken = sourceCode.getLastToken(node); + const lastTokens = sourceCode.getLastTokens(node); + const tokenAfter = sourceCode.getTokenAfter(node); + const tokenBefore = sourceCode.getTokenBefore(node); + const tokenByRangeStart = sourceCode.getTokenByRangeStart(node); + const getTokens = sourceCode.getTokens(node); + const tokensAfter = sourceCode.getTokensAfter(node); + const tokensBefore = sourceCode.getTokensBefore(node); + const tokensBetween = sourceCode.getTokensBetween(node); + const parserServices = sourceCode.parserServices; + }, + + FunctionDeclaration(node) { + const sourceCodeText = sourceCode.getText(); + const sourceLines = sourceCode.getLines(); + const allComments = sourceCode.getAllComments(); + const nodeByRangeIndex = sourceCode.getNodeByRangeIndex(); + const commentsBefore = sourceCode.getCommentsBefore(node); + const commentsAfter = sourceCode.getCommentsAfter(node); + const commentsInside = sourceCode.getCommentsInside(node); + const jsDocComment = sourceCode.getJSDocComment(); + const firstToken = sourceCode.getFirstToken(node); + const firstTokens = sourceCode.getFirstTokens(node); + const lastToken = sourceCode.getLastToken(node); + const lastTokens = sourceCode.getLastTokens(node); + const tokenAfter = sourceCode.getTokenAfter(node); + const tokenBefore = sourceCode.getTokenBefore(node); + const tokenByRangeStart = sourceCode.getTokenByRangeStart(node); + const getTokens = sourceCode.getTokens(node); + const tokensAfter = sourceCode.getTokensAfter(node); + const tokensBefore = sourceCode.getTokensBefore(node); + const tokensBetween = sourceCode.getTokensBetween(node); + const parserServices = sourceCode.parserServices; + }, + }; + } + }; + `.trim() + ) + ); + }); + + it("should migrate recently added methods on sourceCode with signature change", async () => { + const result = await applyTransform( + v9MigrationTransform, + ` + module.exports = { + create(context) { + return { + Program(node) { + const scope = context.getScope(); + const result = context.markVariableAsUsed("foo"); + }, + + MemberExpression(memberExpressionNode) { + const ancestor = context.getAncestors(); + }, + + FunctionDeclaration(functionDeclarationNode) { + const declaredVariables = context.getDeclaredVariables(); + }, + }; + } + }; + ` + ); + + assert.strictEqual( + normalizeLineEndngs(result), + normalizeLineEndngs( + ` + module.exports = { + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + return { + Program(node) { + const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); + const result = sourceCode.markVariableAsUsed ? sourceCode.markVariableAsUsed("foo", node) : context.markVariableAsUsed(); + }, + + MemberExpression(memberExpressionNode) { + const ancestor = sourceCode.getAncestors ? sourceCode.getAncestors(memberExpressionNode) : context.getAncestors(); + }, + + FunctionDeclaration(functionDeclarationNode) { + const declaredVariables = sourceCode.getDeclaredVariables ? sourceCode.getDeclaredVariables(functionDeclarationNode) : context.getDeclaredVariables(); + }, + }; + } + }; + `.trim() + ) + ); + }); + + it("should warn about context.getComments()", async () => { + const spy = sinon.spy(console, "warn"); + + await applyTransform(v9MigrationTransform, { + source: "const comments = context.getComments();", + path: path.resolve(__dirname, __filename) + }); + + assert.strictEqual(spy.callCount, 1); + assert.match( + spy.args[0][0], + /1:17 The "getComments\(\)" method has been removed. Please use "getCommentsBefore\(\)", "getCommentsAfter\(\)", or "getCommentsInside\(\)" instead/u + ); + + spy.restore(); + }); + + it("should warn about codePath.currentSegments", async () => { + const spy = sinon.spy(console, "warn"); + const filePath = path.resolve(__dirname, __filename); + + await applyTransform(v9MigrationTransform, { + path: filePath, + source: ` + module.exports = { + meta: { + docs: {}, + schema: [] + }, + create(context) { + return { + onCodePathStart(codePath, node) { + const currentSegments = codePath.currentSegments(); + }, + + onCodePathEnd(endCodePath, node) { + const currentSegments = endCodePath.currentSegments(); + }, + }; + } + } + ` + }); + + assert.strictEqual(spy.callCount, 2); + assert.match( + spy.args[0][0], + /10:56 The "CodePath#currentSegments" property has been removed and it can't be migrated automatically/u + ); + assert.match( + spy.args[1][0], + /14:56 The "CodePath#currentSegments" property has been removed and it can't be migrated automatically/u + ); + + spy.restore(); + }); +}); diff --git a/tests/utils/index.js b/tests/utils/index.js index 7fa993c..f75e0d4 100644 --- a/tests/utils/index.js +++ b/tests/utils/index.js @@ -16,4 +16,4 @@ function normalizeLineEndngs(input) { module.exports = { normalizeLineEndngs -}; \ No newline at end of file +}; From 67acc389a12125c6734e3812af846e050b29d1f3 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 1 Jun 2024 16:14:45 +0530 Subject: [PATCH 6/8] fix: cover more cases --- README.md | 72 ++++++++++- lib/v9-rule-migration/v9-rule-migration.js | 117 +++++++++++------- tests/lib/new-rule-format/new-rule-format.js | 17 ++- .../v9-rule-migration/v9-rule-migration.js | 40 ++++-- tests/utils/index.js | 4 +- 5 files changed, 181 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 2b17fed..9d50e17 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ Where: `path` - Files or directory to transform. - For more information on jscodeshift, check their official [docs](https://github.com/facebook/jscodeshift). ## Transforms @@ -49,3 +48,74 @@ module.exports = { create: function(context) { ... } }; ``` + +### v9-rule-migration + +```javascript +module.exports = { + create(context) { + return { + Program(node) { + const sourceCode = context.getSourceCode(); + const cwd = context.getCwd(); + const filename = context.getFilename(); + const physicalFilename = context.getPhysicalFilename(); + const sourceCodeText = context.getSource(); + const sourceLines = context.getSourceLines(); + const allComments = context.getAllComments(); + const nodeByRangeIndex = context.getNodeByRangeIndex(); + const commentsBefore = context.getCommentsBefore(node); + const commentsAfter = context.getCommentsAfter(node); + const commentsInside = context.getCommentsInside(node); + const jsDocComment = context.getJSDocComment(); + const firstToken = context.getFirstToken(node); + const firstTokens = context.getFirstTokens(node); + const lastToken = context.getLastToken(node); + const lastTokens = context.getLastTokens(node); + const tokenAfter = context.getTokenAfter(node); + const tokenBefore = context.getTokenBefore(node); + const tokenByRangeStart = context.getTokenByRangeStart(node); + const getTokens = context.getTokens(node); + const tokensAfter = context.getTokensAfter(node); + const tokensBefore = context.getTokensBefore(node); + const tokensBetween = context.getTokensBetween(node); + const parserServices = context.parserServices; + }, + }; + }, +}; +``` + +to the new Rule API introduced in ESLint 9.0.0: + +```javascript +module.exports = { + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + return { + Program(node) { + const sourceCodeText = sourceCode.getText(); + const sourceLines = sourceCode.getLines(); + const allComments = sourceCode.getAllComments(); + const nodeByRangeIndex = sourceCode.getNodeByRangeIndex(); + const commentsBefore = sourceCode.getCommentsBefore(nodeOrToken); + const commentsAfter = sourceCode.getCommentsAfter(nodeOrToken); + const commentsInside = sourceCode.getCommentsInside(nodeOrToken); + const jsDocComment = sourceCode.getJSDocComment(); + const firstToken = sourceCode.getFirstToken(node); + const firstTokens = sourceCode.getFirstTokens(node); + const lastToken = sourceCode.getLastToken(node); + const lastTokens = sourceCode.getLastTokens(node); + const tokenAfter = sourceCode.getTokenAfter(node); + const tokenBefore = sourceCode.getTokenBefore(node); + const tokenByRangeStart = sourceCode.getTokenByRangeStart(node); + const getTokens = sourceCode.getTokens(node); + const tokensAfter = sourceCode.getTokensAfter(node); + const tokensBefore = sourceCode.getTokensBefore(node); + const tokensBetween = sourceCode.getTokensBetween(node); + const parserServices = sourceCode.parserServices; + }, + }; + }, +}; +``` diff --git a/lib/v9-rule-migration/v9-rule-migration.js b/lib/v9-rule-migration/v9-rule-migration.js index 7b922fb..ee39dfd 100644 --- a/lib/v9-rule-migration/v9-rule-migration.js +++ b/lib/v9-rule-migration/v9-rule-migration.js @@ -81,6 +81,7 @@ function getParentObjectMethod(nodePath) { return getParentObjectMethod(nodePath.parentPath); } + //------------------------------------------------------------------------------ // Transform Definition //------------------------------------------------------------------------------ @@ -95,6 +96,50 @@ function getParentObjectMethod(nodePath) { module.exports = function(fileInfo, api) { const j = api.jscodeshift; const root = j(fileInfo.source); + const USED_CONTEXT_METHODS = new Set(); + + /** + * Adds a variable declaration for the context method immediately inside the create() method + * @param {string} methodName The name of the context method + * @param {Array} args The arguments to be passed to the context method + * @returns {void} + */ + function addContextMethodVariableDeclaration(methodName, args = []) { + + if (USED_CONTEXT_METHODS.has(methodName)) { + return; + } + + root.find(j.Property, { + key: { name: "create" } + }).replaceWith(({ node: createNode }) => { + const contextMethodDeclaration = j.variableDeclaration("const", [ + j.variableDeclarator( + j.identifier(contextMethodsToPropertyMapping[methodName]), + j.logicalExpression( + "??", + j.memberExpression( + j.identifier("context"), + j.identifier(contextMethodsToPropertyMapping[methodName]) + ), + j.callExpression( + j.memberExpression( + j.identifier("context"), + j.identifier(methodName) + ), + [...args] + ) + ) + ) + ]); + + // Insert the sourceCodeDeclaration at the beginning of the create() method + createNode.value.body.body.unshift(contextMethodDeclaration); + USED_CONTEXT_METHODS.add(methodName); + + return createNode; + }); + } // Update context methods // context.getSourceCode() -> context.sourceCode ?? context.getSourceCode() @@ -114,56 +159,31 @@ module.exports = function(fileInfo, api) { const method = node.callee.property.name; const args = node.arguments; - // Replace all instances of context.getSourceCode() with sourceCode variable - if (method === "getSourceCode") { - return j.identifier("sourceCode"); - } + addContextMethodVariableDeclaration(method, args); - return j.logicalExpression( - "??", - j.memberExpression( - j.identifier("context"), - j.identifier(contextMethodsToPropertyMapping[method]) - ), - j.callExpression( - j.memberExpression( - j.identifier("context"), - j.identifier(method) - ), - [...args] - ) - ); + // Replace all instances of context methods with corresponding variable created above + return j.identifier(contextMethodsToPropertyMapping[method]); }); - // Add a variable declaration for `const sourceCode = context.sourceCode ?? context.getSourceCode()` immediately inside the create() method - root.find(j.Property, { - key: { name: "create" } - }).replaceWith(({ node }) => { - const sourceCodeDeclaration = j.variableDeclaration("const", [ - j.variableDeclarator( - j.identifier("sourceCode"), - j.logicalExpression( - "??", - j.memberExpression( - j.identifier("context"), - j.identifier("sourceCode") - ), - j.callExpression( - j.memberExpression( - j.identifier("context"), - j.identifier("getSourceCode") - ), - [] - ) - ) - ) - ]); - - // Insert the sourceCodeDeclaration at the beginning of the create() method - node.value.body.body.unshift(sourceCodeDeclaration); - - return node; - }); + // Remove the variable declarations which have value same as the declaration + // const sourceCode = sourceCode -> Remove + root.find(j.VariableDeclaration, { + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: name => + Object.values(contextMethodsToPropertyMapping).includes(name) + }, + init: { + type: "Identifier" + } + } + ] + }) + .filter(({ node }) => node.declarations[0].id.name === node.declarations[0].init.name) + .remove(); // Move context methods to SourceCode // context.getSource() -> sourceCode.getText() @@ -199,6 +219,9 @@ module.exports = function(fileInfo, api) { return node; } + // Add variable declaration for the method if not already added + addContextMethodVariableDeclaration("getSourceCode"); + if (METHODS_WITH_SIGNATURE_CHANGE.has(method)) { const parentObjectMethodNode = getParentObjectMethod(nodePath); const parentObjectMethodParamName = diff --git a/tests/lib/new-rule-format/new-rule-format.js b/tests/lib/new-rule-format/new-rule-format.js index 7e62cd4..3703709 100644 --- a/tests/lib/new-rule-format/new-rule-format.js +++ b/tests/lib/new-rule-format/new-rule-format.js @@ -13,7 +13,7 @@ const path = require("path"); const assert = require("assert"); const newRuleFormatTransform = require("../../../lib/new-rule-format/new-rule-format"); -const { normalizeLineEndngs } = require("../../utils"); +const { normalizeLineEndings } = require("../../utils"); /** * Run a transform against a fixture file and compare results with expected output. @@ -26,8 +26,14 @@ const { normalizeLineEndngs } = require("../../utils"); * @private */ function testTransformWithFixture(transform, transformFixturePrefix) { - const fixtureDir = path.join(__dirname, "../../fixtures/lib/new-rule-format"); - const inputPath = path.join(fixtureDir, `${transformFixturePrefix}.input.js`); + const fixtureDir = path.join( + __dirname, + "../../fixtures/lib/new-rule-format" + ); + const inputPath = path.join( + fixtureDir, + `${transformFixturePrefix}.input.js` + ); const source = fs.readFileSync(inputPath, "utf8"); const expectedOutput = fs.readFileSync( path.join(fixtureDir, `${transformFixturePrefix}.output.js`), @@ -35,7 +41,6 @@ function testTransformWithFixture(transform, transformFixturePrefix) { ); it(`transforms correctly using "${transformFixturePrefix}" fixture`, () => { - const output = transform( { path: inputPath, source }, { jscodeshift }, @@ -43,8 +48,8 @@ function testTransformWithFixture(transform, transformFixturePrefix) { ); assert.strictEqual( - normalizeLineEndngs((output || "").trim()), - normalizeLineEndngs(expectedOutput.trim()) + normalizeLineEndings((output || "").trim()), + normalizeLineEndings(expectedOutput.trim()) ); }); } diff --git a/tests/lib/v9-rule-migration/v9-rule-migration.js b/tests/lib/v9-rule-migration/v9-rule-migration.js index a37a1e4..b887366 100644 --- a/tests/lib/v9-rule-migration/v9-rule-migration.js +++ b/tests/lib/v9-rule-migration/v9-rule-migration.js @@ -10,7 +10,7 @@ const path = require("path"); const { applyTransform } = require("@hypermod/utils"); const assert = require("assert"); const sinon = require("sinon"); -const { normalizeLineEndngs } = require("../../utils"); +const { normalizeLineEndings } = require("../../utils"); const v9MigrationTransform = require("../../../lib/v9-rule-migration/v9-rule-migration"); describe("v9 migration transform", () => { @@ -27,6 +27,13 @@ describe("v9 migration transform", () => { const filename = context.getFilename(); const physicalFilename = context.getPhysicalFilename(); }, + + FunctionDeclaration(node) { + const _sourceCode = context.getSourceCode(); + const _cwd = context.getCwd(); + const _filename = context.getFilename(); + const _physicalFilename = context.getPhysicalFilename(); + } }; } }; @@ -34,19 +41,24 @@ describe("v9 migration transform", () => { ); assert.strictEqual( - normalizeLineEndngs(result), - normalizeLineEndngs( + normalizeLineEndings(result), + normalizeLineEndings( ` module.exports = { create(context) { + const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename(); + const filename = context.filename ?? context.getFilename(); + const cwd = context.cwd ?? context.getCwd(); const sourceCode = context.sourceCode ?? context.getSourceCode(); return { - Program(node) { - const sourceCode = sourceCode; - const cwd = context.cwd ?? context.getCwd(); - const filename = context.filename ?? context.getFilename(); - const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename(); - }, + Program(node) {}, + + FunctionDeclaration(node) { + const _sourceCode = sourceCode; + const _cwd = cwd; + const _filename = filename; + const _physicalFilename = physicalFilename; + } }; } }; @@ -114,8 +126,8 @@ describe("v9 migration transform", () => { ); assert.strictEqual( - normalizeLineEndngs(result), - normalizeLineEndngs( + normalizeLineEndings(result), + normalizeLineEndings( ` module.exports = { create(context) { @@ -184,6 +196,7 @@ describe("v9 migration transform", () => { Program(node) { const scope = context.getScope(); const result = context.markVariableAsUsed("foo"); + const statements = context.getAncestors().filter(node => node.endsWith("Statement")); }, MemberExpression(memberExpressionNode) { @@ -200,8 +213,8 @@ describe("v9 migration transform", () => { ); assert.strictEqual( - normalizeLineEndngs(result), - normalizeLineEndngs( + normalizeLineEndings(result), + normalizeLineEndings( ` module.exports = { create(context) { @@ -210,6 +223,7 @@ describe("v9 migration transform", () => { Program(node) { const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); const result = sourceCode.markVariableAsUsed ? sourceCode.markVariableAsUsed("foo", node) : context.markVariableAsUsed(); + const statements = (sourceCode.getAncestors ? sourceCode.getAncestors(node) : context.getAncestors()).filter(node => node.endsWith("Statement")); }, MemberExpression(memberExpressionNode) { diff --git a/tests/utils/index.js b/tests/utils/index.js index f75e0d4..9ee6627 100644 --- a/tests/utils/index.js +++ b/tests/utils/index.js @@ -10,10 +10,10 @@ const os = require("os"); * @returns {string} a new string with all EOL markers replaced * @private */ -function normalizeLineEndngs(input) { +function normalizeLineEndings(input) { return input.replace(/(\r\n|\n|\r)/gmu, os.EOL); } module.exports = { - normalizeLineEndngs + normalizeLineEndings }; From 795992fc93c378bd1e210da09342606b6b100ec9 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 1 Jun 2024 16:18:37 +0530 Subject: [PATCH 7/8] docs: update --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d50e17..ef217bc 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ module.exports = { ### v9-rule-migration +Transform that migrates an ESLint rule definition from the old Rule API: + ```javascript module.exports = { create(context) { @@ -86,7 +88,7 @@ module.exports = { }; ``` -to the new Rule API introduced in ESLint 9.0.0: +to the new [Rule API introduced in ESLint 9.0.0](https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/): ```javascript module.exports = { From 955d26a181d141eb5c33e1c4a8af72b4d44c10f1 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Thu, 6 Jun 2024 06:48:20 +0530 Subject: [PATCH 8/8] fix: cover more edge cases --- lib/v9-rule-migration/v9-rule-migration.js | 48 +++++++++++++++---- .../v9-rule-migration/v9-rule-migration.js | 38 +++++++++++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/lib/v9-rule-migration/v9-rule-migration.js b/lib/v9-rule-migration/v9-rule-migration.js index ee39dfd..796bd21 100644 --- a/lib/v9-rule-migration/v9-rule-migration.js +++ b/lib/v9-rule-migration/v9-rule-migration.js @@ -73,15 +73,19 @@ const METHODS_WITH_SIGNATURE_CHANGE = new Set([ * @returns {Node} The parent ObjectMethod node */ function getParentObjectMethod(nodePath) { + if (!nodePath) { + return null; + } + const node = nodePath.node; if (node.type && node.type === "Property" && node.method) { return node; } + return getParentObjectMethod(nodePath.parentPath); } - //------------------------------------------------------------------------------ // Transform Definition //------------------------------------------------------------------------------ @@ -105,7 +109,6 @@ module.exports = function(fileInfo, api) { * @returns {void} */ function addContextMethodVariableDeclaration(methodName, args = []) { - if (USED_CONTEXT_METHODS.has(methodName)) { return; } @@ -120,7 +123,9 @@ module.exports = function(fileInfo, api) { "??", j.memberExpression( j.identifier("context"), - j.identifier(contextMethodsToPropertyMapping[methodName]) + j.identifier( + contextMethodsToPropertyMapping[methodName] + ) ), j.callExpression( j.memberExpression( @@ -161,8 +166,27 @@ module.exports = function(fileInfo, api) { addContextMethodVariableDeclaration(method, args); - // Replace all instances of context methods with corresponding variable created above - return j.identifier(contextMethodsToPropertyMapping[method]); + // If the method is already declared as a variable in the create() method + // Replace all instances of context methods with corresponding variable + if (USED_CONTEXT_METHODS.has(method)) { + return j.identifier(contextMethodsToPropertyMapping[method]); + } + + // Otherwise, create a variable declaration for the method + return j.logicalExpression( + "??", + j.memberExpression( + j.identifier("context"), + j.identifier(contextMethodsToPropertyMapping[method]) + ), + j.callExpression( + j.memberExpression( + j.identifier("context"), + j.identifier(method) + ), + args + ) + ); }); // Remove the variable declarations which have value same as the declaration @@ -174,7 +198,9 @@ module.exports = function(fileInfo, api) { id: { type: "Identifier", name: name => - Object.values(contextMethodsToPropertyMapping).includes(name) + Object.values(contextMethodsToPropertyMapping).includes( + name + ) }, init: { type: "Identifier" @@ -182,7 +208,10 @@ module.exports = function(fileInfo, api) { } ] }) - .filter(({ node }) => node.declarations[0].id.name === node.declarations[0].init.name) + .filter( + ({ node }) => + node.declarations[0].id.name === node.declarations[0].init.name + ) .remove(); // Move context methods to SourceCode @@ -225,6 +254,7 @@ module.exports = function(fileInfo, api) { if (METHODS_WITH_SIGNATURE_CHANGE.has(method)) { const parentObjectMethodNode = getParentObjectMethod(nodePath); const parentObjectMethodParamName = + parentObjectMethodNode && parentObjectMethodNode.value.params[0].name; // Return the node as is if the method is called with an argument @@ -239,7 +269,9 @@ module.exports = function(fileInfo, api) { j.identifier("sourceCode"), j.identifier(contextToSourceCodeMapping[method]) ), - [...args, j.identifier(parentObjectMethodParamName)] + parentObjectMethodParamName + ? [...args, j.identifier(parentObjectMethodParamName)] + : args ), j.callExpression( j.memberExpression( diff --git a/tests/lib/v9-rule-migration/v9-rule-migration.js b/tests/lib/v9-rule-migration/v9-rule-migration.js index b887366..116c82d 100644 --- a/tests/lib/v9-rule-migration/v9-rule-migration.js +++ b/tests/lib/v9-rule-migration/v9-rule-migration.js @@ -67,6 +67,44 @@ describe("v9 migration transform", () => { ); }); + it("should migrate deprecated context methods to new properties #2", async () => { + const result = await applyTransform( + v9MigrationTransform, + ` + module.exports = { + create(context) { + const sourceCode = context.getSourceCode(); + const cwd = context.getCwd(); + const filename = context.getFilename(); + const physicalFilename = context.getPhysicalFilename(); + return { + Program(node) {}, + }; + } + }; + ` + ); + + assert.strictEqual( + normalizeLineEndings(result), + normalizeLineEndings( + ` + module.exports = { + create(context) { + const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename(); + const filename = context.filename ?? context.getFilename(); + const cwd = context.cwd ?? context.getCwd(); + const sourceCode = context.sourceCode ?? context.getSourceCode(); + return { + Program(node) {}, + }; + } + }; + `.trim() + ) + ); + }); + it("should migrate deprecated context methods to SourceCode", async () => { const result = await applyTransform( v9MigrationTransform,