convertThisToCtx.js 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. /**
  2. * @typedef {import('jscodeshift').ASTPath} ASTPath
  3. * @typedef {import('jscodeshift').JSCodeshift} JSCodeshift
  4. */
  5. const TARGET_CALLER_NAMES = new Set([
  6. 'describe',
  7. 'it',
  8. 'before',
  9. 'beforeEach',
  10. 'after',
  11. 'afterEach',
  12. ])
  13. /**
  14. * Helper function to check if a 'this' expression belongs directly to a given function scope,
  15. * and not to a nested traditional function defined within that scope.
  16. * @param {ASTPath<ThisExpression>} thisPath - The path to the 'this' expression.
  17. * @param {ASTPath<Function>} targetFunctionPath - The path to the target function scope.
  18. * @param {JSCodeshift} j - The jscodeshift instance.
  19. * @returns {boolean} - True if 'this' belongs to the target function scope.
  20. */
  21. function isThisFromScope(thisPath, targetFunctionPath, j) {
  22. let current = thisPath.parentPath
  23. while (current && current.node !== targetFunctionPath.node) {
  24. if (
  25. (j.FunctionExpression.check(current.node) ||
  26. j.FunctionDeclaration.check(current.node)) &&
  27. current.node !== targetFunctionPath.node
  28. ) {
  29. return false
  30. }
  31. current = current.parentPath
  32. }
  33. return !!current && current.node === targetFunctionPath.node
  34. }
  35. module.exports = function transformer(file, api) {
  36. const j = api.jscodeshift
  37. const root = j(file.source)
  38. const functionsToModify = new Set()
  39. root.find(j.CallExpression).forEach(callPath => {
  40. const callNode = callPath.node
  41. if (
  42. j.Identifier.check(callNode.callee) &&
  43. TARGET_CALLER_NAMES.has(callNode.callee.name)
  44. ) {
  45. callNode.arguments.forEach((arg, index) => {
  46. if (
  47. j.FunctionExpression.check(arg) ||
  48. j.FunctionDeclaration.check(arg)
  49. ) {
  50. const functionArgumentPath = callPath.get('arguments', index)
  51. const containsRelevantThis = j(functionArgumentPath)
  52. .find(j.ThisExpression)
  53. .some(thisPath =>
  54. isThisFromScope(thisPath, functionArgumentPath, j)
  55. )
  56. if (containsRelevantThis) {
  57. functionsToModify.add(functionArgumentPath)
  58. }
  59. }
  60. })
  61. }
  62. })
  63. functionsToModify.forEach((functionPath /*: ASTPath<Function> */) => {
  64. const functionNode = functionPath.node
  65. const hasCtxParam = functionNode.params.some(
  66. param => j.Identifier.check(param) && param.name === 'ctx'
  67. )
  68. if (!hasCtxParam) {
  69. functionNode.params.push(j.identifier('ctx'))
  70. }
  71. j(functionPath)
  72. .find(j.ThisExpression)
  73. .filter(thisPath => isThisFromScope(thisPath, functionPath, j))
  74. .replaceWith(j.identifier('ctx'))
  75. })
  76. return root.toSource({ quote: 'single' })
  77. }