123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- '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 {fixSpaceAroundKeyword} = require('./fix/index.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({
- method: 'flatMap',
- argumentsLength: 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({
- method: 'reduce',
- argumentsLength: 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({
- method: 'concat',
- argumentsLength: 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({
- method: 'reduce',
- argumentsLength: 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({
- method: 'concat',
- argumentsLength: 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({
- methods: ['apply', 'call'],
- argumentsLength: 2,
- allowSpreadElement: true,
- }),
- emptyArraySelector('arguments.0'),
- arrayPrototypeMethodSelector({
- path: 'callee.object',
- method: '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({argumentsLength: 1}),
- getArrayNode: node => node.arguments[0],
- };
- function fix(node, array, sourceCode, shouldSwitchToArray) {
- if (typeof shouldSwitchToArray === 'function') {
- shouldSwitchToArray = shouldSwitchToArray(node);
- }
- return function * (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}`;
- }
- yield fixer.replaceText(node, fixed);
- yield * fixSpaceAroundKeyword(fixer, node, sourceCode);
- };
- }
- 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',
- additionalProperties: false,
- properties: {
- functions: {
- type: 'array',
- uniqueItems: true,
- },
- },
- },
- ];
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Prefer `Array#flat()` over legacy techniques to flatten arrays.',
- },
- fixable: 'code',
- schema,
- messages,
- },
- };
|