1/** 2 * @fileoverview Rule to enforce linebreaks after open and before close array brackets 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 linebreaks after opening and before closing array brackets", 20 category: "Stylistic Issues", 21 recommended: false, 22 url: "https://eslint.org/docs/rules/array-bracket-newline" 23 }, 24 25 fixable: "whitespace", 26 27 schema: [ 28 { 29 oneOf: [ 30 { 31 enum: ["always", "never", "consistent"] 32 }, 33 { 34 type: "object", 35 properties: { 36 multiline: { 37 type: "boolean" 38 }, 39 minItems: { 40 type: ["integer", "null"], 41 minimum: 0 42 } 43 }, 44 additionalProperties: false 45 } 46 ] 47 } 48 ], 49 50 messages: { 51 unexpectedOpeningLinebreak: "There should be no linebreak after '['.", 52 unexpectedClosingLinebreak: "There should be no linebreak before ']'.", 53 missingOpeningLinebreak: "A linebreak is required after '['.", 54 missingClosingLinebreak: "A linebreak is required before ']'." 55 } 56 }, 57 58 create(context) { 59 const sourceCode = context.getSourceCode(); 60 61 62 //---------------------------------------------------------------------- 63 // Helpers 64 //---------------------------------------------------------------------- 65 66 /** 67 * Normalizes a given option value. 68 * @param {string|Object|undefined} option An option value to parse. 69 * @returns {{multiline: boolean, minItems: number}} Normalized option object. 70 */ 71 function normalizeOptionValue(option) { 72 let consistent = false; 73 let multiline = false; 74 let minItems = 0; 75 76 if (option) { 77 if (option === "consistent") { 78 consistent = true; 79 minItems = Number.POSITIVE_INFINITY; 80 } else if (option === "always" || option.minItems === 0) { 81 minItems = 0; 82 } else if (option === "never") { 83 minItems = Number.POSITIVE_INFINITY; 84 } else { 85 multiline = Boolean(option.multiline); 86 minItems = option.minItems || Number.POSITIVE_INFINITY; 87 } 88 } else { 89 consistent = false; 90 multiline = true; 91 minItems = Number.POSITIVE_INFINITY; 92 } 93 94 return { consistent, multiline, minItems }; 95 } 96 97 /** 98 * Normalizes a given option value. 99 * @param {string|Object|undefined} options An option value to parse. 100 * @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object. 101 */ 102 function normalizeOptions(options) { 103 const value = normalizeOptionValue(options); 104 105 return { ArrayExpression: value, ArrayPattern: value }; 106 } 107 108 /** 109 * Reports that there shouldn't be a linebreak after the first token 110 * @param {ASTNode} node The node to report in the event of an error. 111 * @param {Token} token The token to use for the report. 112 * @returns {void} 113 */ 114 function reportNoBeginningLinebreak(node, token) { 115 context.report({ 116 node, 117 loc: token.loc, 118 messageId: "unexpectedOpeningLinebreak", 119 fix(fixer) { 120 const nextToken = sourceCode.getTokenAfter(token, { includeComments: true }); 121 122 if (astUtils.isCommentToken(nextToken)) { 123 return null; 124 } 125 126 return fixer.removeRange([token.range[1], nextToken.range[0]]); 127 } 128 }); 129 } 130 131 /** 132 * Reports that there shouldn't be a linebreak before the last token 133 * @param {ASTNode} node The node to report in the event of an error. 134 * @param {Token} token The token to use for the report. 135 * @returns {void} 136 */ 137 function reportNoEndingLinebreak(node, token) { 138 context.report({ 139 node, 140 loc: token.loc, 141 messageId: "unexpectedClosingLinebreak", 142 fix(fixer) { 143 const previousToken = sourceCode.getTokenBefore(token, { includeComments: true }); 144 145 if (astUtils.isCommentToken(previousToken)) { 146 return null; 147 } 148 149 return fixer.removeRange([previousToken.range[1], token.range[0]]); 150 } 151 }); 152 } 153 154 /** 155 * Reports that there should be a linebreak after the first token 156 * @param {ASTNode} node The node to report in the event of an error. 157 * @param {Token} token The token to use for the report. 158 * @returns {void} 159 */ 160 function reportRequiredBeginningLinebreak(node, token) { 161 context.report({ 162 node, 163 loc: token.loc, 164 messageId: "missingOpeningLinebreak", 165 fix(fixer) { 166 return fixer.insertTextAfter(token, "\n"); 167 } 168 }); 169 } 170 171 /** 172 * Reports that there should be a linebreak before the last token 173 * @param {ASTNode} node The node to report in the event of an error. 174 * @param {Token} token The token to use for the report. 175 * @returns {void} 176 */ 177 function reportRequiredEndingLinebreak(node, token) { 178 context.report({ 179 node, 180 loc: token.loc, 181 messageId: "missingClosingLinebreak", 182 fix(fixer) { 183 return fixer.insertTextBefore(token, "\n"); 184 } 185 }); 186 } 187 188 /** 189 * Reports a given node if it violated this rule. 190 * @param {ASTNode} node A node to check. This is an ArrayExpression node or an ArrayPattern node. 191 * @returns {void} 192 */ 193 function check(node) { 194 const elements = node.elements; 195 const normalizedOptions = normalizeOptions(context.options[0]); 196 const options = normalizedOptions[node.type]; 197 const openBracket = sourceCode.getFirstToken(node); 198 const closeBracket = sourceCode.getLastToken(node); 199 const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true }); 200 const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true }); 201 const first = sourceCode.getTokenAfter(openBracket); 202 const last = sourceCode.getTokenBefore(closeBracket); 203 204 const needsLinebreaks = ( 205 elements.length >= options.minItems || 206 ( 207 options.multiline && 208 elements.length > 0 && 209 firstIncComment.loc.start.line !== lastIncComment.loc.end.line 210 ) || 211 ( 212 elements.length === 0 && 213 firstIncComment.type === "Block" && 214 firstIncComment.loc.start.line !== lastIncComment.loc.end.line && 215 firstIncComment === lastIncComment 216 ) || 217 ( 218 options.consistent && 219 openBracket.loc.end.line !== first.loc.start.line 220 ) 221 ); 222 223 /* 224 * Use tokens or comments to check multiline or not. 225 * But use only tokens to check whether linebreaks are needed. 226 * This allows: 227 * var arr = [ // eslint-disable-line foo 228 * 'a' 229 * ] 230 */ 231 232 if (needsLinebreaks) { 233 if (astUtils.isTokenOnSameLine(openBracket, first)) { 234 reportRequiredBeginningLinebreak(node, openBracket); 235 } 236 if (astUtils.isTokenOnSameLine(last, closeBracket)) { 237 reportRequiredEndingLinebreak(node, closeBracket); 238 } 239 } else { 240 if (!astUtils.isTokenOnSameLine(openBracket, first)) { 241 reportNoBeginningLinebreak(node, openBracket); 242 } 243 if (!astUtils.isTokenOnSameLine(last, closeBracket)) { 244 reportNoEndingLinebreak(node, closeBracket); 245 } 246 } 247 } 248 249 //---------------------------------------------------------------------- 250 // Public 251 //---------------------------------------------------------------------- 252 253 return { 254 ArrayPattern: check, 255 ArrayExpression: check 256 }; 257 } 258}; 259