repo_description: React-specific linting rules for ESLint
repo_topics: development ecmascript eslint javascript lint react
code: /**
   * @fileoverview Prevent missing parentheses around multilines JSX
   * @author Yannick Croissant
   */
  
  'use strict';
  
  const has = require('hasown');
  const docsUrl = require('../util/docsUrl');
  const eslintUtil = require('../util/eslint');
  const jsxUtil = require('../util/jsx');
  const reportC = require('../util/report');
  const isParenthesized = require('../util/ast').isParenthesized;
  
  const getSourceCode = eslintUtil.getSourceCode;
  const getText = eslintUtil.getText;
  
  // ------------------------------------------------------------------------------
  // Constants
  // ------------------------------------------------------------------------------
  
  const DEFAULTS = {
    declaration: 'parens',
    assignment: 'parens',
    return: 'parens',
    arrow: 'parens',
    condition: 'ignore',
    logical: 'ignore',
    prop: 'ignore',
  };
  
  // ------------------------------------------------------------------------------
  // Rule Definition
  // ------------------------------------------------------------------------------
  
  const messages = {
    missingParens: 'Missing parentheses around multilines JSX',
    extraParens: 'Expected no parentheses around multilines JSX',
    parensOnNewLines: 'Parentheses around JSX should be on separate lines',
  };
  
  /** @type {import('eslint').Rule.RuleModule} */
  module.exports = {
    meta: {
      docs: {
        description: 'Disallow missing parentheses around multiline JSX',
        category: 'Stylistic Issues',
        recommended: false,
        url: docsUrl('jsx-wrap-multilines'),
      },
      fixable: 'code',
  
      messages,
  
      schema: [{
        type: 'object',
        // true/false are for backwards compatibility
        properties: {
          declaration: {
            enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
          },
          assignment: {
            enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
          },
          return: {
            enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
          },
          arrow: {
            enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
          },
          condition: {
            enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
          },
          logical: {
            enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
          },
          prop: {
            enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
          },
        },
        additionalProperties: false,
      }],
    },
  
    create(context) {
      function getOption(type) {
        const userOptions = context.options[0] || {};
        if (has(userOptions, type)) {
          return userOptions[type];
        }
        return DEFAULTS[type];
      }
  
      function isEnabled(type) {
        const option = getOption(type);
        return option && option !== 'ignore';
      }
  
      function needsOpeningNewLine(node) {
        const previousToken = getSourceCode(context).getTokenBefore(node);
  
        if (!isParenthesized(context, node)) {
          return false;
        }
  
        if (previousToken.loc.end.line === node.loc.start.line) {
          return true;
        }
  
        return false;
      }
  
      function needsClosingNewLine(node) {
        const nextToken = getSourceCode(context).getTokenAfter(node);
  
        if (!isParenthesized(context, node)) {
          return false;
        }
  
        if (node.loc.end.line === nextToken.loc.end.line) {
          return true;
        }
  
        return false;
      }
  
      function isMultilines(node) {
        return node.loc.start.line !== node.loc.end.line;
      }
  
      function report(node, messageId, fix) {
        reportC(context, messages[messageId], messageId, {
          node,
          fix,
        });
      }
  
      function trimTokenBeforeNewline(node, tokenBefore) {
        // if the token before the jsx is a bracket or curly brace
        // we don't want a space between the opening parentheses and the multiline jsx
        const isBracket = tokenBefore.value === '{' || tokenBefore.value === '[';
        return `${tokenBefore.value.trim()}${isBracket ? '' : ' '}`;
      }
  
      function check(node, type) {
        if (!node || !jsxUtil.isJSX(node)) {
          return;
        }
  
        const sourceCode = getSourceCode(context);
        const option = getOption(type);
  
        if ((option === true || option === 'parens') && !isParenthesized(context, node) && isMultilines(node)) {
          report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${getText(context, node)})`));
        }
  
        if (option === 'parens-new-line' && isMultilines(node)) {
          if (!isParenthesized(context, node)) {
            const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true });
            const tokenAfter = sourceCode.getTokenAfter(node, { includeComments: true });
            const start = node.loc.start;
            if (tokenBefore.loc.end.line < start.line) {
              // Strip newline after operator if parens newline is specified
              report(
                node,
                'missingParens',
                (fixer) => fixer.replaceTextRange(
                  [tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]],
                  `${trimTokenBeforeNewline(node, tokenBefore)}(\n${start.column > 0 ? ' '.repeat(start.column) : ''}${getText(context, node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})`
                )
              );
            } else {
              report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(\n${getText(context, node)}\n)`));
            }
          } else {
            const needsOpening = needsOpeningNewLine(node);
            const needsClosing = needsClosingNewLine(node);
            if (needsOpening || needsClosing) {
              report(node, 'parensOnNewLines', (fixer) => {
                const text = getText(context, node);
                let fixed = text;
                if (needsOpening) {
                  fixed = `\n${fixed}`;
                }
                if (needsClosing) {
                  fixed = `${fixed}\n`;
                }
                return fixer.replaceText(node, fixed);
              });
            }
          }
        }
  
        if (option === 'never' && isParenthesized(context, node)) {
          const tokenBefore = sourceCode.getTokenBefore(node);
          const tokenAfter = sourceCode.getTokenAfter(node);
          report(node, 'extraParens', (fixer) => fixer.replaceTextRange(
            [tokenBefore.range[0], tokenAfter.range[1]],
            getText(context, node)
          ));
        }
      }
  
      // --------------------------------------------------------------------------
      // Public
      // --------------------------------------------------------------------------
  
      return {
  
        VariableDeclarator(node) {
          const type = 'declaration';
          if (!isEnabled(type)) {
            return;
          }
          if (!isEnabled('condition') && node.init && node.init.type === 'ConditionalExpression') {
            check(node.init.consequent, type);
            check(node.init.alternate, type);
            return;
          }
          check(node.init, type);
        },
  
        AssignmentExpression(node) {
          const type = 'assignment';
          if (!isEnabled(type)) {
            return;
          }
          if (!isEnabled('condition') && node.right.type === 'ConditionalExpression') {
            check(node.right.consequent, type);
            check(node.right.alternate, type);
            return;
          }
          check(node.right, type);
        },
  
        ReturnStatement(node) {
          const type = 'return';
          if (isEnabled(type)) {
            check(node.argument, type);
          }
        },
  
        'ArrowFunctionExpression:exit': (node) => {
          const arrowBody = node.body;
          const type = 'arrow';
  
          if (isEnabled(type) && arrowBody.type !== 'BlockStatement') {
            check(arrowBody, type);
          }
        },
  
        ConditionalExpression(node) {
          const type = 'condition';
          if (isEnabled(type)) {
            check(node.consequent, type);
            check(node.alternate, type);
          }
        },
  
        LogicalExpression(node) {
          const type = 'logical';
          if (isEnabled(type)) {
            check(node.right, type);
          }
        },
  
        JSXAttribute(node) {
          const type = 'prop';
          if (isEnabled(type) && node.value && node.value.type === 'JSXExpressionContainer') {
            check(node.value.expression, type);
          }
        },
      };
    },
  };
  