1/** 2 * @fileoverview Rule to warn about using dot notation instead of square bracket notation when possible. 3 * @author Josh Perez 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const astUtils = require("./utils/ast-utils"); 12const keywords = require("./utils/keywords"); 13 14//------------------------------------------------------------------------------ 15// Rule Definition 16//------------------------------------------------------------------------------ 17 18const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/u; 19 20// `null` literal must be handled separately. 21const literalTypesToCheck = new Set(["string", "boolean"]); 22 23module.exports = { 24 meta: { 25 type: "suggestion", 26 27 docs: { 28 description: "enforce dot notation whenever possible", 29 category: "Best Practices", 30 recommended: false, 31 url: "https://eslint.org/docs/rules/dot-notation" 32 }, 33 34 schema: [ 35 { 36 type: "object", 37 properties: { 38 allowKeywords: { 39 type: "boolean", 40 default: true 41 }, 42 allowPattern: { 43 type: "string", 44 default: "" 45 } 46 }, 47 additionalProperties: false 48 } 49 ], 50 51 fixable: "code", 52 53 messages: { 54 useDot: "[{{key}}] is better written in dot notation.", 55 useBrackets: ".{{key}} is a syntax error." 56 } 57 }, 58 59 create(context) { 60 const options = context.options[0] || {}; 61 const allowKeywords = options.allowKeywords === void 0 || options.allowKeywords; 62 const sourceCode = context.getSourceCode(); 63 64 let allowPattern; 65 66 if (options.allowPattern) { 67 allowPattern = new RegExp(options.allowPattern, "u"); 68 } 69 70 /** 71 * Check if the property is valid dot notation 72 * @param {ASTNode} node The dot notation node 73 * @param {string} value Value which is to be checked 74 * @returns {void} 75 */ 76 function checkComputedProperty(node, value) { 77 if ( 78 validIdentifier.test(value) && 79 (allowKeywords || keywords.indexOf(String(value)) === -1) && 80 !(allowPattern && allowPattern.test(value)) 81 ) { 82 const formattedValue = node.property.type === "Literal" ? JSON.stringify(value) : `\`${value}\``; 83 84 context.report({ 85 node: node.property, 86 messageId: "useDot", 87 data: { 88 key: formattedValue 89 }, 90 *fix(fixer) { 91 const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken); 92 const rightBracket = sourceCode.getLastToken(node); 93 const nextToken = sourceCode.getTokenAfter(node); 94 95 // Don't perform any fixes if there are comments inside the brackets. 96 if (sourceCode.commentsExistBetween(leftBracket, rightBracket)) { 97 return; // eslint-disable-line eslint-plugin/fixer-return -- false positive 98 } 99 100 // Replace the brackets by an identifier. 101 if (!node.optional) { 102 yield fixer.insertTextBefore( 103 leftBracket, 104 astUtils.isDecimalInteger(node.object) ? " ." : "." 105 ); 106 } 107 yield fixer.replaceTextRange( 108 [leftBracket.range[0], rightBracket.range[1]], 109 value 110 ); 111 112 // Insert a space after the property if it will be connected to the next token. 113 if ( 114 nextToken && 115 rightBracket.range[1] === nextToken.range[0] && 116 !astUtils.canTokensBeAdjacent(String(value), nextToken) 117 ) { 118 yield fixer.insertTextAfter(node, " "); 119 } 120 } 121 }); 122 } 123 } 124 125 return { 126 MemberExpression(node) { 127 if ( 128 node.computed && 129 node.property.type === "Literal" && 130 (literalTypesToCheck.has(typeof node.property.value) || astUtils.isNullLiteral(node.property)) 131 ) { 132 checkComputedProperty(node, node.property.value); 133 } 134 if ( 135 node.computed && 136 node.property.type === "TemplateLiteral" && 137 node.property.expressions.length === 0 138 ) { 139 checkComputedProperty(node, node.property.quasis[0].value.cooked); 140 } 141 if ( 142 !allowKeywords && 143 !node.computed && 144 keywords.indexOf(String(node.property.name)) !== -1 145 ) { 146 context.report({ 147 node: node.property, 148 messageId: "useBrackets", 149 data: { 150 key: node.property.name 151 }, 152 *fix(fixer) { 153 const dotToken = sourceCode.getTokenBefore(node.property); 154 155 // A statement that starts with `let[` is parsed as a destructuring variable declaration, not a MemberExpression. 156 if (node.object.type === "Identifier" && node.object.name === "let" && !node.optional) { 157 return; // eslint-disable-line eslint-plugin/fixer-return -- false positive 158 } 159 160 // Don't perform any fixes if there are comments between the dot and the property name. 161 if (sourceCode.commentsExistBetween(dotToken, node.property)) { 162 return; // eslint-disable-line eslint-plugin/fixer-return -- false positive 163 } 164 165 // Replace the identifier to brackets. 166 if (!node.optional) { 167 yield fixer.remove(dotToken); 168 } 169 yield fixer.replaceText(node.property, `["${node.property.name}"]`); 170 } 171 }); 172 } 173 } 174 }; 175 } 176}; 177