diff --git a/codemods/define-properties/index.js b/codemods/define-properties/index.js index d601eefc..9e3f2cd2 100644 --- a/codemods/define-properties/index.js +++ b/codemods/define-properties/index.js @@ -1,26 +1,14 @@ -import jscodeshift from 'jscodeshift'; -import { - DEFAULT_IMPORT, - getImportIdentifierMap, - getVariableExpressionHasIdentifier, - insertAfterImports, - insertCommentAboveNode, - removeImport, - replaceRequireMemberExpression, -} from '../shared.js'; +import { ts } from '@ast-grep/napi'; +import { findDefaultImportIdentifier } from '../shared-ast-grep.js'; -/** - * @typedef {import('../../types.js').Codemod} Codemod - * @typedef {import('../../types.js').CodemodOptions} CodemodOptions - */ +const MODULE_NAME = 'define-properties'; /** - * * @param {string} name - * @returns + * @returns {string} */ -const definePropertiesTemplate = (name) => ` -const ${name} = function (object, map) { +const definePropertiesTemplate = (name) => + `const ${name} = function (object, map) { let propKeys = Object.keys(map); propKeys = propKeys.concat(Object.getOwnPropertySymbols(map)); @@ -43,95 +31,107 @@ const ${name} = function (object, map) { return object; };`; +/** + * @typedef {import('../../types.js').Codemod} Codemod + * @typedef {import('../../types.js').CodemodOptions} CodemodOptions + */ + /** * @param {CodemodOptions} [options] * @returns {Codemod} */ export default function (options) { return { - name: 'define-properties', + name: MODULE_NAME, to: 'native', transform: ({ file }) => { - const j = jscodeshift; - const root = j(file.source); - const variableExpressionHasIdentifier = - getVariableExpressionHasIdentifier( - 'define-properties', - 'supportsDescriptors', - root, - j, - ); + const root = ts.parse(file.source).root(); + const edits = []; + + const memberExprs = root.findAll({ + rule: { + pattern: { + context: "require('define-properties').supportsDescriptors", + strictness: 'relaxed', + }, + }, + }); - // Use case 1: require('define-properties').supportsDescriptors - if (variableExpressionHasIdentifier) { - const didReplace = replaceRequireMemberExpression( - 'define-properties', - true, - root, - j, - ); - return didReplace ? root.toSource(options) : file.source; + if (memberExprs.length > 0) { + for (const expr of memberExprs) { + edits.push(expr.replace('true')); + } + return root.commitEdits(edits); } - const map = getImportIdentifierMap('define-properties', root, j); - - const identifier = map[DEFAULT_IMPORT]; + const { imports, identifierName } = findDefaultImportIdentifier( + root, + MODULE_NAME, + ); + if (!identifierName) return file.source; - const callExpressions = root.find(j.CallExpression, { - callee: { - name: identifier, - }, + const calls = root.findAll({ + rule: { pattern: `${identifierName}($$$ARGS)` }, }); - if (!callExpressions.length) { - removeImport('define-properties', root, j); - return root.toSource(options); + if (calls.length === 0) { + for (const imp of imports) { + edits.push(imp.replace('')); + } + return root.commitEdits(edits); } let transformCount = 0; - let dirty = false; - callExpressions.forEach((path) => { - const node = path.node; - const newIdentifier = `$${identifier}`; - - // Use case 2: define(object, map); - if (node.arguments.length === 2) { - if (transformCount === 0) { - const defineFunction = definePropertiesTemplate(newIdentifier); - insertAfterImports(defineFunction, root, j); - } - - // Not all call expressions have a name property, but node.callee should be of type Identifi - if ('name' in node.callee) { - node.callee.name = newIdentifier; - } + for (const call of calls) { + const args = (call.getMultipleMatches('ARGS') || []).filter( + (m) => m.kind() !== ',', + ); + if (args.length === 2) { + const fn = call.field('function'); + if (fn) edits.push(fn.replace(`$${identifierName}`)); transformCount++; - dirty = true; } - // Use case 3: define(object, map, predicates); - if (node.arguments.length === 3) { - const comment = j.commentBlock( - '\n This usage of `define-properties` usage can be cleaned up through a mix of Object.defineProperty() and a custom predicate function.\n details can be found here: https://github.com/es-tooling/module-replacements-codemods/issues/66 \n', - true, - false, - ); - - const startLine = node.loc?.start.line ?? 0; + if (args.length === 3) { + let stmt = call; + while (stmt) { + const parent = stmt.parent(); + if (!parent) break; + const k = parent.kind(); + if ( + k === 'expression_statement' || + k === 'lexical_declaration' || + k === 'variable_declaration' + ) { + stmt = parent; + break; + } + stmt = parent; + } + const comment = + '/*\n This usage of `define-properties` usage can be cleaned up through a mix of Object.defineProperty() and a custom predicate function.\n details can be found here: https://github.com/es-tooling/module-replacements-codemods/issues/66 \n*/'; + edits.push(stmt.replace(`${comment}\n${stmt.text()}`)); + } + } - insertCommentAboveNode(comment, startLine, root, j); + if (transformCount === 0) { + return edits.length > 0 ? root.commitEdits(edits) : file.source; + } - dirty = true; - } - }); + const newName = `$${identifierName}`; + const polyfill = definePropertiesTemplate(newName); + const allTransformed = transformCount === calls.length; - if (transformCount === callExpressions.length) { - removeImport('define-properties', root, j); + if (allTransformed) { + for (const imp of imports) edits.push(imp.replace(polyfill)); + } else { + for (const imp of imports) + edits.push(imp.replace(`${imp.text()}\n\n${polyfill}`)); } - return dirty ? root.toSource(options) : file.source; + return edits.length > 0 ? root.commitEdits(edits) : file.source; }, }; } diff --git a/test/fixtures/define-properties/case-2/after.js b/test/fixtures/define-properties/case-2/after.js index 540fc542..012efbee 100644 --- a/test/fixtures/define-properties/case-2/after.js +++ b/test/fixtures/define-properties/case-2/after.js @@ -1,6 +1,3 @@ -const assert = require('assert'); - - const $define = function (object, map) { let propKeys = Object.keys(map); propKeys = propKeys.concat(Object.getOwnPropertySymbols(map)); @@ -23,6 +20,7 @@ const $define = function (object, map) { return object; }; +const assert = require('assert'); const object1 = { a: 1, b: 2 }; diff --git a/test/fixtures/define-properties/case-2/result.js b/test/fixtures/define-properties/case-2/result.js index 540fc542..012efbee 100644 --- a/test/fixtures/define-properties/case-2/result.js +++ b/test/fixtures/define-properties/case-2/result.js @@ -1,6 +1,3 @@ -const assert = require('assert'); - - const $define = function (object, map) { let propKeys = Object.keys(map); propKeys = propKeys.concat(Object.getOwnPropertySymbols(map)); @@ -23,6 +20,7 @@ const $define = function (object, map) { return object; }; +const assert = require('assert'); const object1 = { a: 1, b: 2 }; diff --git a/test/fixtures/define-properties/case-5/after.js b/test/fixtures/define-properties/case-5/after.js new file mode 100644 index 00000000..831bcf7b --- /dev/null +++ b/test/fixtures/define-properties/case-5/after.js @@ -0,0 +1,5 @@ +const sup = true; + +if (sup) { + console.log("supports descriptors"); +} diff --git a/test/fixtures/define-properties/case-5/before.js b/test/fixtures/define-properties/case-5/before.js new file mode 100644 index 00000000..cb9aa57a --- /dev/null +++ b/test/fixtures/define-properties/case-5/before.js @@ -0,0 +1,5 @@ +const sup = require("define-properties").supportsDescriptors; + +if (sup) { + console.log("supports descriptors"); +} diff --git a/test/fixtures/define-properties/case-5/result.js b/test/fixtures/define-properties/case-5/result.js new file mode 100644 index 00000000..831bcf7b --- /dev/null +++ b/test/fixtures/define-properties/case-5/result.js @@ -0,0 +1,5 @@ +const sup = true; + +if (sup) { + console.log("supports descriptors"); +} diff --git a/types/codemods/define-properties/index.d.ts b/types/codemods/define-properties/index.d.ts index 85026a28..e9c4e6ff 100644 --- a/types/codemods/define-properties/index.d.ts +++ b/types/codemods/define-properties/index.d.ts @@ -1,3 +1,7 @@ +/** + * @typedef {import('../../types.js').Codemod} Codemod + * @typedef {import('../../types.js').CodemodOptions} CodemodOptions + */ /** * @param {CodemodOptions} [options] * @returns {Codemod}