UNPKG

eslint-plugin-criteo

Version:

An opinionated collection of ESLint shared configs and rules used by Criteo.

147 lines (127 loc) 6.38 kB
/** * @fileoverview no-spreading-accumulators * @author Connor Ullman * @author Joe Pikowski */ 'use strict'; const moreInfo = `More info: https://github.com/criteo/eslint-plugin-criteo#no-spreading-accumulators`; export default { meta: { type: 'problem', fixable: 'code', schema: [], messages: { objectMessage: `Creating a new object by spreading the previous accumulator at every iteration of a .reduce() call has O(n^2) time & spatial complexity. If possible, make this O(n) by assigning to the existing accumulator or use mappedBy() or mappedByUnique() instead of .reduce(). ${moreInfo}`, arrayMessage: `Creating a new array by spreading the previous accumulator at every iteration of a .reduce() call has O(n^2) time & spatial complexity. If possible, make this O(n) by pushing to the existing accumulator instead. ${moreInfo}`, }, }, create: function (context) { return { 'CallExpression:has([callee.property.name=reduce])[arguments] > ArrowFunctionExpression'(node) { // only fetch the source code if getNodeText is called, and only fetch it once if we do const sourceCode = { _source: null, getNodeText(node) { if (this._source == null) this._source = context.getSourceCode().text; if (node == null) return null; return this._source.slice(node.range[0], node.range[1]); }, }; const getReportConfig = (spreadNode, fix, messageId) => ({ node: spreadNode.argument, loc: spreadNode.loc, messageId, fix, }); const isSpreadNode = (spreadNode) => spreadNode && (spreadNode.type === 'ExperimentalSpreadProperty' || spreadNode.type === 'SpreadProperty' || spreadNode.type === 'SpreadElement'); const arrowFn = node; const arrowFnFirstArg = arrowFn.params[0]; if (!arrowFnFirstArg) return; const arrowFnFirstArgName = arrowFnFirstArg.name; const expression = arrowFn.body; if (!expression) return; if (expression.type === 'ObjectExpression') { let spreadIndex = null; let hasNonFirstArgumentSpread = true; for (let i = 0; i < expression.properties.length; i++) { const prop = expression.properties[i]; if (isSpreadNode(prop)) { const isAccumulatorBeingSpread = prop.argument.name === arrowFnFirstArgName; if (isAccumulatorBeingSpread) { spreadIndex = i; } else { hasNonFirstArgumentSpread = false; } } } if (spreadIndex == null) return; const spreadNode = expression.properties[spreadIndex]; if (!spreadNode) return; const getObjectFix = () => { // if something other than the accumulator is being spread, we won't automatically fix it if (!hasNonFirstArgumentSpread) return undefined; // if the spread isn't the first argument of the expression, then you can only assign to the accumulator if the entry // isn't already present without potentially changing behavior of the reduce if (spreadIndex !== 0) return undefined; if (arrowFn.params <= 1) return undefined; const paramsText = arrowFn.params.map((o) => sourceCode.getNodeText(o)).join(', '); const nonSpreadPropertyTexts = expression.properties .filter((o, i) => i != spreadIndex) .map( (o) => `${arrowFnFirstArgName}[${sourceCode.getNodeText(o.key)}] = ${sourceCode.getNodeText(o.value)}`, ); if (nonSpreadPropertyTexts.length <= 0) return undefined; const bodyText = `${nonSpreadPropertyTexts.join('; ')}; return ${arrowFnFirstArgName};`; const newArrowFnText = `(${paramsText}) => { ${bodyText} }`; const fix = (fixer) => [fixer.replaceTextRange(arrowFn.range, newArrowFnText)]; return fix; }; const reportConfig = getReportConfig(spreadNode, getObjectFix(), 'objectMessage'); return context.report(reportConfig); } else if (expression.type === 'ArrayExpression') { let spreadIndex = null; let hasNonFirstArgumentSpread = true; for (let i = 0; i < expression.elements.length; i++) { const prop = expression.elements[i]; if (isSpreadNode(prop)) { const isAccumulatorBeingSpread = prop.argument.name === arrowFnFirstArgName; if (isAccumulatorBeingSpread) { spreadIndex = i; } else { hasNonFirstArgumentSpread = false; } } } if (spreadIndex == null) return; const spreadNode = expression.elements[spreadIndex]; if (!spreadNode) return; const getArrayFix = () => { // if something other than the accumulator is being spread, we won't automatically fix it if (!hasNonFirstArgumentSpread) return undefined; // if the spread isn't the first argument of the expression, then you can only assign to the accumulator if the entry // isn't already present without potentially changing behavior of the reduce if (spreadIndex !== 0) return undefined; if (arrowFn.params <= 1) return undefined; const paramsText = arrowFn.params.map((o) => sourceCode.getNodeText(o)).join(', '); const nonSpreadElementTexts = expression.elements .filter((o, i) => i != spreadIndex) .map((o) => sourceCode.getNodeText(o)); if (!nonSpreadElementTexts || nonSpreadElementTexts.length <= 0) return undefined; const bodyText = `${arrowFnFirstArgName}.push(${nonSpreadElementTexts.join( ', ', )}); return ${arrowFnFirstArgName};`; const newArrowFnText = `(${paramsText}) => { ${bodyText} }`; const fix = (fixer) => [fixer.replaceTextRange(arrowFn.range, newArrowFnText)]; return fix; }; const reportConfig = getReportConfig(spreadNode, getArrayFix(), 'arrayMessage'); return context.report(reportConfig); } }, }; }, };