securityos/node_modules/eslint-plugin-unicorn/rules/prefer-object-from-entries.js

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,
},
};