123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- 'use strict';
- const {isCommaToken, isArrowToken, isClosingParenToken} = require('eslint-utils');
- const getDocumentationUrl = require('./utils/get-documentation-url.js');
- const {matches, methodCallSelector} = require('./selectors/index.js');
- const {removeParentheses} = require('./fix/index.js');
- const {getParentheses, getParenthesizedText} = require('./utils/parentheses.js');
- const {isNodeMatches, isNodeMatchesNameOrPath} = require('./utils/is-node-matches.js');
- const MESSAGE_ID_REDUCE = 'reduce';
- const MESSAGE_ID_FUNCTION = 'function';
- const messages = {
- [MESSAGE_ID_REDUCE]: 'Prefer `Object.fromEntries()` over `Array#reduce()`.',
- [MESSAGE_ID_FUNCTION]: 'Prefer `Object.fromEntries()` over `{{functionName}}()`.',
- };
- const createEmptyObjectSelector = path => {
- const prefix = path ? `${path}.` : '';
- return matches([
- // `{}`
- `[${prefix}type="ObjectExpression"][${prefix}properties.length=0]`,
- // `Object.create(null)`
- [
- methodCallSelector({path, object: 'Object', method: 'create', argumentsLength: 1}),
- `[${prefix}arguments.0.type="Literal"]`,
- `[${prefix}arguments.0.raw="null"]`,
- ].join(''),
- ]);
- };
- const createArrowCallbackSelector = path => {
- const prefix = path ? `${path}.` : '';
- return [
- `[${prefix}type="ArrowFunctionExpression"]`,
- `[${prefix}async!=true]`,
- `[${prefix}generator!=true]`,
- `[${prefix}params.length>=1]`,
- `[${prefix}params.0.type="Identifier"]`,
- ].join('');
- };
- const createPropertySelector = path => {
- const prefix = path ? `${path}.` : '';
- return [
- `[${prefix}type="Property"]`,
- `[${prefix}kind="init"]`,
- `[${prefix}method!=true]`,
- ].join('');
- };
- // - `pairs.reduce(…, {})`
- // - `pairs.reduce(…, Object.create(null))`
- const arrayReduceWithEmptyObject = [
- methodCallSelector({method: 'reduce', argumentsLength: 2}),
- createEmptyObjectSelector('arguments.1'),
- ].join('');
- const fixableArrayReduceCases = [
- {
- selector: [
- arrayReduceWithEmptyObject,
- // () => Object.assign(object, {key})
- createArrowCallbackSelector('arguments.0'),
- methodCallSelector({path: 'arguments.0.body', object: 'Object', method: 'assign', argumentsLength: 2}),
- '[arguments.0.body.arguments.0.type="Identifier"]',
- '[arguments.0.body.arguments.1.type="ObjectExpression"]',
- '[arguments.0.body.arguments.1.properties.length=1]',
- createPropertySelector('arguments.0.body.arguments.1.properties.0'),
- ].join(''),
- test: callback => callback.params[0].name === callback.body.arguments[0].name,
- getProperty: callback => callback.body.arguments[1].properties[0],
- },
- {
- selector: [
- arrayReduceWithEmptyObject,
- // () => ({...object, key})
- createArrowCallbackSelector('arguments.0'),
- '[arguments.0.body.type="ObjectExpression"]',
- '[arguments.0.body.properties.length=2]',
- '[arguments.0.body.properties.0.type="SpreadElement"]',
- '[arguments.0.body.properties.0.argument.type="Identifier"]',
- createPropertySelector('arguments.0.body.properties.1'),
- ].join(''),
- test: callback => callback.params[0].name === callback.body.properties[0].argument.name,
- getProperty: callback => callback.body.properties[1],
- },
- ];
- // `_.flatten(array)`
- const lodashFromPairsFunctions = [
- '_.fromPairs',
- 'lodash.fromPairs',
- ];
- const anyCall = [
- 'CallExpression',
- '[optional!=true]',
- '[arguments.length=1]',
- '[arguments.0.type!="SpreadElement"]',
- ' > .callee',
- ].join('');
- function fixReduceAssignOrSpread({sourceCode, node, property}) {
- const removeInitObject = fixer => {
- const initObject = node.arguments[1];
- const parentheses = getParentheses(initObject, sourceCode);
- const firstToken = parentheses[0] || initObject;
- const lastToken = parentheses[parentheses.length - 1] || initObject;
- const startToken = sourceCode.getTokenBefore(firstToken);
- const [start] = startToken.range;
- const [, end] = lastToken.range;
- return fixer.replaceTextRange([start, end], '');
- };
- function * removeFirstParameter(fixer) {
- const parameters = node.arguments[0].params;
- const [firstParameter] = parameters;
- const tokenAfter = sourceCode.getTokenAfter(firstParameter);
- if (isCommaToken(tokenAfter)) {
- yield fixer.remove(tokenAfter);
- }
- let shouldAddParentheses = false;
- if (parameters.length === 1) {
- const arrowToken = sourceCode.getTokenAfter(firstParameter, isArrowToken);
- const tokenBeforeArrowToken = sourceCode.getTokenBefore(arrowToken);
- if (!isClosingParenToken(tokenBeforeArrowToken)) {
- shouldAddParentheses = true;
- }
- }
- yield fixer.replaceText(firstParameter, shouldAddParentheses ? '()' : '');
- }
- const getKeyValueText = () => {
- const {key, value} = property;
- let keyText = getParenthesizedText(key, sourceCode);
- const valueText = getParenthesizedText(value, sourceCode);
- if (!property.computed && key.type === 'Identifier') {
- keyText = `'${keyText}'`;
- }
- return {keyText, valueText};
- };
- function * replaceFunctionBody(fixer) {
- const functionBody = node.arguments[0].body;
- const {keyText, valueText} = getKeyValueText();
- yield fixer.replaceText(functionBody, `[${keyText}, ${valueText}]`);
- yield * removeParentheses(functionBody, fixer, sourceCode);
- }
- return function * (fixer) {
- // Wrap `array.reduce()` with `Object.fromEntries()`
- yield fixer.insertTextBefore(node, 'Object.fromEntries(');
- yield fixer.insertTextAfter(node, ')');
- // Switch `.reduce` to `.map`
- yield fixer.replaceText(node.callee.property, 'map');
- // Remove empty object
- yield removeInitObject(fixer);
- // Remove the first parameter
- yield * removeFirstParameter(fixer);
- // Replace function body
- yield * replaceFunctionBody(fixer);
- };
- }
- /** @param {import('eslint').Rule.RuleContext} context */
- function create(context) {
- const {functions: configFunctions} = {
- functions: [],
- ...context.options[0],
- };
- const functions = [...configFunctions, ...lodashFromPairsFunctions];
- const sourceCode = context.getSourceCode();
- const listeners = {};
- const arrayReduce = new Map();
- for (const {selector, test, getProperty} of fixableArrayReduceCases) {
- listeners[selector] = node => {
- // If this listener exits without adding a fix, the `arrayReduceWithEmptyObject` listener
- // should still add it into the `arrayReduce` map. To be safer, add it here too.
- arrayReduce.set(node, undefined);
- const [callbackFunction] = node.arguments;
- if (!test(callbackFunction)) {
- return;
- }
- const [firstParameter] = callbackFunction.params;
- const variables = context.getDeclaredVariables(callbackFunction);
- const firstParameterVariable = variables.find(variable => variable.identifiers.length === 1 && variable.identifiers[0] === firstParameter);
- if (!firstParameterVariable || firstParameterVariable.references.length !== 1) {
- return;
- }
- arrayReduce.set(
- node,
- // The fix function
- fixReduceAssignOrSpread({
- sourceCode,
- node,
- property: getProperty(callbackFunction),
- }),
- );
- };
- }
- listeners[arrayReduceWithEmptyObject] = node => {
- if (!arrayReduce.has(node)) {
- arrayReduce.set(node, undefined);
- }
- };
- listeners['Program:exit'] = () => {
- for (const [node, fix] of arrayReduce.entries()) {
- context.report({
- node: node.callee.property,
- messageId: MESSAGE_ID_REDUCE,
- fix,
- });
- }
- };
- listeners[anyCall] = node => {
- if (!isNodeMatches(node, functions)) {
- return;
- }
- const functionName = functions.find(nameOrPath => isNodeMatchesNameOrPath(node, nameOrPath)).trim();
- context.report({
- node,
- messageId: MESSAGE_ID_FUNCTION,
- data: {functionName},
- fix: fixer => fixer.replaceText(node, 'Object.fromEntries'),
- });
- };
- return listeners;
- }
- const schema = [
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- functions: {
- type: 'array',
- uniqueItems: true,
- },
- },
- },
- ];
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Prefer using `Object.fromEntries(…)` to transform a list of key-value pairs into an object.',
- url: getDocumentationUrl(__filename),
- },
- fixable: 'code',
- schema,
- messages,
- },
- };
|