1/** 2 * @fileoverview Comma style - enforces comma styles of two types: last and first 3 * @author Vignesh Anand aka vegetableman 4 */ 5 6"use strict"; 7 8const astUtils = require("./utils/ast-utils"); 9 10//------------------------------------------------------------------------------ 11// Rule Definition 12//------------------------------------------------------------------------------ 13 14module.exports = { 15 meta: { 16 type: "layout", 17 18 docs: { 19 description: "enforce consistent comma style", 20 category: "Stylistic Issues", 21 recommended: false, 22 url: "https://eslint.org/docs/rules/comma-style" 23 }, 24 25 fixable: "code", 26 27 schema: [ 28 { 29 enum: ["first", "last"] 30 }, 31 { 32 type: "object", 33 properties: { 34 exceptions: { 35 type: "object", 36 additionalProperties: { 37 type: "boolean" 38 } 39 } 40 }, 41 additionalProperties: false 42 } 43 ], 44 45 messages: { 46 unexpectedLineBeforeAndAfterComma: "Bad line breaking before and after ','.", 47 expectedCommaFirst: "',' should be placed first.", 48 expectedCommaLast: "',' should be placed last." 49 } 50 }, 51 52 create(context) { 53 const style = context.options[0] || "last", 54 sourceCode = context.getSourceCode(); 55 const exceptions = { 56 ArrayPattern: true, 57 ArrowFunctionExpression: true, 58 CallExpression: true, 59 FunctionDeclaration: true, 60 FunctionExpression: true, 61 ImportDeclaration: true, 62 ObjectPattern: true, 63 NewExpression: true 64 }; 65 66 if (context.options.length === 2 && Object.prototype.hasOwnProperty.call(context.options[1], "exceptions")) { 67 const keys = Object.keys(context.options[1].exceptions); 68 69 for (let i = 0; i < keys.length; i++) { 70 exceptions[keys[i]] = context.options[1].exceptions[keys[i]]; 71 } 72 } 73 74 //-------------------------------------------------------------------------- 75 // Helpers 76 //-------------------------------------------------------------------------- 77 78 /** 79 * Modified text based on the style 80 * @param {string} styleType Style type 81 * @param {string} text Source code text 82 * @returns {string} modified text 83 * @private 84 */ 85 function getReplacedText(styleType, text) { 86 switch (styleType) { 87 case "between": 88 return `,${text.replace(astUtils.LINEBREAK_MATCHER, "")}`; 89 90 case "first": 91 return `${text},`; 92 93 case "last": 94 return `,${text}`; 95 96 default: 97 return ""; 98 } 99 } 100 101 /** 102 * Determines the fixer function for a given style. 103 * @param {string} styleType comma style 104 * @param {ASTNode} previousItemToken The token to check. 105 * @param {ASTNode} commaToken The token to check. 106 * @param {ASTNode} currentItemToken The token to check. 107 * @returns {Function} Fixer function 108 * @private 109 */ 110 function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) { 111 const text = 112 sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) + 113 sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]); 114 const range = [previousItemToken.range[1], currentItemToken.range[0]]; 115 116 return function(fixer) { 117 return fixer.replaceTextRange(range, getReplacedText(styleType, text)); 118 }; 119 } 120 121 /** 122 * Validates the spacing around single items in lists. 123 * @param {Token} previousItemToken The last token from the previous item. 124 * @param {Token} commaToken The token representing the comma. 125 * @param {Token} currentItemToken The first token of the current item. 126 * @param {Token} reportItem The item to use when reporting an error. 127 * @returns {void} 128 * @private 129 */ 130 function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) { 131 132 // if single line 133 if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) && 134 astUtils.isTokenOnSameLine(previousItemToken, commaToken)) { 135 136 // do nothing. 137 138 } else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) && 139 !astUtils.isTokenOnSameLine(previousItemToken, commaToken)) { 140 141 const comment = sourceCode.getCommentsAfter(commaToken)[0]; 142 const styleType = comment && comment.type === "Block" && astUtils.isTokenOnSameLine(commaToken, comment) 143 ? style 144 : "between"; 145 146 // lone comma 147 context.report({ 148 node: reportItem, 149 loc: commaToken.loc, 150 messageId: "unexpectedLineBeforeAndAfterComma", 151 fix: getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) 152 }); 153 154 } else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) { 155 156 context.report({ 157 node: reportItem, 158 loc: commaToken.loc, 159 messageId: "expectedCommaFirst", 160 fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken) 161 }); 162 163 } else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) { 164 165 context.report({ 166 node: reportItem, 167 loc: commaToken.loc, 168 messageId: "expectedCommaLast", 169 fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken) 170 }); 171 } 172 } 173 174 /** 175 * Checks the comma placement with regards to a declaration/property/element 176 * @param {ASTNode} node The binary expression node to check 177 * @param {string} property The property of the node containing child nodes. 178 * @private 179 * @returns {void} 180 */ 181 function validateComma(node, property) { 182 const items = node[property], 183 arrayLiteral = (node.type === "ArrayExpression" || node.type === "ArrayPattern"); 184 185 if (items.length > 1 || arrayLiteral) { 186 187 // seed as opening [ 188 let previousItemToken = sourceCode.getFirstToken(node); 189 190 items.forEach(item => { 191 const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken, 192 currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken), 193 reportItem = item || currentItemToken; 194 195 /* 196 * This works by comparing three token locations: 197 * - previousItemToken is the last token of the previous item 198 * - commaToken is the location of the comma before the current item 199 * - currentItemToken is the first token of the current item 200 * 201 * These values get switched around if item is undefined. 202 * previousItemToken will refer to the last token not belonging 203 * to the current item, which could be a comma or an opening 204 * square bracket. currentItemToken could be a comma. 205 * 206 * All comparisons are done based on these tokens directly, so 207 * they are always valid regardless of an undefined item. 208 */ 209 if (astUtils.isCommaToken(commaToken)) { 210 validateCommaItemSpacing(previousItemToken, commaToken, 211 currentItemToken, reportItem); 212 } 213 214 if (item) { 215 const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken); 216 217 previousItemToken = tokenAfterItem 218 ? sourceCode.getTokenBefore(tokenAfterItem) 219 : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1]; 220 } 221 }); 222 223 /* 224 * Special case for array literals that have empty last items, such 225 * as [ 1, 2, ]. These arrays only have two items show up in the 226 * AST, so we need to look at the token to verify that there's no 227 * dangling comma. 228 */ 229 if (arrayLiteral) { 230 231 const lastToken = sourceCode.getLastToken(node), 232 nextToLastToken = sourceCode.getTokenBefore(lastToken); 233 234 if (astUtils.isCommaToken(nextToLastToken)) { 235 validateCommaItemSpacing( 236 sourceCode.getTokenBefore(nextToLastToken), 237 nextToLastToken, 238 lastToken, 239 lastToken 240 ); 241 } 242 } 243 } 244 } 245 246 //-------------------------------------------------------------------------- 247 // Public 248 //-------------------------------------------------------------------------- 249 250 const nodes = {}; 251 252 if (!exceptions.VariableDeclaration) { 253 nodes.VariableDeclaration = function(node) { 254 validateComma(node, "declarations"); 255 }; 256 } 257 if (!exceptions.ObjectExpression) { 258 nodes.ObjectExpression = function(node) { 259 validateComma(node, "properties"); 260 }; 261 } 262 if (!exceptions.ObjectPattern) { 263 nodes.ObjectPattern = function(node) { 264 validateComma(node, "properties"); 265 }; 266 } 267 if (!exceptions.ArrayExpression) { 268 nodes.ArrayExpression = function(node) { 269 validateComma(node, "elements"); 270 }; 271 } 272 if (!exceptions.ArrayPattern) { 273 nodes.ArrayPattern = function(node) { 274 validateComma(node, "elements"); 275 }; 276 } 277 if (!exceptions.FunctionDeclaration) { 278 nodes.FunctionDeclaration = function(node) { 279 validateComma(node, "params"); 280 }; 281 } 282 if (!exceptions.FunctionExpression) { 283 nodes.FunctionExpression = function(node) { 284 validateComma(node, "params"); 285 }; 286 } 287 if (!exceptions.ArrowFunctionExpression) { 288 nodes.ArrowFunctionExpression = function(node) { 289 validateComma(node, "params"); 290 }; 291 } 292 if (!exceptions.CallExpression) { 293 nodes.CallExpression = function(node) { 294 validateComma(node, "arguments"); 295 }; 296 } 297 if (!exceptions.ImportDeclaration) { 298 nodes.ImportDeclaration = function(node) { 299 validateComma(node, "specifiers"); 300 }; 301 } 302 if (!exceptions.NewExpression) { 303 nodes.NewExpression = function(node) { 304 validateComma(node, "arguments"); 305 }; 306 } 307 308 return nodes; 309 } 310}; 311