2e8fff8b90e77d9529160749f52013d0b5c6b482
[dotfiles/.git] / dot-notation.js
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
11 const astUtils = require("./utils/ast-utils");
12 const keywords = require("./utils/keywords");
13
14 //------------------------------------------------------------------------------
15 // Rule Definition
16 //------------------------------------------------------------------------------
17
18 const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/u;
19
20 // `null` literal must be handled separately.
21 const literalTypesToCheck = new Set(["string", "boolean"]);
22
23 module.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 &&
121                     node.property.type === "Literal" &&
122                     (literalTypesToCheck.has(typeof node.property.value) || astUtils.isNullLiteral(node.property))
123                 ) {
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 };