1/** 2 * @fileoverview Disallows or enforces spaces inside of array brackets. 3 * @author Jamund Ferguson 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 inside array brackets", 19 category: "Stylistic Issues", 20 recommended: false, 21 url: "https://eslint.org/docs/rules/array-bracket-spacing" 22 }, 23 24 fixable: "whitespace", 25 26 schema: [ 27 { 28 enum: ["always", "never"] 29 }, 30 { 31 type: "object", 32 properties: { 33 singleValue: { 34 type: "boolean" 35 }, 36 objectsInArrays: { 37 type: "boolean" 38 }, 39 arraysInArrays: { 40 type: "boolean" 41 } 42 }, 43 additionalProperties: false 44 } 45 ], 46 47 messages: { 48 unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.", 49 unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.", 50 missingSpaceAfter: "A space is required after '{{tokenValue}}'.", 51 missingSpaceBefore: "A space is required before '{{tokenValue}}'." 52 } 53 }, 54 create(context) { 55 const spaced = context.options[0] === "always", 56 sourceCode = context.getSourceCode(); 57 58 /** 59 * Determines whether an option is set, relative to the spacing option. 60 * If spaced is "always", then check whether option is set to false. 61 * If spaced is "never", then check whether option is set to true. 62 * @param {Object} option The option to exclude. 63 * @returns {boolean} Whether or not the property is excluded. 64 */ 65 function isOptionSet(option) { 66 return context.options[1] ? context.options[1][option] === !spaced : false; 67 } 68 69 const options = { 70 spaced, 71 singleElementException: isOptionSet("singleValue"), 72 objectsInArraysException: isOptionSet("objectsInArrays"), 73 arraysInArraysException: isOptionSet("arraysInArrays") 74 }; 75 76 //-------------------------------------------------------------------------- 77 // Helpers 78 //-------------------------------------------------------------------------- 79 80 /** 81 * Reports that there shouldn't be a space after the first token 82 * @param {ASTNode} node The node to report in the event of an error. 83 * @param {Token} token The token to use for the report. 84 * @returns {void} 85 */ 86 function reportNoBeginningSpace(node, token) { 87 const nextToken = sourceCode.getTokenAfter(token); 88 89 context.report({ 90 node, 91 loc: { start: token.loc.end, end: nextToken.loc.start }, 92 messageId: "unexpectedSpaceAfter", 93 data: { 94 tokenValue: token.value 95 }, 96 fix(fixer) { 97 return fixer.removeRange([token.range[1], nextToken.range[0]]); 98 } 99 }); 100 } 101 102 /** 103 * Reports that there shouldn't be a space before the last token 104 * @param {ASTNode} node The node to report in the event of an error. 105 * @param {Token} token The token to use for the report. 106 * @returns {void} 107 */ 108 function reportNoEndingSpace(node, token) { 109 const previousToken = sourceCode.getTokenBefore(token); 110 111 context.report({ 112 node, 113 loc: { start: previousToken.loc.end, end: token.loc.start }, 114 messageId: "unexpectedSpaceBefore", 115 data: { 116 tokenValue: token.value 117 }, 118 fix(fixer) { 119 return fixer.removeRange([previousToken.range[1], token.range[0]]); 120 } 121 }); 122 } 123 124 /** 125 * Reports that there should be a space after the first token 126 * @param {ASTNode} node The node to report in the event of an error. 127 * @param {Token} token The token to use for the report. 128 * @returns {void} 129 */ 130 function reportRequiredBeginningSpace(node, token) { 131 context.report({ 132 node, 133 loc: token.loc, 134 messageId: "missingSpaceAfter", 135 data: { 136 tokenValue: token.value 137 }, 138 fix(fixer) { 139 return fixer.insertTextAfter(token, " "); 140 } 141 }); 142 } 143 144 /** 145 * Reports that there should be a space before the last token 146 * @param {ASTNode} node The node to report in the event of an error. 147 * @param {Token} token The token to use for the report. 148 * @returns {void} 149 */ 150 function reportRequiredEndingSpace(node, token) { 151 context.report({ 152 node, 153 loc: token.loc, 154 messageId: "missingSpaceBefore", 155 data: { 156 tokenValue: token.value 157 }, 158 fix(fixer) { 159 return fixer.insertTextBefore(token, " "); 160 } 161 }); 162 } 163 164 /** 165 * Determines if a node is an object type 166 * @param {ASTNode} node The node to check. 167 * @returns {boolean} Whether or not the node is an object type. 168 */ 169 function isObjectType(node) { 170 return node && (node.type === "ObjectExpression" || node.type === "ObjectPattern"); 171 } 172 173 /** 174 * Determines if a node is an array type 175 * @param {ASTNode} node The node to check. 176 * @returns {boolean} Whether or not the node is an array type. 177 */ 178 function isArrayType(node) { 179 return node && (node.type === "ArrayExpression" || node.type === "ArrayPattern"); 180 } 181 182 /** 183 * Validates the spacing around array brackets 184 * @param {ASTNode} node The node we're checking for spacing 185 * @returns {void} 186 */ 187 function validateArraySpacing(node) { 188 if (options.spaced && node.elements.length === 0) { 189 return; 190 } 191 192 const first = sourceCode.getFirstToken(node), 193 second = sourceCode.getFirstToken(node, 1), 194 last = node.typeAnnotation 195 ? sourceCode.getTokenBefore(node.typeAnnotation) 196 : sourceCode.getLastToken(node), 197 penultimate = sourceCode.getTokenBefore(last), 198 firstElement = node.elements[0], 199 lastElement = node.elements[node.elements.length - 1]; 200 201 const openingBracketMustBeSpaced = 202 options.objectsInArraysException && isObjectType(firstElement) || 203 options.arraysInArraysException && isArrayType(firstElement) || 204 options.singleElementException && node.elements.length === 1 205 ? !options.spaced : options.spaced; 206 207 const closingBracketMustBeSpaced = 208 options.objectsInArraysException && isObjectType(lastElement) || 209 options.arraysInArraysException && isArrayType(lastElement) || 210 options.singleElementException && node.elements.length === 1 211 ? !options.spaced : options.spaced; 212 213 if (astUtils.isTokenOnSameLine(first, second)) { 214 if (openingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(first, second)) { 215 reportRequiredBeginningSpace(node, first); 216 } 217 if (!openingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(first, second)) { 218 reportNoBeginningSpace(node, first); 219 } 220 } 221 222 if (first !== penultimate && astUtils.isTokenOnSameLine(penultimate, last)) { 223 if (closingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(penultimate, last)) { 224 reportRequiredEndingSpace(node, last); 225 } 226 if (!closingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(penultimate, last)) { 227 reportNoEndingSpace(node, last); 228 } 229 } 230 } 231 232 //-------------------------------------------------------------------------- 233 // Public 234 //-------------------------------------------------------------------------- 235 236 return { 237 ArrayPattern: validateArraySpacing, 238 ArrayExpression: validateArraySpacing 239 }; 240 } 241}; 242