UNPKG

13.8 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to enforce concise object methods and properties.
3 * @author Jamund Ferguson
4 */
5
6"use strict";
7
8const OPTIONS = {
9 always: "always",
10 never: "never",
11 methods: "methods",
12 properties: "properties",
13 consistent: "consistent",
14 consistentAsNeeded: "consistent-as-needed"
15};
16
17//------------------------------------------------------------------------------
18// Rule Definition
19//------------------------------------------------------------------------------
20module.exports = {
21 meta: {
22 docs: {
23 description: "require or disallow method and property shorthand syntax for object literals",
24 category: "ECMAScript 6",
25 recommended: false
26 },
27
28 fixable: "code",
29
30 schema: {
31 anyOf: [
32 {
33 type: "array",
34 items: [
35 {
36 enum: ["always", "methods", "properties", "never", "consistent", "consistent-as-needed"]
37 }
38 ],
39 minItems: 0,
40 maxItems: 1
41 },
42 {
43 type: "array",
44 items: [
45 {
46 enum: ["always", "methods", "properties"]
47 },
48 {
49 type: "object",
50 properties: {
51 avoidQuotes: {
52 type: "boolean"
53 }
54 },
55 additionalProperties: false
56 }
57 ],
58 minItems: 0,
59 maxItems: 2
60 },
61 {
62 type: "array",
63 items: [
64 {
65 enum: ["always", "methods"]
66 },
67 {
68 type: "object",
69 properties: {
70 ignoreConstructors: {
71 type: "boolean"
72 },
73 avoidQuotes: {
74 type: "boolean"
75 }
76 },
77 additionalProperties: false
78 }
79 ],
80 minItems: 0,
81 maxItems: 2
82 }
83 ]
84 }
85 },
86
87 create(context) {
88 const APPLY = context.options[0] || OPTIONS.always;
89 const APPLY_TO_METHODS = APPLY === OPTIONS.methods || APPLY === OPTIONS.always;
90 const APPLY_TO_PROPS = APPLY === OPTIONS.properties || APPLY === OPTIONS.always;
91 const APPLY_NEVER = APPLY === OPTIONS.never;
92 const APPLY_CONSISTENT = APPLY === OPTIONS.consistent;
93 const APPLY_CONSISTENT_AS_NEEDED = APPLY === OPTIONS.consistentAsNeeded;
94
95 const PARAMS = context.options[1] || {};
96 const IGNORE_CONSTRUCTORS = PARAMS.ignoreConstructors;
97 const AVOID_QUOTES = PARAMS.avoidQuotes;
98
99 //--------------------------------------------------------------------------
100 // Helpers
101 //--------------------------------------------------------------------------
102
103 /**
104 * Determines if the first character of the name is a capital letter.
105 * @param {string} name The name of the node to evaluate.
106 * @returns {boolean} True if the first character of the property name is a capital letter, false if not.
107 * @private
108 */
109 function isConstructor(name) {
110 const firstChar = name.charAt(0);
111
112 return firstChar === firstChar.toUpperCase();
113 }
114
115 /**
116 * Determines if the property is not a getter and a setter.
117 * @param {ASTNode} property Property AST node
118 * @returns {boolean} True if the property is not a getter and a setter, false if it is.
119 * @private
120 **/
121 function isNotGetterOrSetter(property) {
122 return (property.kind !== "set" && property.kind !== "get");
123 }
124
125 /**
126 * Checks whether a node is a string literal.
127 * @param {ASTNode} node - Any AST node.
128 * @returns {boolean} `true` if it is a string literal.
129 */
130 function isStringLiteral(node) {
131 return node.type === "Literal" && typeof node.value === "string";
132 }
133
134 /**
135 * Determines if the property is a shorthand or not.
136 * @param {ASTNode} property Property AST node
137 * @returns {boolean} True if the property is considered shorthand, false if not.
138 * @private
139 **/
140 function isShorthand(property) {
141
142 // property.method is true when `{a(){}}`.
143 return (property.shorthand || property.method);
144 }
145
146 /**
147 * Determines if the property's key and method or value are named equally.
148 * @param {ASTNode} property Property AST node
149 * @returns {boolean} True if the key and value are named equally, false if not.
150 * @private
151 **/
152 function isRedudant(property) {
153 return (property.key && (
154
155 // A function expression
156 property.value && property.value.id && property.value.id.name === property.key.name ||
157
158 // A property
159 property.value && property.value.name === property.key.name
160 ));
161 }
162
163 /**
164 * Ensures that an object's properties are consistently shorthand, or not shorthand at all.
165 * @param {ASTNode} node Property AST node
166 * @param {boolean} checkRedundancy Whether to check longform redundancy
167 * @returns {void}
168 **/
169 function checkConsistency(node, checkRedundancy) {
170
171 // We are excluding getters and setters as they are considered neither longform nor shorthand.
172 const properties = node.properties.filter(isNotGetterOrSetter);
173
174 // Do we still have properties left after filtering the getters and setters?
175 if (properties.length > 0) {
176 const shorthandProperties = properties.filter(isShorthand);
177
178 // If we do not have an equal number of longform properties as
179 // shorthand properties, we are using the annotations inconsistently
180 if (shorthandProperties.length !== properties.length) {
181
182 // We have at least 1 shorthand property
183 if (shorthandProperties.length > 0) {
184 context.report(node, "Unexpected mix of shorthand and non-shorthand properties.");
185 } else if (checkRedundancy) {
186
187 // If all properties of the object contain a method or value with a name matching it's key,
188 // all the keys are redudant.
189 const canAlwaysUseShorthand = properties.every(isRedudant);
190
191 if (canAlwaysUseShorthand) {
192 context.report(node, "Expected shorthand for all properties.");
193 }
194 }
195 }
196 }
197 }
198
199 //--------------------------------------------------------------------------
200 // Public
201 //--------------------------------------------------------------------------
202
203 return {
204 ObjectExpression(node) {
205 if (APPLY_CONSISTENT) {
206 checkConsistency(node, false);
207 } else if (APPLY_CONSISTENT_AS_NEEDED) {
208 checkConsistency(node, true);
209 }
210 },
211
212 Property(node) {
213 const isConciseProperty = node.method || node.shorthand;
214
215 // Ignore destructuring assignment
216 if (node.parent.type === "ObjectPattern") {
217 return;
218 }
219
220 // getters and setters are ignored
221 if (node.kind === "get" || node.kind === "set") {
222 return;
223 }
224
225 // only computed methods can fail the following checks
226 if (node.computed && node.value.type !== "FunctionExpression") {
227 return;
228 }
229
230 //--------------------------------------------------------------
231 // Checks for property/method shorthand.
232 if (isConciseProperty) {
233
234 // if we're "never" and concise we should warn now
235 if (APPLY_NEVER) {
236 const type = node.method ? "method" : "property";
237
238 context.report({
239 node,
240 message: "Expected longform {{type}} syntax.",
241 data: {
242 type
243 },
244 fix(fixer) {
245 if (node.method) {
246 if (node.value.generator) {
247 return fixer.replaceTextRange([node.range[0], node.key.range[1]], `${node.key.name}: function*`);
248 }
249
250 return fixer.insertTextAfter(node.key, ": function");
251 }
252
253 return fixer.insertTextAfter(node.key, `: ${node.key.name}`);
254 }
255 });
256 }
257
258 // {'xyz'() {}} should be written as {'xyz': function() {}}
259 if (AVOID_QUOTES && isStringLiteral(node.key)) {
260 context.report({
261 node,
262 message: "Expected longform method syntax for string literal keys.",
263 fix(fixer) {
264 if (node.computed) {
265 return fixer.insertTextAfterRange([node.key.range[0], node.key.range[1] + 1], ": function");
266 }
267
268 return fixer.insertTextAfter(node.key, ": function");
269 }
270 });
271 }
272
273 return;
274 }
275
276 //--------------------------------------------------------------
277 // Checks for longform properties.
278 if (node.value.type === "FunctionExpression" && !node.value.id && APPLY_TO_METHODS) {
279 if (IGNORE_CONSTRUCTORS && isConstructor(node.key.name)) {
280 return;
281 }
282 if (AVOID_QUOTES && isStringLiteral(node.key)) {
283 return;
284 }
285
286 // {[x]: function(){}} should be written as {[x]() {}}
287 if (node.computed) {
288 context.report({
289 node,
290 message: "Expected method shorthand.",
291 fix(fixer) {
292 if (node.value.generator) {
293 return fixer.replaceTextRange(
294 [node.key.range[0], node.value.range[0] + "function*".length],
295 `*[${node.key.name}]`
296 );
297 }
298
299 return fixer.removeRange([node.key.range[1] + 1, node.value.range[0] + "function".length]);
300 }
301 });
302 return;
303 }
304
305 // {x: function(){}} should be written as {x() {}}
306 context.report({
307 node,
308 message: "Expected method shorthand.",
309 fix(fixer) {
310 if (node.value.generator) {
311 return fixer.replaceTextRange(
312 [node.key.range[0], node.value.range[0] + "function*".length],
313 `*${node.key.name}`
314 );
315 }
316
317 return fixer.removeRange([node.key.range[1], node.value.range[0] + "function".length]);
318 }
319 });
320 } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) {
321
322 // {x: x} should be written as {x}
323 context.report({
324 node,
325 message: "Expected property shorthand.",
326 fix(fixer) {
327 return fixer.replaceText(node, node.value.name);
328 }
329 });
330 } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) {
331 if (AVOID_QUOTES) {
332 return;
333 }
334
335 // {"x": x} should be written as {x}
336 context.report({
337 node,
338 message: "Expected property shorthand.",
339 fix(fixer) {
340 return fixer.replaceText(node, node.value.name);
341 }
342 });
343 }
344 }
345 };
346 }
347};