Skip to content

Commit

Permalink
Merge pull request #68 from andrico1234/feature/define-properties
Browse files Browse the repository at this point in the history
add `define-properties` codemod
  • Loading branch information
thepassle authored Jul 31, 2024
2 parents 117078f + 9f801cf commit 25c03af
Show file tree
Hide file tree
Showing 14 changed files with 691 additions and 0 deletions.
138 changes: 138 additions & 0 deletions codemods/define-properties/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import jscodeshift from 'jscodeshift';
import {
DEFAULT_IMPORT,
getImportIdentifierMap,
getVariableExpressionHasIdentifier,
insertAfterImports,
insertCommentAboveNode,
removeImport,
replaceDefaultImport,
replaceRequireMemberExpression,
} from '../shared.js';
import { dir } from 'console';

/**
* @typedef {import('../../types.js').Codemod} Codemod
* @typedef {import('../../types.js').CodemodOptions} CodemodOptions
*/

/**
*
* @param {string} name
* @returns
*/
const definePropertiesTemplate = (name) => `
const ${name} = function (object, map) {
let propKeys = Object.keys(map);
propKeys = propKeys.concat(Object.getOwnPropertySymbols(map));
for (var i = 0; i < propKeys.length; i += 1) {
const propKey = propKeys[i];
const value = map[propKey];
if (propKey in object) {
continue;
}
Object.defineProperty(object, propKey, {
value,
configurable: true,
enumerable: false,
writable: true,
})
}
return object;
};`;

/**
* @param {CodemodOptions} [options]
* @returns {Codemod}
*/
export default function (options) {
return {
name: 'define-properties',
transform: ({ file }) => {
const j = jscodeshift;
const root = j(file.source);
const variableExpressionHasIdentifier =
getVariableExpressionHasIdentifier(
'define-properties',
'supportsDescriptors',
root,
j,
);

// Use case 1: require('define-properties').supportsDescriptors
if (variableExpressionHasIdentifier) {
const didReplace = replaceRequireMemberExpression(
'define-properties',
true,
root,
j,
);
return didReplace ? root.toSource(options) : file.source;
}

const map = getImportIdentifierMap('define-properties', root, j);

const identifier = map[DEFAULT_IMPORT];

const callExpressions = root.find(j.CallExpression, {
callee: {
name: identifier,
},
});

if (!callExpressions.length) {
removeImport('define-properties', root, j);
return root.toSource(options);
}

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;
}

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;

insertCommentAboveNode(comment, startLine, root, j);

dirty = true;
}
});

if (transformCount === callExpressions.length) {
removeImport('define-properties', root, j);
}

return dirty ? root.toSource(options) : file.source;
},
};
}
152 changes: 152 additions & 0 deletions codemods/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,68 @@ export function removeImport(name, root, j) {
return { identifier };
}

/**
* @param {string} code - code to insert after the last import
* @param {import("jscodeshift").Collection} root - jscodeshift tree of the file containing the import
* @param {import("jscodeshift").JSCodeshift} j - jscodeshift instance
*
*/
export function insertAfterImports(code, root, j) {
const importDeclarations = root.find(j.ImportDeclaration);
const requireDeclarations = root.find(j.VariableDeclarator, {
init: {
callee: {
name: 'require',
},
},
});
const requireAssignments = root.find(j.AssignmentExpression, {
operator: '=',
right: {
callee: {
name: 'require',
},
},
});

// Side effect requires statements like `require("error-cause/auto");`
const sideEffectRequireExpression = root.find(j.ExpressionStatement, {
expression: {
callee: {
name: 'require',
},
},
});

const allNodes = [
...importDeclarations.nodes(),
...requireDeclarations.nodes(),
...requireAssignments.nodes(),
...sideEffectRequireExpression.nodes(),
];

if (allNodes.length === 0) {
return;
}

const sortedNodes = allNodes.sort((a, b) => {
const aStart = a.loc?.start.line ?? 0;
const bStart = b.loc?.start.line ?? 0;
return aStart - bStart;
});

const bottomMostNode = sortedNodes[sortedNodes.length - 1];

const endLine = bottomMostNode.loc?.end.line;

if (!endLine) {
return;
}

const node = getAncestorOnLine(endLine, root, j);
j(node).insertAfter(code);
}

