transform.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. function functionArgsFilter(j, path) {
  2. return ['err', 'error'].includes(path.get('params').value[0].name)
  3. }
  4. function functionBodyProcessor(j, path) {
  5. // the error variable should be the first parameter to the function
  6. const errorVarName = path.get('params').value[0].name
  7. j(path)
  8. // look for if statements
  9. .find(j.IfStatement)
  10. .filter(path => {
  11. let hasReturnError = false
  12. j(path)
  13. // find returns inside the if statement where the error from
  14. // the args is explicitly returned
  15. .find(j.ReturnStatement)
  16. .forEach(
  17. path =>
  18. (hasReturnError =
  19. path.value.argument.arguments[0].name === errorVarName)
  20. )
  21. return hasReturnError
  22. })
  23. .forEach(path => {
  24. j(path)
  25. // within the selected if blocks find calls to logger
  26. .find(j.CallExpression, {
  27. callee: {
  28. object: { name: 'logger' }
  29. }
  30. })
  31. // handle logger.warn, logger.error and logger.err
  32. .filter(path =>
  33. ['warn', 'error', 'err'].includes(
  34. path.get('callee').get('property').value.name
  35. )
  36. )
  37. // replace the logger call with the constructed OError wrapper
  38. .replaceWith(path => {
  39. // extract the error message which is the second arg for logger
  40. const message =
  41. path.value.arguments.length >= 2
  42. ? path.value.arguments[1].value
  43. : 'Error'
  44. // create: err = new OError(...)
  45. return j.assignmentExpression(
  46. '=',
  47. // assign over the existing error var
  48. j.identifier(errorVarName),
  49. j.callExpression(
  50. j.memberExpression(
  51. // create: new OError
  52. j.newExpression(j.identifier('OError'), [
  53. // create: { ... } args for new OError()
  54. j.objectExpression([
  55. // set message property with original error message
  56. j.property(
  57. 'init',
  58. j.identifier('message'),
  59. j.literal(message)
  60. ),
  61. j.property(
  62. 'init',
  63. // set info property with object { info: {} }
  64. j.identifier('info'),
  65. j.objectExpression(
  66. // add properties from original logger info object to the
  67. // OError info object, filtering out the err object itself,
  68. // which is typically one of the args when doing intermediate
  69. // error logging
  70. // TODO: this can fail when the property name does not match
  71. // the variable name. e.g. { err: error } so need to check
  72. // both in the filter
  73. path
  74. .get('arguments')
  75. .value[0].properties.filter(
  76. property => property.key.name !== errorVarName
  77. )
  78. )
  79. )
  80. ])
  81. ]),
  82. // add: .withCause( ) to OError
  83. j.identifier('withCause')
  84. ),
  85. // add original error var as argument: .withCause(err)
  86. [j.identifier(errorVarName)]
  87. )
  88. )
  89. })
  90. })
  91. }
  92. export default function transformer(file, api) {
  93. const j = api.jscodeshift
  94. let source = file.source
  95. // apply transformer to declared functions
  96. source = j(source)
  97. .find(j.FunctionDeclaration)
  98. .filter(path => functionArgsFilter(j, path))
  99. .forEach(path => functionBodyProcessor(j, path))
  100. .toSource()
  101. // apply transformer to inline-functions
  102. source = j(source)
  103. .find(j.FunctionExpression)
  104. .filter(path => functionArgsFilter(j, path))
  105. .forEach(path => functionBodyProcessor(j, path))
  106. .toSource()
  107. // apply transformer to inline-arrow-functions
  108. source = j(source)
  109. .find(j.ArrowFunctionExpression)
  110. .filter(path => functionArgsFilter(j, path))
  111. .forEach(path => functionBodyProcessor(j, path))
  112. .toSource()
  113. // do a plain text search to see if OError is used but not imported
  114. if (source.includes('OError') && !source.includes('@overleaf/o-error')) {
  115. const root = j(source)
  116. // assume the first variable declaration is an import
  117. // TODO: this should check that there is actually a require/import here
  118. // but in most cases it will be
  119. const imports = root.find(j.VariableDeclaration)
  120. const importOError = "const OError = require('@overleaf/o-error')\n"
  121. // if there were imports insert into list, format can re-order
  122. if (imports.length) {
  123. j(imports.at(0).get()).insertAfter(importOError)
  124. }
  125. // otherwise insert at beginning
  126. else {
  127. root.get().node.program.body.unshift(importOError)
  128. }
  129. source = root.toSource()
  130. }
  131. return source
  132. }