bulk-decaffeinate
Version:
Run decaffeinate and related operations on a whole codebase, or just part of one.
734 lines (662 loc) • 30.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = function (fileInfo, api, options) {
var decodedOptions = JSON.parse(_zlib2.default.inflateSync(new Buffer(options['encoded-options'], 'base64')).toString());
var convertedFiles = decodedOptions.convertedFiles,
absoluteImportPaths = decodedOptions.absoluteImportPaths;
var j = api.jscodeshift;
var thisFilePath = (0, _path.resolve)(fileInfo.path);
var root = j(fileInfo.source);
function convertFile() {
if (includes(convertedFiles, thisFilePath)) {
return fixImportsForConvertedFile();
} else {
return fixImportsForOtherFile();
}
}
/**
* This file was just converted to JS, so ANY import has the potential to be
* invalid.
*/
function fixImportsForConvertedFile() {
return root.find(j.ImportDeclaration).replaceWith(fixImportAtPath).toSource();
}
/**
* This file was not just converted to JS, but could potentially import files
* that were. Correct any of those imports.
*/
function fixImportsForOtherFile() {
return root.find(j.ImportDeclaration).filter(function (path) {
var importPath = resolveImportPath(thisFilePath, path.node.source.value);
return includes(convertedFiles, importPath);
}).replaceWith(fixImportAtPath).toSource();
}
/**
* Top-level import-fixing code. We get all relevant information about the
* names being imported and the names exported by the other file, and then
* produce a set of changes on the import statement, including possibly some
* destructure operations after the import.
*/
function fixImportAtPath(path) {
var importPath = path.node.source.value;
var resolvedPath = resolveImportPath(thisFilePath, importPath);
if (resolvedPath === null) {
return path.node;
}
var exportsInfo = getExportsInformation(resolvedPath);
// If we didn't see anything on the other side, it might not even be a JS
// module, so just leave this import as-is.
if (!exportsInfo.hasDefaultExport && exportsInfo.namedExports.length === 0) {
return path.node;
}
var specifierIndex = getSpecifierIndex(path);
var memberAccesses = findAllMemberAccesses(specifierIndex);
var importManifest = getImportManifest(exportsInfo, memberAccesses);
// If any sort of property is accessed from the default import, we need it.
// Also, the default import might be something like a function where we
// imported it and the other module has a default export.
var needsDefaultImport = importManifest.defaultImportDirectAccesses.length > 0 || importManifest.defaultImportObjectAccesses.length > 0 || exportsInfo.hasDefaultExport && specifierIndex.defaultImport !== null;
// If there are object-style accesses of named imports
// (e.g. MyModule.myExport), then handle those with a star import. If we
// also have direct usages of named exports (e.g. myOtherExport), we'll need
// to destructure them from the * import later, but we try to avoid that
// when possible. Also, if a default or star import originally existed, that
// name needs to stay bound somehow, so we make sure to include it as a star
// import if the other module didn't have a default export.
var needsStarImport = importManifest.namedImportObjectAccesses.length > 0 || !needsDefaultImport && (specifierIndex.defaultImport !== null || specifierIndex.starImport !== null);
var _resolveImportObjectN = resolveImportObjectNames(specifierIndex, needsDefaultImport, needsStarImport, exportsInfo.hasDefaultExport, importPath),
defaultImportName = _resolveImportObjectN.defaultImportName,
starImportName = _resolveImportObjectN.starImportName;
path.node.specifiers = createImportSpecifiers(defaultImportName, starImportName, specifierIndex, importManifest);
renameObjectAccesses(defaultImportName, starImportName, importManifest);
if (importManifest.defaultImportDirectAccesses.length > 0) {
insertImportDestructure(path, importManifest.defaultImportDirectAccesses, specifierIndex, defaultImportName);
}
// If we don't have a star import, named imports were done in the import statement.
// Otherwise, we need to destructure from the star import to get direct names from it.
if (starImportName !== null && importManifest.namedImportDirectAccesses.length > 0) {
insertImportDestructure(path, importManifest.namedImportDirectAccesses, specifierIndex, starImportName);
}
return path.node;
}
/**
* Turn an import string into an absolute path to a JS file.
*/
function resolveImportPath(importingFilePath, importPath) {
if (!importPath.endsWith('.js')) {
importPath += '.js';
}
if (importPath.startsWith('.')) {
var currentDir = (0, _path.dirname)(importingFilePath);
var relativePath = (0, _path.resolve)(currentDir, importPath);
if ((0, _fs.existsSync)(relativePath)) {
return relativePath;
}
} else {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = absoluteImportPaths[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var absoluteImportPath = _step.value;
var absolutePath = (0, _path.resolve)(absoluteImportPath, importPath);
if ((0, _fs.existsSync)(absolutePath)) {
return absolutePath;
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
return null;
}
/**
* Determine the names of all exports provided by a module and whether or not
* it has a default export.
*/
function getExportsInformation(filePath) {
var source = (0, _fs.readFileSync)(filePath).toString();
var otherRoot = j(source);
var hasDefaultExport = false;
var namedExports = [];
otherRoot.find(j.ExportNamedDeclaration).forEach(function (p) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = p.node.specifiers[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var specifier = _step2.value;
namedExports.push(specifier.exported.name);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
if (p.node.declaration) {
if (p.node.declaration.declarations) {
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = p.node.declaration.declarations[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var declaration = _step3.value;
namedExports.push(declaration.id.name);
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
}
if (p.node.declaration.id) {
namedExports.push(p.node.declaration.id.name);
}
}
});
otherRoot.find(j.ExportDefaultDeclaration).forEach(function () {
hasDefaultExport = true;
});
otherRoot.find(j.ExportAllDeclaration).forEach(function (p) {
var otherFilePath = resolveImportPath(filePath, p.node.source.value);
if (otherFilePath === null) {
return;
}
var otherFileExports = getExportsInformation(otherFilePath);
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4 = undefined;
try {
for (var _iterator4 = otherFileExports.namedExports[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var namedExport = _step4.value;
namedExports.push(namedExport);
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4.return) {
_iterator4.return();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
});
return { hasDefaultExport: hasDefaultExport, namedExports: namedExports };
}
/**
* Return an object that makes it more convenient to look up import specifiers
* rather than having to loop through the array.
*/
function getSpecifierIndex(path) {
var defaultImport = null;
var starImport = null;
var namedImportsByImportedName = new Map();
var _iteratorNormalCompletion5 = true;
var _didIteratorError5 = false;
var _iteratorError5 = undefined;
try {
for (var _iterator5 = path.node.specifiers[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
var specifier = _step5.value;
if (specifier.type === 'ImportDefaultSpecifier') {
defaultImport = specifier;
} else if (specifier.type === 'ImportNamespaceSpecifier') {
starImport = specifier;
} else if (specifier.type === 'ImportSpecifier') {
namedImportsByImportedName.set(specifier.imported.name, specifier);
}
}
} catch (err) {
_didIteratorError5 = true;
_iteratorError5 = err;
} finally {
try {
if (!_iteratorNormalCompletion5 && _iterator5.return) {
_iterator5.return();
}
} finally {
if (_didIteratorError5) {
throw _iteratorError5;
}
}
}
return {
defaultImport: defaultImport,
starImport: starImport,
namedImportsByImportedName: namedImportsByImportedName
};
}
/**
* Figure out what values are accessed from this import, including attributes
* pulled off of the default or star imports.
*/
function findAllMemberAccesses(specifierIndex) {
var defaultImportAccesses = [];
var starImportAccesses = [];
var directAccesses = [];
if (specifierIndex.defaultImport !== null) {
var name = specifierIndex.defaultImport.local.name;
defaultImportAccesses.push.apply(defaultImportAccesses, _toConsumableArray(getMemberAccessesForName(name)));
}
if (specifierIndex.starImport !== null) {
var _name = specifierIndex.starImport.local.name;
starImportAccesses.push.apply(starImportAccesses, _toConsumableArray(getMemberAccessesForName(_name)));
}
var _iteratorNormalCompletion6 = true;
var _didIteratorError6 = false;
var _iteratorError6 = undefined;
try {
for (var _iterator6 = specifierIndex.namedImportsByImportedName.values()[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
var specifier = _step6.value;
directAccesses.push(specifier.imported.name);
}
} catch (err) {
_didIteratorError6 = true;
_iteratorError6 = err;
} finally {
try {
if (!_iteratorNormalCompletion6 && _iterator6.return) {
_iterator6.return();
}
} finally {
if (_didIteratorError6) {
throw _iteratorError6;
}
}
}
return {
defaultImportAccesses: defaultImportAccesses,
starImportAccesses: starImportAccesses,
directAccesses: directAccesses
};
}
/**
* Given a name, find all cases in the code where a field is accessed from
* that name. For example, if objectName is Foo and the code contains Foo.a,
* Foo.b, and Foo.c, return the set {'a', 'b', 'c'}.
*/
function getMemberAccessesForName(objectName) {
var membersAccessed = new Set();
root.find(j.MemberExpression, {
object: {
name: objectName
}
}).forEach(function (path) {
if (path.node.property.type === 'Identifier') {
membersAccessed.add(path.node.property.name);
}
});
return membersAccessed;
}
/**
* Figure out what types of imports are needed in the resulting code based on
* what names are actually used and what names are exported by the other
* module.
*/
function getImportManifest(exportsInfo, memberAccesses) {
var defaultImportDirectAccesses = [];
var defaultImportObjectAccesses = [];
var namedImportDirectAccesses = [];
var namedImportObjectAccesses = [];
var exportedNames = new Set(exportsInfo.namedExports);
var _iteratorNormalCompletion7 = true;
var _didIteratorError7 = false;
var _iteratorError7 = undefined;
try {
for (var _iterator7 = memberAccesses.defaultImportAccesses[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
var name = _step7.value;
if (exportedNames.has(name)) {
namedImportObjectAccesses.push(name);
} else {
defaultImportObjectAccesses.push(name);
}
}
} catch (err) {
_didIteratorError7 = true;
_iteratorError7 = err;
} finally {
try {
if (!_iteratorNormalCompletion7 && _iterator7.return) {
_iterator7.return();
}
} finally {
if (_didIteratorError7) {
throw _iteratorError7;
}
}
}
var _iteratorNormalCompletion8 = true;
var _didIteratorError8 = false;
var _iteratorError8 = undefined;
try {
for (var _iterator8 = memberAccesses.starImportAccesses[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
var _name2 = _step8.value;
if (exportedNames.has(_name2)) {
namedImportObjectAccesses.push(_name2);
} else {
defaultImportObjectAccesses.push(_name2);
}
}
} catch (err) {
_didIteratorError8 = true;
_iteratorError8 = err;
} finally {
try {
if (!_iteratorNormalCompletion8 && _iterator8.return) {
_iterator8.return();
}
} finally {
if (_didIteratorError8) {
throw _iteratorError8;
}
}
}
var _iteratorNormalCompletion9 = true;
var _didIteratorError9 = false;
var _iteratorError9 = undefined;
try {
for (var _iterator9 = memberAccesses.directAccesses[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) {
var _name3 = _step9.value;
if (exportedNames.has(_name3)) {
namedImportDirectAccesses.push(_name3);
} else {
defaultImportDirectAccesses.push(_name3);
}
}
} catch (err) {
_didIteratorError9 = true;
_iteratorError9 = err;
} finally {
try {
if (!_iteratorNormalCompletion9 && _iterator9.return) {
_iterator9.return();
}
} finally {
if (_didIteratorError9) {
throw _iteratorError9;
}
}
}
return {
defaultImportDirectAccesses: defaultImportDirectAccesses,
defaultImportObjectAccesses: defaultImportObjectAccesses,
namedImportDirectAccesses: namedImportDirectAccesses,
namedImportObjectAccesses: namedImportObjectAccesses
};
}
/**
* Figure out what names to use for the default import and the import *
* values, based on the existing names (if any) and which ones we actually
* need.
*/
function resolveImportObjectNames(specifierIndex, needsDefaultImport, needsStarImport, hasDefaultExport, importPath) {
var existingDefaultImportName = specifierIndex.defaultImport && specifierIndex.defaultImport.local.name;
var existingStarImportName = specifierIndex.starImport && specifierIndex.starImport.local.name;
var defaultImportName = null;
var starImportName = null;
if ((!needsDefaultImport || existingDefaultImportName !== null) && (!needsStarImport || existingStarImportName !== null)) {
// If we already have all the names we need, then no name-generation required!
// Just use them.
if (needsDefaultImport) {
defaultImportName = existingDefaultImportName;
}
if (needsStarImport) {
starImportName = existingStarImportName;
}
} else if (needsDefaultImport && hasDefaultExport && existingDefaultImportName !== null) {
// If we potentially use the default import for anything other than object
// accesses, then we prefer to keep the name as-is, so special-case that.
defaultImportName = existingDefaultImportName;
if (needsStarImport) {
starImportName = findFreeName(defaultImportName + 'Exports');
}
} else if (needsDefaultImport) {
// Otherwise, we need to fill in at least one name and there aren't any
// specific constraints that we have to follow. Give the default import
// naming priority. If we also need a star import, give it a name based
// on our default name.
if (existingDefaultImportName !== null) {
defaultImportName = existingDefaultImportName;
} else if (existingStarImportName !== null && !needsStarImport) {
defaultImportName = existingStarImportName;
} else if (existingStarImportName !== null && needsStarImport) {
defaultImportName = findFreeName(existingStarImportName + 'Default');
} else {
defaultImportName = findFreeName(inferNameFromImportPath(importPath));
}
if (needsStarImport) {
if (existingStarImportName !== null) {
starImportName = existingStarImportName;
} else {
starImportName = findFreeName(defaultImportName + 'Exports');
}
}
} else if (needsStarImport) {
// Otherwise, we might need a star import name but no default import name.
// Try using the existing name or stealing from the default name if
// possible. If not, come up with a new name from the path.
if (existingStarImportName !== null) {
starImportName = existingStarImportName;
} else if (existingDefaultImportName !== null) {
starImportName = existingDefaultImportName;
} else {
starImportName = findFreeName(inferNameFromImportPath(importPath));
}
}
return { defaultImportName: defaultImportName, starImportName: starImportName };
}
/**
* Guess a nice capitalized camelCase name from a filename on an import. For
* example, './util/dashed-name' becomes 'DashedName'.
*/
function inferNameFromImportPath(importPath) {
var lastSlashIndex = importPath.lastIndexOf('/');
var filename = importPath;
if (lastSlashIndex > -1) {
filename = filename.substr(lastSlashIndex + 1);
}
if (filename.endsWith('.js')) {
filename = filename.substr(0, filename.length - 3);
}
return camelCaseName(filename);
}
/**
* Convert the given string to a capitalized camelCase name.
*
* Somewhat based on this discussion:
* http://stackoverflow.com/questions/2970525/converting-any-string-into-camel-case
*/
function camelCaseName(name) {
return name.replace(/(^|[ \-_])(.)/g, function (match) {
return match.toUpperCase();
}).replace(/[ \-_]/g, '');
}
/**
* Find a variable name that is unused in the code to avoid name clashes with
* existing names.
*/
function findFreeName(desiredName) {
if (!isNameTaken(desiredName)) {
return desiredName;
}
for (var i = 1; i < 5000; i++) {
var name = '' + desiredName + i;
if (!isNameTaken(name)) {
return name;
}
}
throw new Error('Could not find a suitable name.');
}
function isNameTaken(desiredName) {
return root.find(j.Identifier, { name: desiredName }).size() > 0;
}
/**
* Create the direct contents of the import statement. This may include a
* default import, a star import, and/or a list of named imports. Note that
* we are now allowed to have both a star import and named imports, so if we
* need both, we do a star import and will destructure it later.
*/
function createImportSpecifiers(defaultImportName, starImportName, specifierIndex, importManifest) {
var specifiers = [];
if (defaultImportName) {
if (specifierIndex.defaultImport !== null) {
specifiers.push(specifierIndex.defaultImport);
} else {
specifiers.push(j.importDefaultSpecifier(j.identifier(defaultImportName)));
}
}
if (starImportName) {
if (specifierIndex.starImport !== null) {
specifiers.push(specifierIndex.starImport);
} else {
specifiers.push(j.importNamespaceSpecifier(j.identifier(starImportName)));
}
}
// If we don't have a star import, named imports can go directly in the
// import statement. Otherwise we'll need to destructure them from the star
// import later.
if (!starImportName) {
var _iteratorNormalCompletion10 = true;
var _didIteratorError10 = false;
var _iteratorError10 = undefined;
try {
for (var _iterator10 = importManifest.namedImportDirectAccesses[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) {
var importName = _step10.value;
specifiers.push(specifierIndex.namedImportsByImportedName.get(importName));
}
} catch (err) {
_didIteratorError10 = true;
_iteratorError10 = err;
} finally {
try {
if (!_iteratorNormalCompletion10 && _iterator10.return) {
_iterator10.return();
}
} finally {
if (_didIteratorError10) {
throw _iteratorError10;
}
}
}
}
return specifiers;
}
/**
* Do a rename operation to handle object-style accesses. For example, if we
* have the import line `import Foo, * as FooExports from './Foo';` and a line
* in the code is `Foo.bar`, but `bar` is a named export on the foo module, we
* need to rename the reference in the code to `FooExports.bar`.
*/
function renameObjectAccesses(defaultImportName, starImportName, importManifest) {
var defaultImportProperties = new Set(importManifest.defaultImportObjectAccesses);
var starImportProperties = new Set(importManifest.namedImportObjectAccesses);
root.find(j.MemberExpression).replaceWith(function (path) {
var _path$node = path.node,
object = _path$node.object,
property = _path$node.property;
if (object.type !== 'Identifier' || object.name !== defaultImportName && object.name !== starImportName || property.type !== 'Identifier') {
return path.node;
}
if (defaultImportProperties.has(property.name)) {
object.name = defaultImportName;
}
if (starImportProperties.has(property.name)) {
object.name = starImportName;
}
return path.node;
});
}
/**
* Create a destructure statement after the import statement. This is a way
* to simulate named imports for default imports and star imports.
*/
function insertImportDestructure(path, importNames, specifierIndex, fullImportName) {
var destructureFields = importNames.map(function (importName) {
var specifier = specifierIndex.namedImportsByImportedName.get(importName);
return {
accessName: specifier.imported.name,
boundName: specifier.local.name
};
});
path.insertAfter(makeDestructureStatement(destructureFields, fullImportName));
}
function makeDestructureStatement(destructureFields, objName) {
var properties = destructureFields.map(function (_ref) {
var accessName = _ref.accessName,
boundName = _ref.boundName;
var property = j.property('init', j.identifier(accessName), j.identifier(boundName));
if (accessName === boundName) {
property.shorthand = true;
}
return property;
});
return j.variableDeclaration('const', [j.variableDeclarator(j.objectPattern(properties), j.identifier(objName))]);
}
return convertFile();
};
var _fs = require('fs');
var _path = require('path');
var _zlib = require('zlib');
var _zlib2 = _interopRequireDefault(_zlib);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } /**
* Script that fixes import styles to properly match the export style of the
* file being imported. Since decaffeinate doesn't do whole-codebase analysis,
* we need to do this as a follow-up step.
*
* Note that this conversion runs on ALL project files (or, at least, any files
* that could import a converted file). In general, it can handle any import
* statement, read the exports of the file being imported, and adjust the import
* usage to properly use the named/default exports as necessary.
*
* See the test examples starting with "fix-imports" for lots of examples.
*
* The script is quite thorough and mostly correct, but it can fail in the case
* of variable shadowing, dynamic usages of a default import or an import *
* object, or code that depends on the "live binding" behavior of imports, and
* likely other subtle cases.
*
* See https://github.com/decaffeinate/decaffeinate/issues/402 for some more
* details on why decaffeinate can't solve this itself.
*/
/**
* Little helper since we don't have Array.prototype.includes.
*/
function includes(arr, elem) {
return arr.indexOf(elem) > -1;
}