UNPKG

7.67 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to flag non-quoted property names in object literals.
3 * @author Mathias Bynens <http://mathiasbynens.be/>
4 * @copyright 2014 Brandon Mills. All rights reserved.
5 * @copyright 2015 Tomasz Olędzki. All rights reserved.
6 */
7"use strict";
8
9//------------------------------------------------------------------------------
10// Requirements
11//------------------------------------------------------------------------------
12
13var espree = require("espree"),
14 keywords = require("../util/keywords");
15
16//------------------------------------------------------------------------------
17// Rule Definition
18//------------------------------------------------------------------------------
19
20module.exports = function(context) {
21
22 var MODE = context.options[0],
23 KEYWORDS = context.options[1] && context.options[1].keywords,
24 CHECK_UNNECESSARY = !context.options[1] || context.options[1].unnecessary !== false,
25 NUMBERS = context.options[1] && context.options[1].numbers,
26
27 MESSAGE_UNNECESSARY = "Unnecessarily quoted property '{{property}}' found.",
28 MESSAGE_UNQUOTED = "Unquoted property '{{property}}' found.",
29 MESSAGE_NUMERIC = "Unquoted number literal '{{property}}' used as key.",
30 MESSAGE_RESERVED = "Unquoted reserved word '{{property}}' used as key.";
31
32
33 /**
34 * Checks whether a certain string constitutes an ES3 token
35 * @param {string} tokenStr - The string to be checked.
36 * @returns {boolean} `true` if it is an ES3 token.
37 */
38 function isKeyword(tokenStr) {
39 return keywords.indexOf(tokenStr) >= 0;
40 }
41
42 /**
43 * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary)
44 * @param {espreeTokens} tokens The espree-tokenized node key
45 * @param {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked
46 * @returns {boolean} Whether or not a key has redundant quotes.
47 * @private
48 */
49 function areQuotesRedundant(tokens, skipNumberLiterals) {
50 return tokens.length === 1 &&
51 (["Identifier", "Keyword", "Null", "Boolean"].indexOf(tokens[0].type) >= 0 ||
52 (tokens[0].type === "Numeric" && !skipNumberLiterals && "" + +tokens[0].value === tokens[0].value));
53 }
54
55 /**
56 * Ensures that a property's key is quoted only when necessary
57 * @param {ASTNode} node Property AST node
58 * @returns {void}
59 */
60 function checkUnnecessaryQuotes(node) {
61 var key = node.key,
62 isKeywordToken,
63 tokens;
64
65 if (node.method || node.computed || node.shorthand) {
66 return;
67 }
68
69 if (key.type === "Literal" && typeof key.value === "string") {
70 try {
71 tokens = espree.tokenize(key.value);
72 } catch (e) {
73 return;
74 }
75
76 if (tokens.length !== 1) {
77 return;
78 }
79
80 isKeywordToken = isKeyword(tokens[0].value);
81
82 if (isKeywordToken && KEYWORDS) {
83 return;
84 }
85
86 if (CHECK_UNNECESSARY && areQuotesRedundant(tokens, NUMBERS)) {
87 context.report(node, MESSAGE_UNNECESSARY, {property: key.value});
88 }
89 } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) {
90 context.report(node, MESSAGE_RESERVED, {property: key.name});
91 } else if (NUMBERS && key.type === "Literal" && typeof key.value === "number") {
92 context.report(node, MESSAGE_NUMERIC, {property: key.value});
93 }
94 }
95
96 /**
97 * Ensures that a property's key is quoted
98 * @param {ASTNode} node Property AST node
99 * @returns {void}
100 */
101 function checkOmittedQuotes(node) {
102 var key = node.key;
103
104 if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) {
105 context.report(node, MESSAGE_UNQUOTED, {
106 property: key.name || key.value
107 });
108 }
109 }
110
111 /**
112 * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes
113 * @param {ASTNode} node Property AST node
114 * @param {boolean} checkQuotesRedundancy Whether to check quotes' redundancy
115 * @returns {void}
116 */
117 function checkConsistency(node, checkQuotesRedundancy) {
118 var quotes = false,
119 lackOfQuotes = false,
120 necessaryQuotes = false;
121
122 node.properties.forEach(function(property) {
123 var key = property.key,
124 tokens;
125
126 if (!key || property.method || property.computed || property.shorthand) {
127 return;
128 }
129
130 if (key.type === "Literal" && typeof key.value === "string") {
131
132 quotes = true;
133
134 if (checkQuotesRedundancy) {
135 try {
136 tokens = espree.tokenize(key.value);
137 } catch (e) {
138 necessaryQuotes = true;
139 return;
140 }
141
142 necessaryQuotes = necessaryQuotes || !areQuotesRedundant(tokens) || KEYWORDS && isKeyword(tokens[0].value);
143 }
144 } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) {
145 necessaryQuotes = true;
146 context.report(node, "Properties should be quoted as '{{property}}' is a reserved word.", {property: key.name});
147 } else {
148 lackOfQuotes = true;
149 }
150
151 if (quotes && lackOfQuotes) {
152 context.report(node, "Inconsistently quoted property '{{key}}' found.", {
153 key: key.name || key.value
154 });
155 }
156 });
157
158 if (checkQuotesRedundancy && quotes && !necessaryQuotes) {
159 context.report(node, "Properties shouldn't be quoted as all quotes are redundant.");
160 }
161 }
162
163 return {
164 "Property": function(node) {
165 if (MODE === "always" || !MODE) {
166 checkOmittedQuotes(node);
167 }
168 if (MODE === "as-needed") {
169 checkUnnecessaryQuotes(node);
170 }
171 },
172 "ObjectExpression": function(node) {
173 if (MODE === "consistent") {
174 checkConsistency(node, false);
175 }
176 if (MODE === "consistent-as-needed") {
177 checkConsistency(node, true);
178 }
179 }
180 };
181
182};
183
184module.exports.schema = {
185 "anyOf": [
186 {
187 "type": "array",
188 "items": [
189 {
190 "enum": ["always", "as-needed", "consistent", "consistent-as-needed"]
191 }
192 ],
193 "minItems": 0,
194 "maxItems": 1
195 },
196 {
197 "type": "array",
198 "items": [
199 {
200 "enum": ["always", "as-needed", "consistent", "consistent-as-needed"]
201 },
202 {
203 "type": "object",
204 "properties": {
205 "keywords": {
206 "type": "boolean"
207 },
208 "unnecessary": {
209 "type": "boolean"
210 },
211 "numbers": {
212 "type": "boolean"
213 }
214 },
215 "additionalProperties": false
216 }
217 ],
218 "minItems": 0,
219 "maxItems": 2
220 }
221 ]
222};