yannickcr/eslint-plugin-react

View on GitHub
lib/rules/jsx-max-props-per-line.js

Summary

Maintainability
B
5 hrs
Test Coverage
/**
 * @fileoverview Limit maximum of props on a single line in JSX
 * @author Yannick Croissant
 */

'use strict';

const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
const report = require('../util/report');

function getPropName(context, propNode) {
  if (propNode.type === 'JSXSpreadAttribute') {
    return getText(context, propNode.argument);
  }
  return propNode.name.name;
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

const messages = {
  newLine: 'Prop `{{prop}}` must be placed on a new line',
};

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    docs: {
      description: 'Enforce maximum of props on a single line in JSX',
      category: 'Stylistic Issues',
      recommended: false,
      url: docsUrl('jsx-max-props-per-line'),
    },
    fixable: 'code',

    messages,

    schema: [{
      anyOf: [{
        type: 'object',
        properties: {
          maximum: {
            type: 'object',
            properties: {
              single: {
                type: 'integer',
                minimum: 1,
              },
              multi: {
                type: 'integer',
                minimum: 1,
              },
            },
          },
        },
        additionalProperties: false,
      }, {
        type: 'object',
        properties: {
          maximum: {
            type: 'number',
            minimum: 1,
          },
          when: {
            type: 'string',
            enum: ['always', 'multiline'],
          },
        },
        additionalProperties: false,
      }],
    }],
  },

  create(context) {
    const configuration = context.options[0] || {};
    const maximum = configuration.maximum || 1;

    const maxConfig = typeof maximum === 'number'
      ? {
        single: configuration.when === 'multiline' ? Infinity : maximum,
        multi: maximum,
      }
      : {
        single: maximum.single || Infinity,
        multi: maximum.multi || Infinity,
      };

    function generateFixFunction(line, max) {
      const output = [];
      const front = line[0].range[0];
      const back = line[line.length - 1].range[1];

      for (let i = 0; i < line.length; i += max) {
        const nodes = line.slice(i, i + max);
        output.push(nodes.reduce((prev, curr) => {
          if (prev === '') {
            return getText(context, curr);
          }
          return `${prev} ${getText(context, curr)}`;
        }, ''));
      }

      const code = output.join('\n');

      return function fix(fixer) {
        return fixer.replaceTextRange([front, back], code);
      };
    }

    return {
      JSXOpeningElement(node) {
        if (!node.attributes.length) {
          return;
        }

        const isSingleLineTag = node.loc.start.line === node.loc.end.line;

        if ((isSingleLineTag ? maxConfig.single : maxConfig.multi) === Infinity) {
          return;
        }

        const firstProp = node.attributes[0];
        const linePartitionedProps = [[firstProp]];

        node.attributes.reduce((last, decl) => {
          if (last.loc.end.line === decl.loc.start.line) {
            linePartitionedProps[linePartitionedProps.length - 1].push(decl);
          } else {
            linePartitionedProps.push([decl]);
          }
          return decl;
        });

        linePartitionedProps.forEach((propsInLine) => {
          const maxPropsCountPerLine = isSingleLineTag && propsInLine[0].loc.start.line === node.loc.start.line
            ? maxConfig.single
            : maxConfig.multi;

          if (propsInLine.length > maxPropsCountPerLine) {
            const name = getPropName(context, propsInLine[maxPropsCountPerLine]);
            report(context, messages.newLine, 'newLine', {
              node: propsInLine[maxPropsCountPerLine],
              data: {
                prop: name,
              },
              fix: generateFixFunction(propsInLine, maxPropsCountPerLine),
            });
          }
        });
      },
    };
  },
};