From 528b94ffd12b63aa8aad8aabdc38fd5223860d02 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Thu, 6 Jun 2024 07:13:50 +0530 Subject: [PATCH] feat: add transform for v9 migration (#29) * feat: add transform for v9 migration * chore: add test cases * chore: remove unwanted changes * chore: fix tests on windows * feat: cover more cases * fix: cover more cases * docs: update * fix: cover more edge cases --- README.md | 74 +++- lib/v9-rule-migration/v9-rule-migration.js | 335 +++++++++++++++++ package.json | 4 +- tests/lib/new-rule-format/new-rule-format.js | 29 +- .../v9-rule-migration/v9-rule-migration.js | 338 ++++++++++++++++++ tests/utils/index.js | 19 + 6 files changed, 779 insertions(+), 20 deletions(-) create mode 100644 lib/v9-rule-migration/v9-rule-migration.js create mode 100644 tests/lib/v9-rule-migration/v9-rule-migration.js create mode 100644 tests/utils/index.js diff --git a/README.md b/README.md index 2b17fed..ef217bc 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,76 @@ module.exports = { create: function(context) { ... } }; ``` + +### v9-rule-migration + +Transform that migrates an ESLint rule definition from the old Rule API: + +```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](https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/): + +```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 new file mode 100644 index 0000000..796bd21 --- /dev/null +++ b/lib/v9-rule-migration/v9-rule-migration.js @@ -0,0 +1,335 @@ +/** + * @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) { + if (!nodePath) { + return null; + } + + 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); + 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() + 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; + + addContextMethodVariableDeclaration(method, args); + + // 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 + // 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() + 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; + } + + // 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 = + parentObjectMethodNode && + 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]) + ), + parentObjectMethodParamName + ? [...args, j.identifier(parentObjectMethodParamName)] + : args + ), + 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/package.json b/package.json index 5bb5cdd..4aabdcc 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,13 @@ "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", "globals": "^15.2.0", - "mocha": "^10.4.0" + "mocha": "^10.4.0", + "sinon": "^18.0.0" }, "keywords": [ "javascript", diff --git a/tests/lib/new-rule-format/new-rule-format.js b/tests/lib/new-rule-format/new-rule-format.js index dd6af7d..3703709 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 { normalizeLineEndings } = require("../../utils"); /** * Run a transform against a fixture file and compare results with expected output. @@ -38,8 +26,14 @@ function normalizeLineEndngs(input) { * @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`), @@ -47,7 +41,6 @@ function testTransformWithFixture(transform, transformFixturePrefix) { ); it(`transforms correctly using "${transformFixturePrefix}" fixture`, () => { - const output = transform( { path: inputPath, source }, { jscodeshift }, @@ -55,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 new file mode 100644 index 0000000..116c82d --- /dev/null +++ b/tests/lib/v9-rule-migration/v9-rule-migration.js @@ -0,0 +1,338 @@ +/** + * @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 { normalizeLineEndings } = 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(); + }, + + FunctionDeclaration(node) { + const _sourceCode = context.getSourceCode(); + const _cwd = context.getCwd(); + const _filename = context.getFilename(); + const _physicalFilename = context.getPhysicalFilename(); + } + }; + } + }; + ` + ); + + 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) {}, + + FunctionDeclaration(node) { + const _sourceCode = sourceCode; + const _cwd = cwd; + const _filename = filename; + const _physicalFilename = physicalFilename; + } + }; + } + }; + `.trim() + ) + ); + }); + + 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, + ` + 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( + normalizeLineEndings(result), + normalizeLineEndings( + ` + 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"); + const statements = context.getAncestors().filter(node => node.endsWith("Statement")); + }, + + MemberExpression(memberExpressionNode) { + const ancestor = context.getAncestors(); + }, + + FunctionDeclaration(functionDeclarationNode) { + const declaredVariables = context.getDeclaredVariables(); + }, + }; + } + }; + ` + ); + + assert.strictEqual( + normalizeLineEndings(result), + normalizeLineEndings( + ` + 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(); + const statements = (sourceCode.getAncestors ? sourceCode.getAncestors(node) : context.getAncestors()).filter(node => node.endsWith("Statement")); + }, + + 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 new file mode 100644 index 0000000..9ee6627 --- /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 normalizeLineEndings(input) { + return input.replace(/(\r\n|\n|\r)/gmu, os.EOL); +} + +module.exports = { + normalizeLineEndings +};