264 lines
7.6 KiB
JavaScript
264 lines
7.6 KiB
JavaScript
'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,
|
|
},
|
|
};
|