export const DEFAULT_IMPORT = Symbol('DEFAULT_IMPORT');
export const NAMESPACE_IMPORT = Symbol('NAMESPACE_IMPORT');

Expand Down Expand Up @@ -404,3 +466,93 @@ export function transformMathPolyfill(importName, methodName, root, j) {

return dirtyFlag;
}

/**
* @param {string} importName = e.g., `define-properties`
* @param {string} identifier = e.g., `supportsDescriptors`
* @param {import("jscodeshift").Collection} root
* @param {import("jscodeshift").JSCodeshift} j - jscodeshift instance
*/
export function getVariableExpressionHasIdentifier(
importName,
identifier,
root,
j,
) {
const requireDeclarationWithProperty = root.find(j.VariableDeclarator, {
init: {
object: {
callee: {
name: 'require',
},
},
property: {
name: identifier,
},
},
});

const source = root.toSource();

return requireDeclarationWithProperty.length > 0;
}

/**
* @param {string} importName = e.g., `define-properties`
* @param {string | boolean | null | number | RegExp} value = e.g., true or "string value"
* @param {import("jscodeshift").Collection} root
* @param {import("jscodeshift").JSCodeshift} j - jscodeshift instance
*/
export function replaceRequireMemberExpression(importName, value, root, j) {
const requireDeclaration = root
.find(j.MemberExpression, {
object: {
callee: {
name: 'require',
},
arguments: [
{
value: importName,
},
],
},
})
.forEach((path) => {
j(path).replaceWith(j.literal(value));
});

return true;
}

/**
*
* @param {number} line
* @param {import("jscodeshift").Collection} root
* @param {import("jscodeshift").JSCodeshift} j
*/
export function getAncestorOnLine(line, root, j) {
return root
.find(j.Node, {
loc: {
start: {
line,
},
},
})
.at(0)
.get();
}

/**
*
* @param {import("jscodeshift").CommentBlock} comment
* @param {number} startLine
* @param {import("jscodeshift").Collection} root
* @param {import("jscodeshift").JSCodeshift} j
*/
export function insertCommentAboveNode(comment, startLine, root, j) {
const node = getAncestorOnLine(startLine, root, j);

node.value.comments = node.value.comments || [];
node.value.comments.push(comment);
}
5 changes: 5 additions & 0 deletions test/fixtures/define-properties/case-1/after.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const sup = true;

if (sup) {
console.log('supports descriptors');
}
5 changes: 5 additions & 0 deletions test/fixtures/define-properties/case-1/before.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const sup = require('define-properties').supportsDescriptors;

if (sup) {
console.log('supports descriptors');
}
5 changes: 5 additions & 0 deletions test/fixtures/define-properties/case-1/result.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const sup = true;

if (sup) {
console.log('supports descriptors');
}
63 changes: 63 additions & 0 deletions test/fixtures/define-properties/case-2/after.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const assert = require('assert');


const $define = function (object, map) {
let propKeys = Object.keys(map);
propKeys = propKeys.concat(Object.getOwnPropertySymbols(map));

for (var i = 0; i < propKeys.length; i += 1) {
const propKey = propKeys[i];
const value = map[propKey];

if (propKey in object) {
continue;
}

Object.defineProperty(object, propKey, {
value,
configurable: true,
enumerable: false,
writable: true,
})
}

return object;
};

const object1 = { a: 1, b: 2 };

const res1 = $define(object1, {
a: 10,
b: 20,
c: 30,
[Symbol.for('d')]: 40
});

assert(res1.a === 1);
assert(res1.b === 2);
assert(res1.c === 30);
assert(res1[Symbol.for('d')] === 40);

assert.deepEqual(res1, {
a: 1,
b: 2
})

assert.deepEqual(Object.keys(res1), ['a', 'b']);
assert.deepEqual(Object.getOwnPropertyNames(res1), ['a', 'b', 'c']);
assert.deepEqual(Object.getOwnPropertyDescriptor(res1, 'c'), {
configurable: true,
enumerable: false,
value: 30,
writable: true
});

const object2 = { a: 1, b: 2 };

$define(object2, {
c: 30
})

assert(object2.a === 1);
assert(object2.b === 2);
assert(object2.c === 30);
Loading

0 comments on commit 25c03af

Please sign in to comment.