UNPKG

19.9 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// Requirements
19//------------------------------------------------------------------------------
20const astUtils = require("../ast-utils");
21
22//------------------------------------------------------------------------------
23// Rule Definition
24//------------------------------------------------------------------------------
25module.exports = {
26 meta: {
27 docs: {
28 description: "require or disallow method and property shorthand syntax for object literals",
29 category: "ECMAScript 6",
30 recommended: false,
31 url: "https://eslint.org/docs/rules/object-shorthand"
32 },
33
34 fixable: "code",
35
36 schema: {
37 anyOf: [
38 {
39 type: "array",
40 items: [
41 {
42 enum: ["always", "methods", "properties", "never", "consistent", "consistent-as-needed"]
43 }
44 ],
45 minItems: 0,
46 maxItems: 1
47 },
48 {
49 type: "array",
50 items: [
51 {
52 enum: ["always", "methods", "properties"]
53 },
54 {
55 type: "object",
56 properties: {
57 avoidQuotes: {
58 type: "boolean"
59 }
60 },
61 additionalProperties: false
62 }
63 ],
64 minItems: 0,
65 maxItems: 2
66 },
67 {
68 type: "array",
69 items: [
70 {
71 enum: ["always", "methods"]
72 },
73 {
74 type: "object",
75 properties: {
76 ignoreConstructors: {
77 type: "boolean"
78 },
79 avoidQuotes: {
80 type: "boolean"
81 },
82 avoidExplicitReturnArrows: {
83 type: "boolean"
84 }
85 },
86 additionalProperties: false
87 }
88 ],
89 minItems: 0,
90 maxItems: 2
91 }
92 ]
93 }
94 },
95
96 create(context) {
97 const APPLY = context.options[0] || OPTIONS.always;
98 const APPLY_TO_METHODS = APPLY === OPTIONS.methods || APPLY === OPTIONS.always;
99 const APPLY_TO_PROPS = APPLY === OPTIONS.properties || APPLY === OPTIONS.always;
100 const APPLY_NEVER = APPLY === OPTIONS.never;
101 const APPLY_CONSISTENT = APPLY === OPTIONS.consistent;
102 const APPLY_CONSISTENT_AS_NEEDED = APPLY === OPTIONS.consistentAsNeeded;
103
104 const PARAMS = context.options[1] || {};
105 const IGNORE_CONSTRUCTORS = PARAMS.ignoreConstructors;
106 const AVOID_QUOTES = PARAMS.avoidQuotes;
107 const AVOID_EXPLICIT_RETURN_ARROWS = !!PARAMS.avoidExplicitReturnArrows;
108 const sourceCode = context.getSourceCode();
109
110 //--------------------------------------------------------------------------
111 // Helpers
112 //--------------------------------------------------------------------------
113
114 /**
115 * Determines if the first character of the name is a capital letter.
116 * @param {string} name The name of the node to evaluate.
117 * @returns {boolean} True if the first character of the property name is a capital letter, false if not.
118 * @private
119 */
120 function isConstructor(name) {
121 const firstChar = name.charAt(0);
122
123 return firstChar === firstChar.toUpperCase();
124 }
125
126 /**
127 * Determines if the property can have a shorthand form.
128 * @param {ASTNode} property Property AST node
129 * @returns {boolean} True if the property can have a shorthand form
130 * @private
131 *
132 */
133 function canHaveShorthand(property) {
134 return (property.kind !== "set" && property.kind !== "get" && property.type !== "SpreadElement" && property.type !== "SpreadProperty" && property.type !== "ExperimentalSpreadProperty");
135 }
136
137 /**
138 * Checks whether a node is a string literal.
139 * @param {ASTNode} node - Any AST node.
140 * @returns {boolean} `true` if it is a string literal.
141 */
142 function isStringLiteral(node) {
143 return node.type === "Literal" && typeof node.value === "string";
144 }
145
146 /**
147 * Determines if the property is a shorthand or not.
148 * @param {ASTNode} property Property AST node
149 * @returns {boolean} True if the property is considered shorthand, false if not.
150 * @private
151 *
152 */
153 function isShorthand(property) {
154
155 // property.method is true when `{a(){}}`.
156 return (property.shorthand || property.method);
157 }
158
159 /**
160 * Determines if the property's key and method or value are named equally.
161 * @param {ASTNode} property Property AST node
162 * @returns {boolean} True if the key and value are named equally, false if not.
163 * @private
164 *
165 */
166 function isRedundant(property) {
167 const value = property.value;
168
169 if (value.type === "FunctionExpression") {
170 return !value.id; // Only anonymous should be shorthand method.
171 }
172 if (value.type === "Identifier") {
173 return astUtils.getStaticPropertyName(property) === value.name;
174 }
175
176 return false;
177 }
178
179 /**
180 * Ensures that an object's properties are consistently shorthand, or not shorthand at all.
181 * @param {ASTNode} node Property AST node
182 * @param {boolean} checkRedundancy Whether to check longform redundancy
183 * @returns {void}
184 *
185 */
186 function checkConsistency(node, checkRedundancy) {
187
188 // We are excluding getters/setters and spread properties as they are considered neither longform nor shorthand.
189 const properties = node.properties.filter(canHaveShorthand);
190
191 // Do we still have properties left after filtering the getters and setters?
192 if (properties.length > 0) {
193 const shorthandProperties = properties.filter(isShorthand);
194
195 /*
196 * If we do not have an equal number of longform properties as
197 * shorthand properties, we are using the annotations inconsistently
198 */
199 if (shorthandProperties.length !== properties.length) {
200
201 // We have at least 1 shorthand property
202 if (shorthandProperties.length > 0) {
203 context.report({ node, message: "Unexpected mix of shorthand and non-shorthand properties." });
204 } else if (checkRedundancy) {
205
206 /*
207 * If all properties of the object contain a method or value with a name matching it's key,
208 * all the keys are redundant.
209 */
210 const canAlwaysUseShorthand = properties.every(isRedundant);
211
212 if (canAlwaysUseShorthand) {
213 context.report({ node, message: "Expected shorthand for all properties." });
214 }
215 }
216 }
217 }
218 }
219
220 /**
221 * Fixes a FunctionExpression node by making it into a shorthand property.
222 * @param {SourceCodeFixer} fixer The fixer object
223 * @param {ASTNode} node A `Property` node that has a `FunctionExpression` or `ArrowFunctionExpression` as its value
224 * @returns {Object} A fix for this node
225 */
226 function makeFunctionShorthand(fixer, node) {
227 const firstKeyToken = node.computed
228 ? sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken)
229 : sourceCode.getFirstToken(node.key);
230 const lastKeyToken = node.computed
231 ? sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken)
232 : sourceCode.getLastToken(node.key);
233 const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
234 let keyPrefix = "";
235
236 if (node.value.async) {
237 keyPrefix += "async ";
238 }
239 if (node.value.generator) {
240 keyPrefix += "*";
241 }
242
243 if (node.value.type === "FunctionExpression") {
244 const functionToken = sourceCode.getTokens(node.value).find(token => token.type === "Keyword" && token.value === "function");
245 const tokenBeforeParams = node.value.generator ? sourceCode.getTokenAfter(functionToken) : functionToken;
246
247 return fixer.replaceTextRange(
248 [firstKeyToken.range[0], node.range[1]],
249 keyPrefix + keyText + sourceCode.text.slice(tokenBeforeParams.range[1], node.value.range[1])
250 );
251 }
252 const arrowToken = sourceCode.getTokens(node.value).find(token => token.value === "=>");
253 const tokenBeforeArrow = sourceCode.getTokenBefore(arrowToken);
254 const hasParensAroundParameters = tokenBeforeArrow.type === "Punctuator" && tokenBeforeArrow.value === ")";
255 const oldParamText = sourceCode.text.slice(sourceCode.getFirstToken(node.value, node.value.async ? 1 : 0).range[0], tokenBeforeArrow.range[1]);
256 const newParamText = hasParensAroundParameters ? oldParamText : `(${oldParamText})`;
257
258 return fixer.replaceTextRange(
259 [firstKeyToken.range[0], node.range[1]],
260 keyPrefix + keyText + newParamText + sourceCode.text.slice(arrowToken.range[1], node.value.range[1])
261 );
262
263 }
264
265 /**
266 * Fixes a FunctionExpression node by making it into a longform property.
267 * @param {SourceCodeFixer} fixer The fixer object
268 * @param {ASTNode} node A `Property` node that has a `FunctionExpression` as its value
269 * @returns {Object} A fix for this node
270 */
271 function makeFunctionLongform(fixer, node) {
272 const firstKeyToken = node.computed ? sourceCode.getTokens(node).find(token => token.value === "[") : sourceCode.getFirstToken(node.key);
273 const lastKeyToken = node.computed ? sourceCode.getTokensBetween(node.key, node.value).find(token => token.value === "]") : sourceCode.getLastToken(node.key);
274 const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
275 let functionHeader = "function";
276
277 if (node.value.async) {
278 functionHeader = `async ${functionHeader}`;
279 }
280 if (node.value.generator) {
281 functionHeader = `${functionHeader}*`;
282 }
283
284 return fixer.replaceTextRange([node.range[0], lastKeyToken.range[1]], `${keyText}: ${functionHeader}`);
285 }
286
287 /*
288 * To determine whether a given arrow function has a lexical identifier (`this`, `arguments`, `super`, or `new.target`),
289 * create a stack of functions that define these identifiers (i.e. all functions except arrow functions) as the AST is
290 * traversed. Whenever a new function is encountered, create a new entry on the stack (corresponding to a different lexical
291 * scope of `this`), and whenever a function is exited, pop that entry off the stack. When an arrow function is entered,
292 * keep a reference to it on the current stack entry, and remove that reference when the arrow function is exited.
293 * When a lexical identifier is encountered, mark all the arrow functions on the current stack entry by adding them
294 * to an `arrowsWithLexicalIdentifiers` set. Any arrow function in that set will not be reported by this rule,
295 * because converting it into a method would change the value of one of the lexical identifiers.
296 */
297 const lexicalScopeStack = [];
298 const arrowsWithLexicalIdentifiers = new WeakSet();
299 const argumentsIdentifiers = new WeakSet();
300
301 /**
302 * Enters a function. This creates a new lexical identifier scope, so a new Set of arrow functions is pushed onto the stack.
303 * Also, this marks all `arguments` identifiers so that they can be detected later.
304 * @returns {void}
305 */
306 function enterFunction() {
307 lexicalScopeStack.unshift(new Set());
308 context.getScope().variables.filter(variable => variable.name === "arguments").forEach(variable => {
309 variable.references.map(ref => ref.identifier).forEach(identifier => argumentsIdentifiers.add(identifier));
310 });
311 }
312
313 /**
314 * Exits a function. This pops the current set of arrow functions off the lexical scope stack.
315 * @returns {void}
316 */
317 function exitFunction() {
318 lexicalScopeStack.shift();
319 }
320
321 /**
322 * Marks the current function as having a lexical keyword. This implies that all arrow functions
323 * in the current lexical scope contain a reference to this lexical keyword.
324 * @returns {void}
325 */
326 function reportLexicalIdentifier() {
327 lexicalScopeStack[0].forEach(arrowFunction => arrowsWithLexicalIdentifiers.add(arrowFunction));
328 }
329
330 //--------------------------------------------------------------------------
331 // Public
332 //--------------------------------------------------------------------------
333
334 return {
335 Program: enterFunction,
336 FunctionDeclaration: enterFunction,
337 FunctionExpression: enterFunction,
338 "Program:exit": exitFunction,
339 "FunctionDeclaration:exit": exitFunction,
340 "FunctionExpression:exit": exitFunction,
341
342 ArrowFunctionExpression(node) {
343 lexicalScopeStack[0].add(node);
344 },
345 "ArrowFunctionExpression:exit"(node) {
346 lexicalScopeStack[0].delete(node);
347 },
348
349 ThisExpression: reportLexicalIdentifier,
350 Super: reportLexicalIdentifier,
351 MetaProperty(node) {
352 if (node.meta.name === "new" && node.property.name === "target") {
353 reportLexicalIdentifier();
354 }
355 },
356 Identifier(node) {
357 if (argumentsIdentifiers.has(node)) {
358 reportLexicalIdentifier();
359 }
360 },
361
362 ObjectExpression(node) {
363 if (APPLY_CONSISTENT) {
364 checkConsistency(node, false);
365 } else if (APPLY_CONSISTENT_AS_NEEDED) {
366 checkConsistency(node, true);
367 }
368 },
369
370 "Property:exit"(node) {
371 const isConciseProperty = node.method || node.shorthand;
372
373 // Ignore destructuring assignment
374 if (node.parent.type === "ObjectPattern") {
375 return;
376 }
377
378 // getters and setters are ignored
379 if (node.kind === "get" || node.kind === "set") {
380 return;
381 }
382
383 // only computed methods can fail the following checks
384 if (node.computed && node.value.type !== "FunctionExpression" && node.value.type !== "ArrowFunctionExpression") {
385 return;
386 }
387
388 //--------------------------------------------------------------
389 // Checks for property/method shorthand.
390 if (isConciseProperty) {
391 if (node.method && (APPLY_NEVER || AVOID_QUOTES && isStringLiteral(node.key))) {
392 const message = APPLY_NEVER ? "Expected longform method syntax." : "Expected longform method syntax for string literal keys.";
393
394 // { x() {} } should be written as { x: function() {} }
395 context.report({
396 node,
397 message,
398 fix: fixer => makeFunctionLongform(fixer, node)
399 });
400 } else if (APPLY_NEVER) {
401
402 // { x } should be written as { x: x }
403 context.report({
404 node,
405 message: "Expected longform property syntax.",
406 fix: fixer => fixer.insertTextAfter(node.key, `: ${node.key.name}`)
407 });
408 }
409 } else if (APPLY_TO_METHODS && !node.value.id && (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression")) {
410 if (IGNORE_CONSTRUCTORS && node.key.type === "Identifier" && isConstructor(node.key.name)) {
411 return;
412 }
413 if (AVOID_QUOTES && isStringLiteral(node.key)) {
414 return;
415 }
416
417 // {[x]: function(){}} should be written as {[x]() {}}
418 if (node.value.type === "FunctionExpression" ||
419 node.value.type === "ArrowFunctionExpression" &&
420 node.value.body.type === "BlockStatement" &&
421 AVOID_EXPLICIT_RETURN_ARROWS &&
422 !arrowsWithLexicalIdentifiers.has(node.value)
423 ) {
424 context.report({
425 node,
426 message: "Expected method shorthand.",
427 fix: fixer => makeFunctionShorthand(fixer, node)
428 });
429 }
430 } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) {
431
432 // {x: x} should be written as {x}
433 context.report({
434 node,
435 message: "Expected property shorthand.",
436 fix(fixer) {
437 return fixer.replaceText(node, node.value.name);
438 }
439 });
440 } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) {
441 if (AVOID_QUOTES) {
442 return;
443 }
444
445 // {"x": x} should be written as {x}
446 context.report({
447 node,
448 message: "Expected property shorthand.",
449 fix(fixer) {
450 return fixer.replaceText(node, node.value.name);
451 }
452 });
453 }
454 }
455 };
456 }
457};