1/** 2 * @fileoverview Rule to enforce line breaks after each array element 3 * @author Jan Peer Stöcklmair <https://github.com/JPeer264> 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 line breaks after each array element", 20 category: "Stylistic Issues", 21 recommended: false, 22 url: "https://eslint.org/docs/rules/array-element-newline" 23 }, 24 25 fixable: "whitespace", 26 27 schema: { 28 definitions: { 29 basicConfig: { 30 oneOf: [ 31 { 32 enum: ["always", "never", "consistent"] 33 }, 34 { 35 type: "object", 36 properties: { 37 multiline: { 38 type: "boolean" 39 }, 40 minItems: { 41 type: ["integer", "null"], 42 minimum: 0 43 } 44 }, 45 additionalProperties: false 46 } 47 ] 48 } 49 }, 50 items: [ 51 { 52 oneOf: [ 53 { 54 $ref: "#/definitions/basicConfig" 55 }, 56 { 57 type: "object", 58 properties: { 59 ArrayExpression: { 60 $ref: "#/definitions/basicConfig" 61 }, 62 ArrayPattern: { 63 $ref: "#/definitions/basicConfig" 64 } 65 }, 66 additionalProperties: false, 67 minProperties: 1 68 } 69 ] 70 } 71 ] 72 }, 73 74 messages: { 75 unexpectedLineBreak: "There should be no linebreak here.", 76 missingLineBreak: "There should be a linebreak after this element." 77 } 78 }, 79 80 create(context) { 81 const sourceCode = context.getSourceCode(); 82 83 //---------------------------------------------------------------------- 84 // Helpers 85 //---------------------------------------------------------------------- 86 87 /** 88 * Normalizes a given option value. 89 * @param {string|Object|undefined} providedOption An option value to parse. 90 * @returns {{multiline: boolean, minItems: number}} Normalized option object. 91 */ 92 function normalizeOptionValue(providedOption) { 93 let consistent = false; 94 let multiline = false; 95 let minItems; 96 97 const option = providedOption || "always"; 98 99 if (!option || option === "always" || option.minItems === 0) { 100 minItems = 0; 101 } else if (option === "never") { 102 minItems = Number.POSITIVE_INFINITY; 103 } else if (option === "consistent") { 104 consistent = true; 105 minItems = Number.POSITIVE_INFINITY; 106 } else { 107 multiline = Boolean(option.multiline); 108 minItems = option.minItems || Number.POSITIVE_INFINITY; 109 } 110 111 return { consistent, multiline, minItems }; 112 } 113 114 /** 115 * Normalizes a given option value. 116 * @param {string|Object|undefined} options An option value to parse. 117 * @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object. 118 */ 119 function normalizeOptions(options) { 120 if (options && (options.ArrayExpression || options.ArrayPattern)) { 121 let expressionOptions, patternOptions; 122 123 if (options.ArrayExpression) { 124 expressionOptions = normalizeOptionValue(options.ArrayExpression); 125 } 126 127 if (options.ArrayPattern) { 128 patternOptions = normalizeOptionValue(options.ArrayPattern); 129 } 130 131 return { ArrayExpression: expressionOptions, ArrayPattern: patternOptions }; 132 } 133 134 const value = normalizeOptionValue(options); 135 136 return { ArrayExpression: value, ArrayPattern: value }; 137 } 138 139 /** 140 * Reports that there shouldn't be a line break after the first token 141 * @param {Token} token The token to use for the report. 142 * @returns {void} 143 */ 144 function reportNoLineBreak(token) { 145 const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true }); 146 147 context.report({ 148 loc: { 149 start: tokenBefore.loc.end, 150 end: token.loc.start 151 }, 152 messageId: "unexpectedLineBreak", 153 fix(fixer) { 154 if (astUtils.isCommentToken(tokenBefore)) { 155 return null; 156 } 157 158 if (!astUtils.isTokenOnSameLine(tokenBefore, token)) { 159 return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " "); 160 } 161 162 /* 163 * This will check if the comma is on the same line as the next element 164 * Following array: 165 * [ 166 * 1 167 * , 2 168 * , 3 169 * ] 170 * 171 * will be fixed to: 172 * [ 173 * 1, 2, 3 174 * ] 175 */ 176 const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true }); 177 178 if (astUtils.isCommentToken(twoTokensBefore)) { 179 return null; 180 } 181 182 return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], ""); 183 184 } 185 }); 186 } 187 188 /** 189 * Reports that there should be a line break after the first token 190 * @param {Token} token The token to use for the report. 191 * @returns {void} 192 */ 193 function reportRequiredLineBreak(token) { 194 const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true }); 195 196 context.report({ 197 loc: { 198 start: tokenBefore.loc.end, 199 end: token.loc.start 200 }, 201 messageId: "missingLineBreak", 202 fix(fixer) { 203 return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n"); 204 } 205 }); 206 } 207 208 /** 209 * Reports a given node if it violated this rule. 210 * @param {ASTNode} node A node to check. This is an ObjectExpression node or an ObjectPattern node. 211 * @returns {void} 212 */ 213 function check(node) { 214 const elements = node.elements; 215 const normalizedOptions = normalizeOptions(context.options[0]); 216 const options = normalizedOptions[node.type]; 217 218 if (!options) { 219 return; 220 } 221 222 let elementBreak = false; 223 224 /* 225 * MULTILINE: true 226 * loop through every element and check 227 * if at least one element has linebreaks inside 228 * this ensures that following is not valid (due to elements are on the same line): 229 * 230 * [ 231 * 1, 232 * 2, 233 * 3 234 * ] 235 */ 236 if (options.multiline) { 237 elementBreak = elements 238 .filter(element => element !== null) 239 .some(element => element.loc.start.line !== element.loc.end.line); 240 } 241 242 const linebreaksCount = node.elements.map((element, i) => { 243 const previousElement = elements[i - 1]; 244 245 if (i === 0 || element === null || previousElement === null) { 246 return false; 247 } 248 249 const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken); 250 const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken); 251 const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken); 252 253 return !astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement); 254 }).filter(isBreak => isBreak === true).length; 255 256 const needsLinebreaks = ( 257 elements.length >= options.minItems || 258 ( 259 options.multiline && 260 elementBreak 261 ) || 262 ( 263 options.consistent && 264 linebreaksCount > 0 && 265 linebreaksCount < node.elements.length 266 ) 267 ); 268 269 elements.forEach((element, i) => { 270 const previousElement = elements[i - 1]; 271 272 if (i === 0 || element === null || previousElement === null) { 273 return; 274 } 275 276 const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken); 277 const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken); 278 const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken); 279 280 if (needsLinebreaks) { 281 if (astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) { 282 reportRequiredLineBreak(firstTokenOfCurrentElement); 283 } 284 } else { 285 if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) { 286 reportNoLineBreak(firstTokenOfCurrentElement); 287 } 288 } 289 }); 290 } 291 292 //---------------------------------------------------------------------- 293 // Public 294 //---------------------------------------------------------------------- 295 296 return { 297 ArrayPattern: check, 298 ArrayExpression: check 299 }; 300 } 301}; 302