1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | "use strict";
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | var espree = require("espree"),
|
14 | keywords = require("../util/keywords");
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | module.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 |
|
35 |
|
36 |
|
37 |
|
38 | function isKeyword(tokenStr) {
|
39 | return keywords.indexOf(tokenStr) >= 0;
|
40 | }
|
41 |
|
42 | |
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
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 |
|
57 |
|
58 |
|
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 |
|
98 |
|
99 |
|
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 |
|
113 |
|
114 |
|
115 |
|
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 |
|
184 | module.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 | };
|