1/** 2 * @fileoverview Rule to forbid or enforce dangling commas. 3 * @author Ian Christian Myers 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const lodash = require("lodash"); 13const astUtils = require("./utils/ast-utils"); 14 15//------------------------------------------------------------------------------ 16// Helpers 17//------------------------------------------------------------------------------ 18 19const DEFAULT_OPTIONS = Object.freeze({ 20 arrays: "never", 21 objects: "never", 22 imports: "never", 23 exports: "never", 24 functions: "never" 25}); 26 27/** 28 * Checks whether or not a trailing comma is allowed in a given node. 29 * If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas. 30 * @param {ASTNode} lastItem The node of the last element in the given node. 31 * @returns {boolean} `true` if a trailing comma is allowed. 32 */ 33function isTrailingCommaAllowed(lastItem) { 34 return !( 35 lastItem.type === "RestElement" || 36 lastItem.type === "RestProperty" || 37 lastItem.type === "ExperimentalRestProperty" 38 ); 39} 40 41/** 42 * Normalize option value. 43 * @param {string|Object|undefined} optionValue The 1st option value to normalize. 44 * @param {number} ecmaVersion The normalized ECMAScript version. 45 * @returns {Object} The normalized option value. 46 */ 47function normalizeOptions(optionValue, ecmaVersion) { 48 if (typeof optionValue === "string") { 49 return { 50 arrays: optionValue, 51 objects: optionValue, 52 imports: optionValue, 53 exports: optionValue, 54 functions: (!ecmaVersion || ecmaVersion < 8) ? "ignore" : optionValue 55 }; 56 } 57 if (typeof optionValue === "object" && optionValue !== null) { 58 return { 59 arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays, 60 objects: optionValue.objects || DEFAULT_OPTIONS.objects, 61 imports: optionValue.imports || DEFAULT_OPTIONS.imports, 62 exports: optionValue.exports || DEFAULT_OPTIONS.exports, 63 functions: optionValue.functions || DEFAULT_OPTIONS.functions 64 }; 65 } 66 67 return DEFAULT_OPTIONS; 68} 69 70//------------------------------------------------------------------------------ 71// Rule Definition 72//------------------------------------------------------------------------------ 73 74module.exports = { 75 meta: { 76 type: "layout", 77 78 docs: { 79 description: "require or disallow trailing commas", 80 category: "Stylistic Issues", 81 recommended: false, 82 url: "https://eslint.org/docs/rules/comma-dangle" 83 }, 84 85 fixable: "code", 86 87 schema: { 88 definitions: { 89 value: { 90 enum: [ 91 "always-multiline", 92 "always", 93 "never", 94 "only-multiline" 95 ] 96 }, 97 valueWithIgnore: { 98 enum: [ 99 "always-multiline", 100 "always", 101 "ignore", 102 "never", 103 "only-multiline" 104 ] 105 } 106 }, 107 type: "array", 108 items: [ 109 { 110 oneOf: [ 111 { 112 $ref: "#/definitions/value" 113 }, 114 { 115 type: "object", 116 properties: { 117 arrays: { $ref: "#/definitions/valueWithIgnore" }, 118 objects: { $ref: "#/definitions/valueWithIgnore" }, 119 imports: { $ref: "#/definitions/valueWithIgnore" }, 120 exports: { $ref: "#/definitions/valueWithIgnore" }, 121 functions: { $ref: "#/definitions/valueWithIgnore" } 122 }, 123 additionalProperties: false 124 } 125 ] 126 } 127 ] 128 }, 129 130 messages: { 131 unexpected: "Unexpected trailing comma.", 132 missing: "Missing trailing comma." 133 } 134 }, 135 136 create(context) { 137 const options = normalizeOptions(context.options[0], context.parserOptions.ecmaVersion); 138 139 const sourceCode = context.getSourceCode(); 140 141 /** 142 * Gets the last item of the given node. 143 * @param {ASTNode} node The node to get. 144 * @returns {ASTNode|null} The last node or null. 145 */ 146 function getLastItem(node) { 147 switch (node.type) { 148 case "ObjectExpression": 149 case "ObjectPattern": 150 return lodash.last(node.properties); 151 case "ArrayExpression": 152 case "ArrayPattern": 153 return lodash.last(node.elements); 154 case "ImportDeclaration": 155 case "ExportNamedDeclaration": 156 return lodash.last(node.specifiers); 157 case "FunctionDeclaration": 158 case "FunctionExpression": 159 case "ArrowFunctionExpression": 160 return lodash.last(node.params); 161 case "CallExpression": 162 case "NewExpression": 163 return lodash.last(node.arguments); 164 default: 165 return null; 166 } 167 } 168 169 /** 170 * Gets the trailing comma token of the given node. 171 * If the trailing comma does not exist, this returns the token which is 172 * the insertion point of the trailing comma token. 173 * @param {ASTNode} node The node to get. 174 * @param {ASTNode} lastItem The last item of the node. 175 * @returns {Token} The trailing comma token or the insertion point. 176 */ 177 function getTrailingToken(node, lastItem) { 178 switch (node.type) { 179 case "ObjectExpression": 180 case "ArrayExpression": 181 case "CallExpression": 182 case "NewExpression": 183 return sourceCode.getLastToken(node, 1); 184 default: { 185 const nextToken = sourceCode.getTokenAfter(lastItem); 186 187 if (astUtils.isCommaToken(nextToken)) { 188 return nextToken; 189 } 190 return sourceCode.getLastToken(lastItem); 191 } 192 } 193 } 194 195 /** 196 * Checks whether or not a given node is multiline. 197 * This rule handles a given node as multiline when the closing parenthesis 198 * and the last element are not on the same line. 199 * @param {ASTNode} node A node to check. 200 * @returns {boolean} `true` if the node is multiline. 201 */ 202 function isMultiline(node) { 203 const lastItem = getLastItem(node); 204 205 if (!lastItem) { 206 return false; 207 } 208 209 const penultimateToken = getTrailingToken(node, lastItem); 210 const lastToken = sourceCode.getTokenAfter(penultimateToken); 211 212 return lastToken.loc.end.line !== penultimateToken.loc.end.line; 213 } 214 215 /** 216 * Reports a trailing comma if it exists. 217 * @param {ASTNode} node A node to check. Its type is one of 218 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, 219 * ImportDeclaration, and ExportNamedDeclaration. 220 * @returns {void} 221 */ 222 function forbidTrailingComma(node) { 223 const lastItem = getLastItem(node); 224 225 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) { 226 return; 227 } 228 229 const trailingToken = getTrailingToken(node, lastItem); 230 231 if (astUtils.isCommaToken(trailingToken)) { 232 context.report({ 233 node: lastItem, 234 loc: trailingToken.loc, 235 messageId: "unexpected", 236 fix(fixer) { 237 return fixer.remove(trailingToken); 238 } 239 }); 240 } 241 } 242 243 /** 244 * Reports the last element of a given node if it does not have a trailing 245 * comma. 246 * 247 * If a given node is `ArrayPattern` which has `RestElement`, the trailing 248 * comma is disallowed, so report if it exists. 249 * @param {ASTNode} node A node to check. Its type is one of 250 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, 251 * ImportDeclaration, and ExportNamedDeclaration. 252 * @returns {void} 253 */ 254 function forceTrailingComma(node) { 255 const lastItem = getLastItem(node); 256 257 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) { 258 return; 259 } 260 if (!isTrailingCommaAllowed(lastItem)) { 261 forbidTrailingComma(node); 262 return; 263 } 264 265 const trailingToken = getTrailingToken(node, lastItem); 266 267 if (trailingToken.value !== ",") { 268 context.report({ 269 node: lastItem, 270 loc: { 271 start: trailingToken.loc.end, 272 end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end) 273 }, 274 messageId: "missing", 275 fix(fixer) { 276 return fixer.insertTextAfter(trailingToken, ","); 277 } 278 }); 279 } 280 } 281 282 /** 283 * If a given node is multiline, reports the last element of a given node 284 * when it does not have a trailing comma. 285 * Otherwise, reports a trailing comma if it exists. 286 * @param {ASTNode} node A node to check. Its type is one of 287 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, 288 * ImportDeclaration, and ExportNamedDeclaration. 289 * @returns {void} 290 */ 291 function forceTrailingCommaIfMultiline(node) { 292 if (isMultiline(node)) { 293 forceTrailingComma(node); 294 } else { 295 forbidTrailingComma(node); 296 } 297 } 298 299 /** 300 * Only if a given node is not multiline, reports the last element of a given node 301 * when it does not have a trailing comma. 302 * Otherwise, reports a trailing comma if it exists. 303 * @param {ASTNode} node A node to check. Its type is one of 304 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, 305 * ImportDeclaration, and ExportNamedDeclaration. 306 * @returns {void} 307 */ 308 function allowTrailingCommaIfMultiline(node) { 309 if (!isMultiline(node)) { 310 forbidTrailingComma(node); 311 } 312 } 313 314 const predicate = { 315 always: forceTrailingComma, 316 "always-multiline": forceTrailingCommaIfMultiline, 317 "only-multiline": allowTrailingCommaIfMultiline, 318 never: forbidTrailingComma, 319 ignore: lodash.noop 320 }; 321 322 return { 323 ObjectExpression: predicate[options.objects], 324 ObjectPattern: predicate[options.objects], 325 326 ArrayExpression: predicate[options.arrays], 327 ArrayPattern: predicate[options.arrays], 328 329 ImportDeclaration: predicate[options.imports], 330 331 ExportNamedDeclaration: predicate[options.exports], 332 333 FunctionDeclaration: predicate[options.functions], 334 FunctionExpression: predicate[options.functions], 335 ArrowFunctionExpression: predicate[options.functions], 336 CallExpression: predicate[options.functions], 337 NewExpression: predicate[options.functions] 338 }; 339 } 340}; 341