source: imaps-frontend/node_modules/eslint/lib/rules/object-shorthand.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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