| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 | 'use strict';const {isCommaToken, isArrowToken, isClosingParenToken} = require('@eslint-community/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 => {			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['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,	},};
 |