platform-helpers/jscodeshift-scripts/helpers/transform-utils.js

'use strict';

// from https://raw.githubusercontent.com/sgilroy/async-await-codemod/master/lib/utils.js

function recursiveGetObjectPatternNames (node, names) {
  if (node.type === 'Identifier' && node.name) {
    names.push(node.name);
  } else if (node.type === 'ObjectPattern') {
    // recurse through the object pattern
    for (const property of node.properties) {
      if (property && property.key && property.key.type === 'Identifier') {
        if (property.value && property.value.type === 'Identifier') {
          names.push(property.value.name);
        } else if (property.value && property.value.type === 'ObjectPattern') {
          recursiveGetObjectPatternNames(property.value, names);
          names.push(property.key.name);
        }
      }
    }
  }
}

/**
 * Recursively walks up the path to find the names of all declarations in scope
 * @param {Path} p
 * @param {Array<String>} [names]
 * @return {Array<String>}
 */
function getNames (p, names = []) {
  if (p && p.value) {
    if (p.value.body && Array.isArray(p.value.body.body)) {
      for (const node of p.value.body.body) {
        if (node.declarations) {
          for (const declaration of node.declarations) {
            if (declaration.id) {
              if (declaration.id.type === 'ArrayPattern') {
                for (const element of declaration.id.elements) {
                  element && names.push(element.name);
                }
              } else if (declaration.id.name) {
                names.push(declaration.id.name);
              }
            }
          }
        }
        if (node.id && node.id.name) {
          names.push(node.id.name);
        }
      }
    }

    if (p.value.params) {
      for (const param of p.value.params) {
        recursiveGetObjectPatternNames(param, names);
      }
    }
  }

  if (p.parentPath) {
    return getNames(p.parentPath, names);
  } else {
    return names;
  }
}

const suffixLimit = 9;
function getUniqueName (namesInScope, param) {
  let safeName;
  let name = param.name;
  if (!name) {
    return;
  }
  let i = 1;
  do {
    if (!namesInScope.includes(name)) {
      safeName = name;
    } else {
      i++;
      name = param.name + i;
    }
  } while (!safeName && i < suffixLimit);

  return safeName;
}

function renameElement (j, p, parent, element, namesInScope) {
  const newName = getUniqueName(namesInScope, element);
  if (!newName || newName === element.name) {
    // no safe name or name already unique
    return;
  }

  const rootScope = p.scope;
  const oldName = element.name;

  // rename usages of the element
  // this borrows heavily from the renameTo transform from VariableDeclarator in jscodeshift
  j(parent)
    .find(j.Identifier, { name: oldName })
    .filter(function (path) {
      // ignore non-variables
      const parent = path.parent.node;

      if (
        j.MemberExpression.check(parent) &&
        parent.property === path.node &&
        !parent.computed
      ) {
        // obj.oldName
        return false;
      }

      if (
        j.Property.check(parent) &&
        parent.key === path.node &&
        !parent.computed
      ) {
        // { oldName: 3 }
        return false;
      }

      if (
        j.MethodDefinition.check(parent) &&
        parent.key === path.node &&
        !parent.computed
      ) {
        // class A { oldName() {} }
        return false;
      }

      if (
        j.JSXAttribute.check(parent) &&
        parent.name === path.node &&
        !parent.computed
      ) {
        // <Foo oldName={oldName} />
        return false;
      }

      return true;
    })
    .forEach(function (path) {
      let scope = path.scope;
      while (scope && scope !== rootScope) {
        if (scope.declares(oldName)) {
          return;
        }
        scope = scope.parent;
      }

      // identifier must refer to declared variable
      // It may look like we filtered out properties,
      // but the filter only ignored property "keys", not "value"s
      // In shorthand properties, "key" and "value" both have an
      // Identifier with the same structure.
      const parent = path.parent.node;
      if (j.Property.check(parent) && parent.shorthand && !parent.method) {
        path.parent.get('shorthand').replace(false);
      }

      path.get('name').replace(newName);
    });

  // rename the element declaration
  element.name = newName;
}

const extractNamesFromIdentifierLike = id => {
  if (!id) {
    return [];
  } else if (id.type === 'ObjectPattern') {
    return id.properties
      .map(
        d =>
          d.type === 'SpreadProperty'
            ? [d.argument.name]
            : extractNamesFromIdentifierLike(d.value)
      )
      .reduce((acc, val) => acc.concat(val), []);
  } else if (id.type === 'ArrayPattern') {
    return id.elements
      .map(extractNamesFromIdentifierLike)
      .reduce((acc, val) => acc.concat(val), []);
  } else if (id.type === 'Identifier') {
    return [id.name];
  } else if (id.type === 'RestElement') {
    return [id.argument.name];
  } else {
    return [];
  }
};

