yannickcr/eslint-plugin-react

View on GitHub
lib/rules/jsx-props-no-multi-spaces.js

Summary

Maintainability
C
7 hrs
Test Coverage
/**
 * @fileoverview Disallow multiple spaces between inline JSX props
 * @author Adrian Moennich
 */

'use strict';

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

const getSourceCode = eslintUtil.getSourceCode;
const getText = eslintUtil.getText;

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

const messages = {
  noLineGap: 'Expected no line gap between “{{prop1}}” and “{{prop2}}”',
  onlyOneSpace: 'Expected only one space between “{{prop1}}” and “{{prop2}}”',
};

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    docs: {
      description: 'Disallow multiple spaces between inline JSX props',
      category: 'Stylistic Issues',
      recommended: false,
      url: docsUrl('jsx-props-no-multi-spaces'),
    },
    fixable: 'code',

    messages,

    schema: [],
  },

  create(context) {
    const sourceCode = getSourceCode(context);

    function getPropName(propNode) {
      switch (propNode.type) {
        case 'JSXSpreadAttribute':
          return getText(context, propNode.argument);
        case 'JSXIdentifier':
          return propNode.name;
        case 'JSXMemberExpression':
          return `${getPropName(propNode.object)}.${propNode.property.name}`;
        default:
          return propNode.name
            ? propNode.name.name
            : `${getText(context, propNode.object)}.${propNode.property.name}`; // needed for typescript-eslint parser
      }
    }

    // First and second must be adjacent nodes
    function hasEmptyLines(first, second) {
      const comments = sourceCode.getCommentsBefore ? sourceCode.getCommentsBefore(second) : [];
      const nodes = [].concat(first, comments, second);

      for (let i = 1; i < nodes.length; i += 1) {
        const prev = nodes[i - 1];
        const curr = nodes[i];
        if (curr.loc.start.line - prev.loc.end.line >= 2) {
          return true;
        }
      }

      return false;
    }

    function checkSpacing(prev, node) {
      if (hasEmptyLines(prev, node)) {
        report(context, messages.noLineGap, 'noLineGap', {
          node,
          data: {
            prop1: getPropName(prev),
            prop2: getPropName(node),
          },
        });
      }

      if (prev.loc.end.line !== node.loc.end.line) {
        return;
      }

      const between = getSourceCode(context).text.slice(prev.range[1], node.range[0]);

      if (between !== ' ') {
        report(context, messages.onlyOneSpace, 'onlyOneSpace', {
          node,
          data: {
            prop1: getPropName(prev),
            prop2: getPropName(node),
          },
          fix(fixer) {
            return fixer.replaceTextRange([prev.range[1], node.range[0]], ' ');
          },
        });
      }
    }

    function containsGenericType(node) {
      const nodeTypeParams = node.typeParameters;
      if (typeof nodeTypeParams === 'undefined') {
        return false;
      }

      return nodeTypeParams.type === 'TSTypeParameterInstantiation';
    }

    function getGenericNode(node) {
      const name = node.name;
      if (containsGenericType(node)) {
        const type = node.typeParameters;

        return Object.assign(
          {},
          node,
          {
            range: [
              name.range[0],
              type.range[1],
            ],
          }
        );
      }

      return name;
    }

    return {
      JSXOpeningElement(node) {
        node.attributes.reduce((prev, prop) => {
          checkSpacing(prev, prop);
          return prop;
        }, getGenericNode(node));
      },
    };
  },
};