1 | /**
|
---|
2 | * @fileoverview Rule to flag non-quoted property names in object literals.
|
---|
3 | * @author Mathias Bynens <http://mathiasbynens.be/>
|
---|
4 | * @deprecated in ESLint v8.53.0
|
---|
5 | */
|
---|
6 | "use strict";
|
---|
7 |
|
---|
8 | //------------------------------------------------------------------------------
|
---|
9 | // Requirements
|
---|
10 | //------------------------------------------------------------------------------
|
---|
11 |
|
---|
12 | const espree = require("espree");
|
---|
13 | const astUtils = require("./utils/ast-utils");
|
---|
14 | const keywords = require("./utils/keywords");
|
---|
15 |
|
---|
16 | //------------------------------------------------------------------------------
|
---|
17 | // Rule Definition
|
---|
18 | //------------------------------------------------------------------------------
|
---|
19 |
|
---|
20 | /** @type {import('../shared/types').Rule} */
|
---|
21 | module.exports = {
|
---|
22 | meta: {
|
---|
23 | deprecated: true,
|
---|
24 | replacedBy: [],
|
---|
25 | type: "suggestion",
|
---|
26 |
|
---|
27 | docs: {
|
---|
28 | description: "Require quotes around object literal property names",
|
---|
29 | recommended: false,
|
---|
30 | url: "https://eslint.org/docs/latest/rules/quote-props"
|
---|
31 | },
|
---|
32 |
|
---|
33 | schema: {
|
---|
34 | anyOf: [
|
---|
35 | {
|
---|
36 | type: "array",
|
---|
37 | items: [
|
---|
38 | {
|
---|
39 | enum: ["always", "as-needed", "consistent", "consistent-as-needed"]
|
---|
40 | }
|
---|
41 | ],
|
---|
42 | minItems: 0,
|
---|
43 | maxItems: 1
|
---|
44 | },
|
---|
45 | {
|
---|
46 | type: "array",
|
---|
47 | items: [
|
---|
48 | {
|
---|
49 | enum: ["always", "as-needed", "consistent", "consistent-as-needed"]
|
---|
50 | },
|
---|
51 | {
|
---|
52 | type: "object",
|
---|
53 | properties: {
|
---|
54 | keywords: {
|
---|
55 | type: "boolean"
|
---|
56 | },
|
---|
57 | unnecessary: {
|
---|
58 | type: "boolean"
|
---|
59 | },
|
---|
60 | numbers: {
|
---|
61 | type: "boolean"
|
---|
62 | }
|
---|
63 | },
|
---|
64 | additionalProperties: false
|
---|
65 | }
|
---|
66 | ],
|
---|
67 | minItems: 0,
|
---|
68 | maxItems: 2
|
---|
69 | }
|
---|
70 | ]
|
---|
71 | },
|
---|
72 |
|
---|
73 | fixable: "code",
|
---|
74 | messages: {
|
---|
75 | requireQuotesDueToReservedWord: "Properties should be quoted as '{{property}}' is a reserved word.",
|
---|
76 | inconsistentlyQuotedProperty: "Inconsistently quoted property '{{key}}' found.",
|
---|
77 | unnecessarilyQuotedProperty: "Unnecessarily quoted property '{{property}}' found.",
|
---|
78 | unquotedReservedProperty: "Unquoted reserved word '{{property}}' used as key.",
|
---|
79 | unquotedNumericProperty: "Unquoted number literal '{{property}}' used as key.",
|
---|
80 | unquotedPropertyFound: "Unquoted property '{{property}}' found.",
|
---|
81 | redundantQuoting: "Properties shouldn't be quoted as all quotes are redundant."
|
---|
82 | }
|
---|
83 | },
|
---|
84 |
|
---|
85 | create(context) {
|
---|
86 |
|
---|
87 | const MODE = context.options[0],
|
---|
88 | KEYWORDS = context.options[1] && context.options[1].keywords,
|
---|
89 | CHECK_UNNECESSARY = !context.options[1] || context.options[1].unnecessary !== false,
|
---|
90 | NUMBERS = context.options[1] && context.options[1].numbers,
|
---|
91 |
|
---|
92 | sourceCode = context.sourceCode;
|
---|
93 |
|
---|
94 |
|
---|
95 | /**
|
---|
96 | * Checks whether a certain string constitutes an ES3 token
|
---|
97 | * @param {string} tokenStr The string to be checked.
|
---|
98 | * @returns {boolean} `true` if it is an ES3 token.
|
---|
99 | */
|
---|
100 | function isKeyword(tokenStr) {
|
---|
101 | return keywords.includes(tokenStr);
|
---|
102 | }
|
---|
103 |
|
---|
104 | /**
|
---|
105 | * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary)
|
---|
106 | * @param {string} rawKey The raw key value from the source
|
---|
107 | * @param {espreeTokens} tokens The espree-tokenized node key
|
---|
108 | * @param {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked
|
---|
109 | * @returns {boolean} Whether or not a key has redundant quotes.
|
---|
110 | * @private
|
---|
111 | */
|
---|
112 | function areQuotesRedundant(rawKey, tokens, skipNumberLiterals) {
|
---|
113 | return tokens.length === 1 && tokens[0].start === 0 && tokens[0].end === rawKey.length &&
|
---|
114 | (["Identifier", "Keyword", "Null", "Boolean"].includes(tokens[0].type) ||
|
---|
115 | (tokens[0].type === "Numeric" && !skipNumberLiterals && String(+tokens[0].value) === tokens[0].value));
|
---|
116 | }
|
---|
117 |
|
---|
118 | /**
|
---|
119 | * Returns a string representation of a property node with quotes removed
|
---|
120 | * @param {ASTNode} key Key AST Node, which may or may not be quoted
|
---|
121 | * @returns {string} A replacement string for this property
|
---|
122 | */
|
---|
123 | function getUnquotedKey(key) {
|
---|
124 | return key.type === "Identifier" ? key.name : key.value;
|
---|
125 | }
|
---|
126 |
|
---|
127 | /**
|
---|
128 | * Returns a string representation of a property node with quotes added
|
---|
129 | * @param {ASTNode} key Key AST Node, which may or may not be quoted
|
---|
130 | * @returns {string} A replacement string for this property
|
---|
131 | */
|
---|
132 | function getQuotedKey(key) {
|
---|
133 | if (key.type === "Literal" && typeof key.value === "string") {
|
---|
134 |
|
---|
135 | // If the key is already a string literal, don't replace the quotes with double quotes.
|
---|
136 | return sourceCode.getText(key);
|
---|
137 | }
|
---|
138 |
|
---|
139 | // Otherwise, the key is either an identifier or a number literal.
|
---|
140 | return `"${key.type === "Identifier" ? key.name : key.value}"`;
|
---|
141 | }
|
---|
142 |
|
---|
143 | /**
|
---|
144 | * Ensures that a property's key is quoted only when necessary
|
---|
145 | * @param {ASTNode} node Property AST node
|
---|
146 | * @returns {void}
|
---|
147 | */
|
---|
148 | function checkUnnecessaryQuotes(node) {
|
---|
149 | const key = node.key;
|
---|
150 |
|
---|
151 | if (node.method || node.computed || node.shorthand) {
|
---|
152 | return;
|
---|
153 | }
|
---|
154 |
|
---|
155 | if (key.type === "Literal" && typeof key.value === "string") {
|
---|
156 | let tokens;
|
---|
157 |
|
---|
158 | try {
|
---|
159 | tokens = espree.tokenize(key.value);
|
---|
160 | } catch {
|
---|
161 | return;
|
---|
162 | }
|
---|
163 |
|
---|
164 | if (tokens.length !== 1) {
|
---|
165 | return;
|
---|
166 | }
|
---|
167 |
|
---|
168 | const isKeywordToken = isKeyword(tokens[0].value);
|
---|
169 |
|
---|
170 | if (isKeywordToken && KEYWORDS) {
|
---|
171 | return;
|
---|
172 | }
|
---|
173 |
|
---|
174 | if (CHECK_UNNECESSARY && areQuotesRedundant(key.value, tokens, NUMBERS)) {
|
---|
175 | context.report({
|
---|
176 | node,
|
---|
177 | messageId: "unnecessarilyQuotedProperty",
|
---|
178 | data: { property: key.value },
|
---|
179 | fix: fixer => fixer.replaceText(key, getUnquotedKey(key))
|
---|
180 | });
|
---|
181 | }
|
---|
182 | } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) {
|
---|
183 | context.report({
|
---|
184 | node,
|
---|
185 | messageId: "unquotedReservedProperty",
|
---|
186 | data: { property: key.name },
|
---|
187 | fix: fixer => fixer.replaceText(key, getQuotedKey(key))
|
---|
188 | });
|
---|
189 | } else if (NUMBERS && key.type === "Literal" && astUtils.isNumericLiteral(key)) {
|
---|
190 | context.report({
|
---|
191 | node,
|
---|
192 | messageId: "unquotedNumericProperty",
|
---|
193 | data: { property: key.value },
|
---|
194 | fix: fixer => fixer.replaceText(key, getQuotedKey(key))
|
---|
195 | });
|
---|
196 | }
|
---|
197 | }
|
---|
198 |
|
---|
199 | /**
|
---|
200 | * Ensures that a property's key is quoted
|
---|
201 | * @param {ASTNode} node Property AST node
|
---|
202 | * @returns {void}
|
---|
203 | */
|
---|
204 | function checkOmittedQuotes(node) {
|
---|
205 | const key = node.key;
|
---|
206 |
|
---|
207 | if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) {
|
---|
208 | context.report({
|
---|
209 | node,
|
---|
210 | messageId: "unquotedPropertyFound",
|
---|
211 | data: { property: key.name || key.value },
|
---|
212 | fix: fixer => fixer.replaceText(key, getQuotedKey(key))
|
---|
213 | });
|
---|
214 | }
|
---|
215 | }
|
---|
216 |
|
---|
217 | /**
|
---|
218 | * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes
|
---|
219 | * @param {ASTNode} node Property AST node
|
---|
220 | * @param {boolean} checkQuotesRedundancy Whether to check quotes' redundancy
|
---|
221 | * @returns {void}
|
---|
222 | */
|
---|
223 | function checkConsistency(node, checkQuotesRedundancy) {
|
---|
224 | const quotedProps = [],
|
---|
225 | unquotedProps = [];
|
---|
226 | let keywordKeyName = null,
|
---|
227 | necessaryQuotes = false;
|
---|
228 |
|
---|
229 | node.properties.forEach(property => {
|
---|
230 | const key = property.key;
|
---|
231 |
|
---|
232 | if (!key || property.method || property.computed || property.shorthand) {
|
---|
233 | return;
|
---|
234 | }
|
---|
235 |
|
---|
236 | if (key.type === "Literal" && typeof key.value === "string") {
|
---|
237 |
|
---|
238 | quotedProps.push(property);
|
---|
239 |
|
---|
240 | if (checkQuotesRedundancy) {
|
---|
241 | let tokens;
|
---|
242 |
|
---|
243 | try {
|
---|
244 | tokens = espree.tokenize(key.value);
|
---|
245 | } catch {
|
---|
246 | necessaryQuotes = true;
|
---|
247 | return;
|
---|
248 | }
|
---|
249 |
|
---|
250 | necessaryQuotes = necessaryQuotes || !areQuotesRedundant(key.value, tokens) || KEYWORDS && isKeyword(tokens[0].value);
|
---|
251 | }
|
---|
252 | } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) {
|
---|
253 | unquotedProps.push(property);
|
---|
254 | necessaryQuotes = true;
|
---|
255 | keywordKeyName = key.name;
|
---|
256 | } else {
|
---|
257 | unquotedProps.push(property);
|
---|
258 | }
|
---|
259 | });
|
---|
260 |
|
---|
261 | if (checkQuotesRedundancy && quotedProps.length && !necessaryQuotes) {
|
---|
262 | quotedProps.forEach(property => {
|
---|
263 | context.report({
|
---|
264 | node: property,
|
---|
265 | messageId: "redundantQuoting",
|
---|
266 | fix: fixer => fixer.replaceText(property.key, getUnquotedKey(property.key))
|
---|
267 | });
|
---|
268 | });
|
---|
269 | } else if (unquotedProps.length && keywordKeyName) {
|
---|
270 | unquotedProps.forEach(property => {
|
---|
271 | context.report({
|
---|
272 | node: property,
|
---|
273 | messageId: "requireQuotesDueToReservedWord",
|
---|
274 | data: { property: keywordKeyName },
|
---|
275 | fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key))
|
---|
276 | });
|
---|
277 | });
|
---|
278 | } else if (quotedProps.length && unquotedProps.length) {
|
---|
279 | unquotedProps.forEach(property => {
|
---|
280 | context.report({
|
---|
281 | node: property,
|
---|
282 | messageId: "inconsistentlyQuotedProperty",
|
---|
283 | data: { key: property.key.name || property.key.value },
|
---|
284 | fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key))
|
---|
285 | });
|
---|
286 | });
|
---|
287 | }
|
---|
288 | }
|
---|
289 |
|
---|
290 | return {
|
---|
291 | Property(node) {
|
---|
292 | if (MODE === "always" || !MODE) {
|
---|
293 | checkOmittedQuotes(node);
|
---|
294 | }
|
---|
295 | if (MODE === "as-needed") {
|
---|
296 | checkUnnecessaryQuotes(node);
|
---|
297 | }
|
---|
298 | },
|
---|
299 | ObjectExpression(node) {
|
---|
300 | if (MODE === "consistent") {
|
---|
301 | checkConsistency(node, false);
|
---|
302 | }
|
---|
303 | if (MODE === "consistent-as-needed") {
|
---|
304 | checkConsistency(node, true);
|
---|
305 | }
|
---|
306 | }
|
---|
307 | };
|
---|
308 |
|
---|
309 | }
|
---|
310 | };
|
---|