Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add transform for v9 migration #29

Merged
merged 8 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
},
};
},
};
```
303 changes: 303 additions & 0 deletions lib/v9-rule-migration/v9-rule-migration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
/**
* @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);
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);

// Replace all instances of context methods with corresponding variable created above
return j.identifier(contextMethodsToPropertyMapping[method]);
});

// 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.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();
};
Loading