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