1/** 2 * @fileoverview Comma spacing - validates spacing before and after comma 3 * @author Vignesh Anand aka vegetableman. 4 */ 5"use strict"; 6 7const astUtils = require("./utils/ast-utils"); 8 9//------------------------------------------------------------------------------ 10// Rule Definition 11//------------------------------------------------------------------------------ 12 13module.exports = { 14 meta: { 15 type: "layout", 16 17 docs: { 18 description: "enforce consistent spacing before and after commas", 19 category: "Stylistic Issues", 20 recommended: false, 21 url: "https://eslint.org/docs/rules/comma-spacing" 22 }, 23 24 fixable: "whitespace", 25 26 schema: [ 27 { 28 type: "object", 29 properties: { 30 before: { 31 type: "boolean", 32 default: false 33 }, 34 after: { 35 type: "boolean", 36 default: true 37 } 38 }, 39 additionalProperties: false 40 } 41 ], 42 43 messages: { 44 missing: "A space is required {{loc}} ','.", 45 unexpected: "There should be no space {{loc}} ','." 46 } 47 }, 48 49 create(context) { 50 51 const sourceCode = context.getSourceCode(); 52 const tokensAndComments = sourceCode.tokensAndComments; 53 54 const options = { 55 before: context.options[0] ? context.options[0].before : false, 56 after: context.options[0] ? context.options[0].after : true 57 }; 58 59 //-------------------------------------------------------------------------- 60 // Helpers 61 //-------------------------------------------------------------------------- 62 63 // list of comma tokens to ignore for the check of leading whitespace 64 const commaTokensToIgnore = []; 65 66 /** 67 * Reports a spacing error with an appropriate message. 68 * @param {ASTNode} node The binary expression node to report. 69 * @param {string} loc Is the error "before" or "after" the comma? 70 * @param {ASTNode} otherNode The node at the left or right of `node` 71 * @returns {void} 72 * @private 73 */ 74 function report(node, loc, otherNode) { 75 context.report({ 76 node, 77 fix(fixer) { 78 if (options[loc]) { 79 if (loc === "before") { 80 return fixer.insertTextBefore(node, " "); 81 } 82 return fixer.insertTextAfter(node, " "); 83 84 } 85 let start, end; 86 const newText = ""; 87 88 if (loc === "before") { 89 start = otherNode.range[1]; 90 end = node.range[0]; 91 } else { 92 start = node.range[1]; 93 end = otherNode.range[0]; 94 } 95 96 return fixer.replaceTextRange([start, end], newText); 97 98 }, 99 messageId: options[loc] ? "missing" : "unexpected", 100 data: { 101 loc 102 } 103 }); 104 } 105 106 /** 107 * Validates the spacing around a comma token. 108 * @param {Object} tokens The tokens to be validated. 109 * @param {Token} tokens.comma The token representing the comma. 110 * @param {Token} [tokens.left] The last token before the comma. 111 * @param {Token} [tokens.right] The first token after the comma. 112 * @param {Token|ASTNode} reportItem The item to use when reporting an error. 113 * @returns {void} 114 * @private 115 */ 116 function validateCommaItemSpacing(tokens, reportItem) { 117 if (tokens.left && astUtils.isTokenOnSameLine(tokens.left, tokens.comma) && 118 (options.before !== sourceCode.isSpaceBetweenTokens(tokens.left, tokens.comma)) 119 ) { 120 report(reportItem, "before", tokens.left); 121 } 122 123 if (tokens.right && astUtils.isClosingParenToken(tokens.right)) { 124 return; 125 } 126 127 if (tokens.right && !options.after && tokens.right.type === "Line") { 128 return; 129 } 130 131 if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) && 132 (options.after !== sourceCode.isSpaceBetweenTokens(tokens.comma, tokens.right)) 133 ) { 134 report(reportItem, "after", tokens.right); 135 } 136 } 137 138 /** 139 * Adds null elements of the given ArrayExpression or ArrayPattern node to the ignore list. 140 * @param {ASTNode} node An ArrayExpression or ArrayPattern node. 141 * @returns {void} 142 */ 143 function addNullElementsToIgnoreList(node) { 144 let previousToken = sourceCode.getFirstToken(node); 145 146 node.elements.forEach(element => { 147 let token; 148 149 if (element === null) { 150 token = sourceCode.getTokenAfter(previousToken); 151 152 if (astUtils.isCommaToken(token)) { 153 commaTokensToIgnore.push(token); 154 } 155 } else { 156 token = sourceCode.getTokenAfter(element); 157 } 158 159 previousToken = token; 160 }); 161 } 162 163 //-------------------------------------------------------------------------- 164 // Public 165 //-------------------------------------------------------------------------- 166 167 return { 168 "Program:exit"() { 169 tokensAndComments.forEach((token, i) => { 170 171 if (!astUtils.isCommaToken(token)) { 172 return; 173 } 174 175 if (token && token.type === "JSXText") { 176 return; 177 } 178 179 const previousToken = tokensAndComments[i - 1]; 180 const nextToken = tokensAndComments[i + 1]; 181 182 validateCommaItemSpacing({ 183 comma: token, 184 left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken, 185 right: astUtils.isCommaToken(nextToken) ? null : nextToken 186 }, token); 187 }); 188 }, 189 ArrayExpression: addNullElementsToIgnoreList, 190 ArrayPattern: addNullElementsToIgnoreList 191 192 }; 193 194 } 195}; 196