yannickcr/eslint-plugin-react

View on GitHub
lib/rules/jsx-newline.js

Summary

Maintainability
C
7 hrs
Test Coverage
/**
 * @fileoverview Require or prevent a new line after jsx elements and expressions.
 * @author Johnny Zabala
 * @author Joseph Stiles
 */

'use strict';

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

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

const messages = {
  require: 'JSX element should start in a new line',
  prevent: 'JSX element should not start in a new line',
  allowMultilines: 'Multiline JSX elements should start in a new line',
};

function isMultilined(node) {
  return node && node.loc.start.line !== node.loc.end.line;
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    docs: {
      description: 'Require or prevent a new line after jsx elements and expressions.',
      category: 'Stylistic Issues',
      recommended: false,
      url: docsUrl('jsx-newline'),
    },
    fixable: 'code',

    messages,
    schema: [
      {
        type: 'object',
        properties: {
          prevent: {
            default: false,
            type: 'boolean',
          },
          allowMultilines: {
            default: false,
            type: 'boolean',
          },
        },
        additionalProperties: false,
        if: {
          properties: {
            allowMultilines: {
              const: true,
            },
          },
        },
        then: {
          properties: {
            prevent: {
              const: true,
            },
          },
          required: [
            'prevent',
          ],
        },
      },
    ],
  },
  create(context) {
    const jsxElementParents = new Set();

    function isBlockCommentInCurlyBraces(element) {
      const elementRawValue = getText(context, element);
      return /^\s*{\/\*/.test(elementRawValue);
    }

    function isNonBlockComment(element) {
      return !isBlockCommentInCurlyBraces(element) && (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer');
    }

    return {
      'Program:exit'() {
        jsxElementParents.forEach((parent) => {
          parent.children.forEach((element, index, elements) => {
            if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') {
              const configuration = context.options[0] || {};
              const prevent = configuration.prevent || false;
              const allowMultilines = configuration.allowMultilines || false;

              const firstAdjacentSibling = elements[index + 1];
              const secondAdjacentSibling = elements[index + 2];

              const hasSibling = firstAdjacentSibling
              && secondAdjacentSibling
              && (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText');

              if (!hasSibling) return;

              // Check adjacent sibling has the proper amount of newlines
              const isWithoutNewLine = !/\n\s*\n/.test(firstAdjacentSibling.value);

              if (isBlockCommentInCurlyBraces(element)) return;
              if (
                allowMultilines
                && (
                  isMultilined(element)
                  || isMultilined(elements.slice(index + 2).find(isNonBlockComment))
                )
              ) {
                if (!isWithoutNewLine) return;

                const regex = /(\n)(?!.*\1)/g;
                const replacement = '\n\n';
                const messageId = 'allowMultilines';

                report(context, messages[messageId], messageId, {
                  node: secondAdjacentSibling,
                  fix(fixer) {
                    return fixer.replaceText(
                      firstAdjacentSibling,
                      getText(context, firstAdjacentSibling).replace(regex, replacement)
                    );
                  },
                });

                return;
              }

              if (isWithoutNewLine === prevent) return;

              const messageId = prevent
                ? 'prevent'
                : 'require';

              const regex = prevent
                ? /(\n\n)(?!.*\1)/g
                : /(\n)(?!.*\1)/g;

              const replacement = prevent
                ? '\n'
                : '\n\n';

              report(context, messages[messageId], messageId, {
                node: secondAdjacentSibling,
                fix(fixer) {
                  return fixer.replaceText(
                    firstAdjacentSibling,
                    // double or remove the last newline
                    getText(context, firstAdjacentSibling).replace(regex, replacement)
                  );
                },
              });
            }
          });
        });
      },
      ':matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)': (node) => {
        jsxElementParents.add(node.parent);
      },
    };
  },
};