1/** 2 * @fileoverview Rule to validate spacing before function paren. 3 * @author Mathias Schreck <https://github.com/lo1tuma> 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const astUtils = require("./utils/ast-utils"); 12 13//------------------------------------------------------------------------------ 14// Rule Definition 15//------------------------------------------------------------------------------ 16 17module.exports = { 18 meta: { 19 type: "layout", 20 21 docs: { 22 description: "enforce consistent spacing before `function` definition opening parenthesis", 23 category: "Stylistic Issues", 24 recommended: false, 25 url: "https://eslint.org/docs/rules/space-before-function-paren" 26 }, 27 28 fixable: "whitespace", 29 30 schema: [ 31 { 32 oneOf: [ 33 { 34 enum: ["always", "never"] 35 }, 36 { 37 type: "object", 38 properties: { 39 anonymous: { 40 enum: ["always", "never", "ignore"] 41 }, 42 named: { 43 enum: ["always", "never", "ignore"] 44 }, 45 asyncArrow: { 46 enum: ["always", "never", "ignore"] 47 } 48 }, 49 additionalProperties: false 50 } 51 ] 52 } 53 ], 54 55 messages: { 56 unexpectedSpace: "Unexpected space before function parentheses.", 57 missingSpace: "Missing space before function parentheses." 58 } 59 }, 60 61 create(context) { 62 const sourceCode = context.getSourceCode(); 63 const baseConfig = typeof context.options[0] === "string" ? context.options[0] : "always"; 64 const overrideConfig = typeof context.options[0] === "object" ? context.options[0] : {}; 65 66 /** 67 * Determines whether a function has a name. 68 * @param {ASTNode} node The function node. 69 * @returns {boolean} Whether the function has a name. 70 */ 71 function isNamedFunction(node) { 72 if (node.id) { 73 return true; 74 } 75 76 const parent = node.parent; 77 78 return parent.type === "MethodDefinition" || 79 (parent.type === "Property" && 80 ( 81 parent.kind === "get" || 82 parent.kind === "set" || 83 parent.method 84 ) 85 ); 86 } 87 88 /** 89 * Gets the config for a given function 90 * @param {ASTNode} node The function node 91 * @returns {string} "always", "never", or "ignore" 92 */ 93 function getConfigForFunction(node) { 94 if (node.type === "ArrowFunctionExpression") { 95 96 // Always ignore non-async functions and arrow functions without parens, e.g. async foo => bar 97 if (node.async && astUtils.isOpeningParenToken(sourceCode.getFirstToken(node, { skip: 1 }))) { 98 return overrideConfig.asyncArrow || baseConfig; 99 } 100 } else if (isNamedFunction(node)) { 101 return overrideConfig.named || baseConfig; 102 103 // `generator-star-spacing` should warn anonymous generators. E.g. `function* () {}` 104 } else if (!node.generator) { 105 return overrideConfig.anonymous || baseConfig; 106 } 107 108 return "ignore"; 109 } 110 111 /** 112 * Checks the parens of a function node 113 * @param {ASTNode} node A function node 114 * @returns {void} 115 */ 116 function checkFunction(node) { 117 const functionConfig = getConfigForFunction(node); 118 119 if (functionConfig === "ignore") { 120 return; 121 } 122 123 const rightToken = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken); 124 const leftToken = sourceCode.getTokenBefore(rightToken); 125 const hasSpacing = sourceCode.isSpaceBetweenTokens(leftToken, rightToken); 126 127 if (hasSpacing && functionConfig === "never") { 128 context.report({ 129 node, 130 loc: { 131 start: leftToken.loc.end, 132 end: rightToken.loc.start 133 }, 134 messageId: "unexpectedSpace", 135 fix(fixer) { 136 const comments = sourceCode.getCommentsBefore(rightToken); 137 138 // Don't fix anything if there's a single line comment between the left and the right token 139 if (comments.some(comment => comment.type === "Line")) { 140 return null; 141 } 142 return fixer.replaceTextRange( 143 [leftToken.range[1], rightToken.range[0]], 144 comments.reduce((text, comment) => text + sourceCode.getText(comment), "") 145 ); 146 } 147 }); 148 } else if (!hasSpacing && functionConfig === "always") { 149 context.report({ 150 node, 151 loc: rightToken.loc, 152 messageId: "missingSpace", 153 fix: fixer => fixer.insertTextAfter(leftToken, " ") 154 }); 155 } 156 } 157 158 return { 159 ArrowFunctionExpression: checkFunction, 160 FunctionDeclaration: checkFunction, 161 FunctionExpression: checkFunction 162 }; 163 } 164}; 165