UNPKG

eslint-plugin-unicorn

Version:
254 lines (230 loc) 7.26 kB
'use strict'; const { methodCallSelector, arrayPrototypeMethodSelector, emptyArraySelector, callExpressionSelector } = require('./selectors/index.js'); const needsSemicolon = require('./utils/needs-semicolon.js'); const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); const {isNodeMatches, isNodeMatchesNameOrPath} = require('./utils/is-node-matches.js'); const {getParenthesizedText, isParenthesized} = require('./utils/parentheses.js'); const MESSAGE_ID = 'prefer-array-flat'; const messages = { [MESSAGE_ID]: 'Prefer `Array#flat()` over `{{description}}` to flatten an array.' }; // `array.flatMap(x => x)` const arrayFlatMap = { selector: [ methodCallSelector({ name: 'flatMap', length: 1 }), '[arguments.0.type="ArrowFunctionExpression"]', '[arguments.0.async!=true]', '[arguments.0.generator!=true]', '[arguments.0.params.length=1]', '[arguments.0.params.0.type="Identifier"]', '[arguments.0.body.type="Identifier"]' ].join(''), testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.name, getArrayNode: node => node.callee.object, description: 'Array#flatMap()' }; // `array.reduce((a, b) => a.concat(b), [])` const arrayReduce = { selector: [ methodCallSelector({ name: 'reduce', length: 2 }), '[arguments.0.type="ArrowFunctionExpression"]', '[arguments.0.async!=true]', '[arguments.0.generator!=true]', '[arguments.0.params.length=2]', '[arguments.0.params.0.type="Identifier"]', '[arguments.0.params.1.type="Identifier"]', methodCallSelector({ name: 'concat', length: 1, path: 'arguments.0.body' }), '[arguments.0.body.callee.object.type="Identifier"]', '[arguments.0.body.arguments.0.type="Identifier"]', emptyArraySelector('arguments.1') ].join(''), testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.callee.object.name && node.arguments[0].params[1].name === node.arguments[0].body.arguments[0].name, getArrayNode: node => node.callee.object, description: 'Array#reduce()' }; // `array.reduce((a, b) => [...a, ...b], [])` const arrayReduce2 = { selector: [ methodCallSelector({ name: 'reduce', length: 2 }), '[arguments.0.type="ArrowFunctionExpression"]', '[arguments.0.async!=true]', '[arguments.0.generator!=true]', '[arguments.0.params.length=2]', '[arguments.0.params.0.type="Identifier"]', '[arguments.0.params.1.type="Identifier"]', '[arguments.0.body.type="ArrayExpression"]', '[arguments.0.body.elements.length=2]', '[arguments.0.body.elements.0.type="SpreadElement"]', '[arguments.0.body.elements.0.argument.type="Identifier"]', '[arguments.0.body.elements.1.type="SpreadElement"]', '[arguments.0.body.elements.1.argument.type="Identifier"]', emptyArraySelector('arguments.1') ].join(''), testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.elements[0].argument.name && node.arguments[0].params[1].name === node.arguments[0].body.elements[1].argument.name, getArrayNode: node => node.callee.object, description: 'Array#reduce()' }; // `[].concat(maybeArray)` and `[].concat(...array)` const emptyArrayConcat = { selector: [ methodCallSelector({ name: 'concat', length: 1, allowSpreadElement: true }), emptyArraySelector('callee.object') ].join(''), getArrayNode: node => { const argumentNode = node.arguments[0]; return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode; }, description: '[].concat()', shouldSwitchToArray: node => node.arguments[0].type !== 'SpreadElement' }; // - `[].concat.apply([], array)` and `Array.prototype.concat.apply([], array)` // - `[].concat.call([], maybeArray)` and `Array.prototype.concat.call([], maybeArray)` // - `[].concat.call([], ...array)` and `Array.prototype.concat.call([], ...array)` const arrayPrototypeConcat = { selector: [ methodCallSelector({ names: ['apply', 'call'], length: 2, allowSpreadElement: true }), emptyArraySelector('arguments.0'), arrayPrototypeMethodSelector({ path: 'callee.object', name: 'concat' }) ].join(''), testFunction: node => node.arguments[1].type !== 'SpreadElement' || node.callee.property.name === 'call', getArrayNode: node => { const argumentNode = node.arguments[1]; return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode; }, description: 'Array.prototype.concat()', shouldSwitchToArray: node => node.arguments[1].type !== 'SpreadElement' && node.callee.property.name === 'call' }; const lodashFlattenFunctions = [ '_.flatten', 'lodash.flatten', 'underscore.flatten' ]; const anyCall = { selector: callExpressionSelector({length: 1}), getArrayNode: node => node.arguments[0] }; function fix(node, array, sourceCode, shouldSwitchToArray) { if (typeof shouldSwitchToArray === 'function') { shouldSwitchToArray = shouldSwitchToArray(node); } return fixer => { let fixed = getParenthesizedText(array, sourceCode); if (shouldSwitchToArray) { // `array` is an argument, when it changes to `array[]`, we don't need add extra parentheses fixed = `[${fixed}]`; // And we don't need to add parentheses to the new array to call `.flat()` } else if ( !isParenthesized(array, sourceCode) && shouldAddParenthesesToMemberExpressionObject(array, sourceCode) ) { fixed = `(${fixed})`; } fixed = `${fixed}.flat()`; const tokenBefore = sourceCode.getTokenBefore(node); if (needsSemicolon(tokenBefore, sourceCode, fixed)) { fixed = `;${fixed}`; } return fixer.replaceText(node, fixed); }; } function create(context) { const {functions: configFunctions} = { functions: [], ...context.options[0] }; const functions = [...configFunctions, ...lodashFlattenFunctions]; const sourceCode = context.getSourceCode(); const listeners = {}; const cases = [ arrayFlatMap, arrayReduce, arrayReduce2, emptyArrayConcat, arrayPrototypeConcat, { ...anyCall, testFunction: node => isNodeMatches(node.callee, functions), description: node => `${functions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)).trim()}()` } ]; for (const {selector, testFunction, description, getArrayNode, shouldSwitchToArray} of cases) { listeners[selector] = function (node) { if (testFunction && !testFunction(node)) { return; } const array = getArrayNode(node); const data = { description: typeof description === 'string' ? description : description(node) }; const problem = { node, messageId: MESSAGE_ID, data }; // Don't fix if it has comments. if ( sourceCode.getCommentsInside(node).length === sourceCode.getCommentsInside(array).length ) { problem.fix = fix(node, array, sourceCode, shouldSwitchToArray); } return problem; }; } return listeners; } const schema = [ { type: 'object', properties: { functions: { type: 'array', uniqueItems: true } }, additionalProperties: false } ]; module.exports = { create, meta: { type: 'suggestion', docs: { description: 'Prefer `Array#flat()` over legacy techniques to flatten arrays.' }, fixable: 'code', schema, messages } };