blob: 2e8fff8b90e77d9529160749f52013d0b5c6b482 [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001/**
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");
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010012const keywords = require("./utils/keywords");
Yang Guo4fd355c2019-09-19 10:59:03 +020013
14//------------------------------------------------------------------------------
15// Rule Definition
16//------------------------------------------------------------------------------
17
18const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/u;
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010019
20// `null` literal must be handled separately.
21const literalTypesToCheck = new Set(["string", "boolean"]);
Yang Guo4fd355c2019-09-19 10:59:03 +020022
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
94 if (sourceCode.getFirstTokenBetween(leftBracket, rightBracket, { includeComments: true, filter: astUtils.isCommentToken })) {
95
96 // Don't perform any fixes if there are comments inside the brackets.
97 return null;
98 }
99
100 const tokenAfterProperty = sourceCode.getTokenAfter(rightBracket);
101 const needsSpaceAfterProperty = tokenAfterProperty &&
102 rightBracket.range[1] === tokenAfterProperty.range[0] &&
103 !astUtils.canTokensBeAdjacent(String(value), tokenAfterProperty);
104
105 const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : "";
106 const textAfterProperty = needsSpaceAfterProperty ? " " : "";
107
108 return fixer.replaceTextRange(
109 [leftBracket.range[0], rightBracket.range[1]],
110 `${textBeforeDot}.${value}${textAfterProperty}`
111 );
112 }
113 });
114 }
115 }
116
117 return {
118 MemberExpression(node) {
119 if (
120 node.computed &&
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100121 node.property.type === "Literal" &&
122 (literalTypesToCheck.has(typeof node.property.value) || astUtils.isNullLiteral(node.property))
Yang Guo4fd355c2019-09-19 10:59:03 +0200123 ) {
124 checkComputedProperty(node, node.property.value);
125 }
126 if (
127 node.computed &&
128 node.property.type === "TemplateLiteral" &&
129 node.property.expressions.length === 0
130 ) {
131 checkComputedProperty(node, node.property.quasis[0].value.cooked);
132 }
133 if (
134 !allowKeywords &&
135 !node.computed &&
136 keywords.indexOf(String(node.property.name)) !== -1
137 ) {
138 context.report({
139 node: node.property,
140 messageId: "useBrackets",
141 data: {
142 key: node.property.name
143 },
144 fix(fixer) {
145 const dot = sourceCode.getTokenBefore(node.property);
146 const textAfterDot = sourceCode.text.slice(dot.range[1], node.property.range[0]);
147
148 if (textAfterDot.trim()) {
149
150 // Don't perform any fixes if there are comments between the dot and the property name.
151 return null;
152 }
153
154 if (node.object.type === "Identifier" && node.object.name === "let") {
155
156 /*
157 * A statement that starts with `let[` is parsed as a destructuring variable declaration, not
158 * a MemberExpression.
159 */
160 return null;
161 }
162
163 return fixer.replaceTextRange(
164 [dot.range[0], node.property.range[1]],
165 `[${textAfterDot}"${node.property.name}"]`
166 );
167 }
168 });
169 }
170 }
171 };
172 }
173};