process-exit-as-throw.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /* eslint-disable eslint-plugin/prefer-message-ids */
  2. /**
  3. * @author Toru Nagashima
  4. * See LICENSE file in root directory for full license.
  5. */
  6. "use strict"
  7. const CodePathAnalyzer = safeRequire(
  8. "eslint/lib/linter/code-path-analysis/code-path-analyzer",
  9. "eslint/lib/code-path-analysis/code-path-analyzer"
  10. )
  11. const CodePathSegment = safeRequire(
  12. "eslint/lib/linter/code-path-analysis/code-path-segment",
  13. "eslint/lib/code-path-analysis/code-path-segment"
  14. )
  15. const CodePath = safeRequire(
  16. "eslint/lib/linter/code-path-analysis/code-path",
  17. "eslint/lib/code-path-analysis/code-path"
  18. )
  19. const originalLeaveNode =
  20. CodePathAnalyzer && CodePathAnalyzer.prototype.leaveNode
  21. /**
  22. * Imports a specific module.
  23. * @param {...string} moduleNames - module names to import.
  24. * @returns {object|null} The imported object, or null.
  25. */
  26. function safeRequire(...moduleNames) {
  27. for (const moduleName of moduleNames) {
  28. try {
  29. return require(moduleName)
  30. } catch (_err) {
  31. // Ignore.
  32. }
  33. }
  34. return null
  35. }
  36. /* istanbul ignore next */
  37. /**
  38. * Copied from https://github.com/eslint/eslint/blob/16fad5880bb70e9dddbeab8ed0f425ae51f5841f/lib/code-path-analysis/code-path-analyzer.js#L137
  39. *
  40. * @param {CodePathAnalyzer} analyzer - The instance.
  41. * @param {ASTNode} node - The current AST node.
  42. * @returns {void}
  43. */
  44. function forwardCurrentToHead(analyzer, node) {
  45. const codePath = analyzer.codePath
  46. const state = CodePath.getState(codePath)
  47. const currentSegments = state.currentSegments
  48. const headSegments = state.headSegments
  49. const end = Math.max(currentSegments.length, headSegments.length)
  50. let i = 0
  51. let currentSegment = null
  52. let headSegment = null
  53. // Fires leaving events.
  54. for (i = 0; i < end; ++i) {
  55. currentSegment = currentSegments[i]
  56. headSegment = headSegments[i]
  57. if (currentSegment !== headSegment && currentSegment) {
  58. if (currentSegment.reachable) {
  59. analyzer.emitter.emit(
  60. "onCodePathSegmentEnd",
  61. currentSegment,
  62. node
  63. )
  64. }
  65. }
  66. }
  67. // Update state.
  68. state.currentSegments = headSegments
  69. // Fires entering events.
  70. for (i = 0; i < end; ++i) {
  71. currentSegment = currentSegments[i]
  72. headSegment = headSegments[i]
  73. if (currentSegment !== headSegment && headSegment) {
  74. CodePathSegment.markUsed(headSegment)
  75. if (headSegment.reachable) {
  76. analyzer.emitter.emit(
  77. "onCodePathSegmentStart",
  78. headSegment,
  79. node
  80. )
  81. }
  82. }
  83. }
  84. }
  85. /**
  86. * Checks whether a given node is `process.exit()` or not.
  87. *
  88. * @param {ASTNode} node - A node to check.
  89. * @returns {boolean} `true` if the node is `process.exit()`.
  90. */
  91. function isProcessExit(node) {
  92. return (
  93. node.type === "CallExpression" &&
  94. node.callee.type === "MemberExpression" &&
  95. node.callee.computed === false &&
  96. node.callee.object.type === "Identifier" &&
  97. node.callee.object.name === "process" &&
  98. node.callee.property.type === "Identifier" &&
  99. node.callee.property.name === "exit"
  100. )
  101. }
  102. /**
  103. * The function to override `CodePathAnalyzer.prototype.leaveNode` in order to
  104. * address `process.exit()` as throw.
  105. *
  106. * @this CodePathAnalyzer
  107. * @param {ASTNode} node - A node to be left.
  108. * @returns {void}
  109. */
  110. function overrideLeaveNode(node) {
  111. if (isProcessExit(node)) {
  112. this.currentNode = node
  113. forwardCurrentToHead(this, node)
  114. CodePath.getState(this.codePath).makeThrow()
  115. this.original.leaveNode(node)
  116. this.currentNode = null
  117. } else {
  118. originalLeaveNode.call(this, node)
  119. }
  120. }
  121. const visitor =
  122. CodePathAnalyzer == null
  123. ? {}
  124. : {
  125. Program: function installProcessExitAsThrow() {
  126. CodePathAnalyzer.prototype.leaveNode = overrideLeaveNode
  127. },
  128. "Program:exit": function restoreProcessExitAsThrow() {
  129. CodePathAnalyzer.prototype.leaveNode = originalLeaveNode
  130. },
  131. }
  132. module.exports = {
  133. meta: {
  134. docs: {
  135. description:
  136. "require that `process.exit()` expressions use the same code path as `throw`",
  137. category: "Possible Errors",
  138. recommended: true,
  139. url: "https://github.com/weiran-zsd/eslint-plugin-node/blob/HEAD/docs/rules/process-exit-as-throw.md",
  140. },
  141. type: "problem",
  142. fixable: null,
  143. schema: [],
  144. supported: CodePathAnalyzer != null,
  145. },
  146. create() {
  147. return visitor
  148. },
  149. }