wrap-iife.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. /**
  2. * @fileoverview Rule to flag when IIFE is not wrapped in parens
  3. * @author Ilya Volodin
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. const eslintUtils = require("eslint-utils");
  11. //----------------------------------------------------------------------
  12. // Helpers
  13. //----------------------------------------------------------------------
  14. /**
  15. * Check if the given node is callee of a `NewExpression` node
  16. * @param {ASTNode} node node to check
  17. * @returns {boolean} True if the node is callee of a `NewExpression` node
  18. * @private
  19. */
  20. function isCalleeOfNewExpression(node) {
  21. const maybeCallee = node.parent.type === "ChainExpression"
  22. ? node.parent
  23. : node;
  24. return (
  25. maybeCallee.parent.type === "NewExpression" &&
  26. maybeCallee.parent.callee === maybeCallee
  27. );
  28. }
  29. //------------------------------------------------------------------------------
  30. // Rule Definition
  31. //------------------------------------------------------------------------------
  32. module.exports = {
  33. meta: {
  34. type: "layout",
  35. docs: {
  36. description: "require parentheses around immediate `function` invocations",
  37. category: "Best Practices",
  38. recommended: false,
  39. url: "https://eslint.org/docs/rules/wrap-iife"
  40. },
  41. schema: [
  42. {
  43. enum: ["outside", "inside", "any"]
  44. },
  45. {
  46. type: "object",
  47. properties: {
  48. functionPrototypeMethods: {
  49. type: "boolean",
  50. default: false
  51. }
  52. },
  53. additionalProperties: false
  54. }
  55. ],
  56. fixable: "code",
  57. messages: {
  58. wrapInvocation: "Wrap an immediate function invocation in parentheses.",
  59. wrapExpression: "Wrap only the function expression in parens.",
  60. moveInvocation: "Move the invocation into the parens that contain the function."
  61. }
  62. },
  63. create(context) {
  64. const style = context.options[0] || "outside";
  65. const includeFunctionPrototypeMethods = context.options[1] && context.options[1].functionPrototypeMethods;
  66. const sourceCode = context.getSourceCode();
  67. /**
  68. * Check if the node is wrapped in any (). All parens count: grouping parens and parens for constructs such as if()
  69. * @param {ASTNode} node node to evaluate
  70. * @returns {boolean} True if it is wrapped in any parens
  71. * @private
  72. */
  73. function isWrappedInAnyParens(node) {
  74. return astUtils.isParenthesised(sourceCode, node);
  75. }
  76. /**
  77. * Check if the node is wrapped in grouping (). Parens for constructs such as if() don't count
  78. * @param {ASTNode} node node to evaluate
  79. * @returns {boolean} True if it is wrapped in grouping parens
  80. * @private
  81. */
  82. function isWrappedInGroupingParens(node) {
  83. return eslintUtils.isParenthesized(1, node, sourceCode);
  84. }
  85. /**
  86. * Get the function node from an IIFE
  87. * @param {ASTNode} node node to evaluate
  88. * @returns {ASTNode} node that is the function expression of the given IIFE, or null if none exist
  89. */
  90. function getFunctionNodeFromIIFE(node) {
  91. const callee = astUtils.skipChainExpression(node.callee);
  92. if (callee.type === "FunctionExpression") {
  93. return callee;
  94. }
  95. if (includeFunctionPrototypeMethods &&
  96. callee.type === "MemberExpression" &&
  97. callee.object.type === "FunctionExpression" &&
  98. (astUtils.getStaticPropertyName(callee) === "call" || astUtils.getStaticPropertyName(callee) === "apply")
  99. ) {
  100. return callee.object;
  101. }
  102. return null;
  103. }
  104. return {
  105. CallExpression(node) {
  106. const innerNode = getFunctionNodeFromIIFE(node);
  107. if (!innerNode) {
  108. return;
  109. }
  110. const isCallExpressionWrapped = isWrappedInAnyParens(node),
  111. isFunctionExpressionWrapped = isWrappedInAnyParens(innerNode);
  112. if (!isCallExpressionWrapped && !isFunctionExpressionWrapped) {
  113. context.report({
  114. node,
  115. messageId: "wrapInvocation",
  116. fix(fixer) {
  117. const nodeToSurround = style === "inside" ? innerNode : node;
  118. return fixer.replaceText(nodeToSurround, `(${sourceCode.getText(nodeToSurround)})`);
  119. }
  120. });
  121. } else if (style === "inside" && !isFunctionExpressionWrapped) {
  122. context.report({
  123. node,
  124. messageId: "wrapExpression",
  125. fix(fixer) {
  126. // The outer call expression will always be wrapped at this point.
  127. if (isWrappedInGroupingParens(node) && !isCalleeOfNewExpression(node)) {
  128. /*
  129. * Parenthesize the function expression and remove unnecessary grouping parens around the call expression.
  130. * Replace the range between the end of the function expression and the end of the call expression.
  131. * for example, in `(function(foo) {}(bar))`, the range `(bar))` should get replaced with `)(bar)`.
  132. */
  133. const parenAfter = sourceCode.getTokenAfter(node);
  134. return fixer.replaceTextRange(
  135. [innerNode.range[1], parenAfter.range[1]],
  136. `)${sourceCode.getText().slice(innerNode.range[1], parenAfter.range[0])}`
  137. );
  138. }
  139. /*
  140. * Call expression is wrapped in mandatory parens such as if(), or in necessary grouping parens.
  141. * These parens cannot be removed, so just parenthesize the function expression.
  142. */
  143. return fixer.replaceText(innerNode, `(${sourceCode.getText(innerNode)})`);
  144. }
  145. });
  146. } else if (style === "outside" && !isCallExpressionWrapped) {
  147. context.report({
  148. node,
  149. messageId: "moveInvocation",
  150. fix(fixer) {
  151. /*
  152. * The inner function expression will always be wrapped at this point.
  153. * It's only necessary to replace the range between the end of the function expression
  154. * and the call expression. For example, in `(function(foo) {})(bar)`, the range `)(bar)`
  155. * should get replaced with `(bar))`.
  156. */
  157. const parenAfter = sourceCode.getTokenAfter(innerNode);
  158. return fixer.replaceTextRange(
  159. [parenAfter.range[0], node.range[1]],
  160. `${sourceCode.getText().slice(parenAfter.range[1], node.range[1])})`
  161. );
  162. }
  163. });
  164. }
  165. }
  166. };
  167. }
  168. };