const isIdInElement = (element, name) => {
  return extractNamesFromIdentifierLike(element).indexOf(name) !== -1;
};

function isParamMutated (j, p, parent, element) {
  // detect any reassignments of the element
  const reassigned =
    j(parent)
      .find(j.AssignmentExpression)
      .filter(function (path) {
        return extractNamesFromIdentifierLike(path.value.left).some(name => {
          return isIdInElement(element, name);
        });
      })
      .size() > 0;

  // detect any update, such as i++ of the element
  const hasUpdateMutation =
    j(parent)
      .find(j.UpdateExpression)
      .filter(n => {
        return isIdInElement(element, n.value.argument.name);
      })
      .size() > 0;

  return reassigned || hasUpdateMutation;
}

module.exports = {
  isPromiseCall: node => {
    return (
      node &&
      node.type === 'CallExpression' &&
      node.callee.property &&
      (node.callee.property.name === 'then' ||
        node.callee.property.name === 'spread' ||
        (node.callee.property.name === 'catch' &&
          node.callee.object &&
          node.callee.object.type === 'CallExpression' &&
          node.callee.object.callee.property &&
          (node.callee.object.callee.property.name === 'then' ||
            node.callee.object.callee.property.name === 'spread')))
    );
  },

  genAwaitionDeclarator: (j, callExp, callBack, kind, params, exp) => {
    let declaratorId;
    if (
      params.length > 1 ||
      (callExp.callee &&
        callExp.callee.property &&
        callExp.callee.property.name === 'spread' &&
        callBack.params)
    ) {
      declaratorId = j.arrayPattern(params);
    } else {
      declaratorId = params[0];
    }

    return j.variableDeclaration(kind, [
      j.variableDeclarator(declaratorId, j.awaitExpression(exp))
    ]);
  },

  /**
   * Determine the appropriate callbacks from the .catch or .then arguments of the call expression.
   * @param {Node} callExp
   * @return {{errorCallBack: Node, callBack: Node, thenCalleeObject: Node}}
   */
  parseCallExpression: callExp => {
    let errorCallBack, callBack;
    let thenCalleeObject;
    if (callExp.callee.property && callExp.callee.property.name === 'catch') {
      errorCallBack = callExp.arguments[0];
      callBack = callExp.callee.object.arguments[0];
      thenCalleeObject = callExp.callee.object.callee.object;
    } else {
      callBack = callExp.arguments[0];
      thenCalleeObject = callExp.callee.object;

      if (callExp.arguments[1]) {
        errorCallBack = callExp.arguments[1];
      }
    }
    return { errorCallBack, callBack, thenCalleeObject };
  },

  /**
   * Resolves any name conflicts (renames the params) that would arise in path p from adding
   * variables based on the params of the callBack
   * @param j jscodeshift API facade
   * @param {Path} p The parent path
   * @param {Node} callBack
   */
  resolveParamNameConflicts: (j, p, callBack) => {
    const namesInScope = getNames(p);
    for (const param of callBack.params) {
      if (param.type === 'ArrayPattern') {
        for (const element of param.elements) {
          if (element) {
            renameElement(j, p, callBack.body, element, namesInScope);
          }
        }
      } else {
        renameElement(j, p, callBack.body, param, namesInScope);
      }
    }
  },

  /**
   * Renames any variable declarations or functions in body that would conflict with any names in path p
   * @param j jscodeshift API facade
   * @param {Path} p The parent path
   * @param {Body} body
   */
  resolveNameConflicts: (j, p, body) => {
    const namesInScope = getNames(p);
    for (const node of body.body) {
      if (node.declarations) {
        for (const declaration of node.declarations) {
          if (declaration.id) {
            if (declaration.id.type === 'ArrayPattern') {
              for (const element of declaration.id.elements) {
                renameElement(j, p, body, element, namesInScope);
              }
            } else if (declaration.id.name) {
              renameElement(j, p, body, declaration.id, namesInScope);
            }
          }
        }
      }
      if (node.id && node.id.name) {
        renameElement(j, p, body, node.id, namesInScope);
      }
    }
  },

  getParamsDeclarationKind: (j, p, callBack) => {
    let reassignment = false;
    for (const param of callBack.params) {
      if (isParamMutated(j, p, callBack.body, param)) {
        reassignment = true;
      }
    }

    // use const unless there is a mutation of one of the params
    return reassignment ? 'let' : 'const';
  }
};