jsx-curly-spacing.js 7.01 KB
/**
 * @fileoverview Enforce or disallow spaces inside of curly braces in JSX attributes.
 * @author Jamund Ferguson
 * @author Brandyn Bennett
 * @author Michael Ficarra
 * @author Vignesh Anand
 * @author Jamund Ferguson
 * @author Yannick Croissant
 * @author Erik Wendel
 */
'use strict';

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

module.exports = function(context) {
  var spaced = context.options[0] === 'always';
  var multiline = context.options[1] ? context.options[1].allowMultiline : true;

  // --------------------------------------------------------------------------
  // Helpers
  // --------------------------------------------------------------------------

  /**
   * Determines whether two adjacent tokens have a newline between them.
   * @param {Object} left - The left token object.
   * @param {Object} right - The right token object.
   * @returns {boolean} Whether or not there is a newline between the tokens.
   */
  function isMultiline(left, right) {
    return left.loc.start.line !== right.loc.start.line;
  }

  /**
   * Determines whether two adjacent tokens have whitespace between them.
   * @param {Object} left - The left token object.
   * @param {Object} right - The right token object.
   * @returns {boolean} Whether or not there is space between the tokens.
   */
  function isSpaced(left, right) {
    return left.range[1] < right.range[0];
  }

  /**
  * Reports that there shouldn't be a newline after the first token
  * @param {ASTNode} node - The node to report in the event of an error.
  * @param {Token} token - The token to use for the report.
  * @returns {void}
  */
  function reportNoBeginningNewline(node, token) {
    context.report({
      node: node,
      loc: token.loc.start,
      message: 'There should be no newline after \'' + token.value + '\'',
      fix: function(fixer) {
        var nextToken = context.getSourceCode().getTokenAfter(token);
        return fixer.replaceTextRange([token.range[1], nextToken.range[0]], spaced ? ' ' : '');
      }
    });
  }

  /**
  * Reports that there shouldn't be a newline before the last token
  * @param {ASTNode} node - The node to report in the event of an error.
  * @param {Token} token - The token to use for the report.
  * @returns {void}
  */
  function reportNoEndingNewline(node, token) {
    context.report({
      node: node,
      loc: token.loc.start,
      message: 'There should be no newline before \'' + token.value + '\'',
      fix: function(fixer) {
        var previousToken = context.getSourceCode().getTokenBefore(token);
        return fixer.replaceTextRange([previousToken.range[1], token.range[0]], spaced ? ' ' : '');
      }
    });
  }

  /**
  * Reports that there shouldn't be a space after the first token
  * @param {ASTNode} node - The node to report in the event of an error.
  * @param {Token} token - The token to use for the report.
  * @returns {void}
  */
  function reportNoBeginningSpace(node, token) {
    context.report({
      node: node,
      loc: token.loc.start,
      message: 'There should be no space after \'' + token.value + '\'',
      fix: function(fixer) {
        var nextToken = context.getSourceCode().getTokenAfter(token);
        return fixer.removeRange([token.range[1], nextToken.range[0]]);
      }
    });
  }

  /**
  * Reports that there shouldn't be a space before the last token
  * @param {ASTNode} node - The node to report in the event of an error.
  * @param {Token} token - The token to use for the report.
  * @returns {void}
  */
  function reportNoEndingSpace(node, token) {
    context.report({
      node: node,
      loc: token.loc.start,
      message: 'There should be no space before \'' + token.value + '\'',
      fix: function(fixer) {
        var previousToken = context.getSourceCode().getTokenBefore(token);
        return fixer.removeRange([previousToken.range[1], token.range[0]]);
      }
    });
  }

  /**
  * Reports that there should be a space after the first token
  * @param {ASTNode} node - The node to report in the event of an error.
  * @param {Token} token - The token to use for the report.
  * @returns {void}
  */
  function reportRequiredBeginningSpace(node, token) {
    context.report({
      node: node,
      loc: token.loc.start,
      message: 'A space is required after \'' + token.value + '\'',
      fix: function(fixer) {
        return fixer.insertTextAfter(token, ' ');
      }
    });
  }

  /**
  * Reports that there should be a space before the last token
  * @param {ASTNode} node - The node to report in the event of an error.
  * @param {Token} token - The token to use for the report.
  * @returns {void}
  */
  function reportRequiredEndingSpace(node, token) {
    context.report({
      node: node,
      loc: token.loc.start,
      message: 'A space is required before \'' + token.value + '\'',
      fix: function(fixer) {
        return fixer.insertTextBefore(token, ' ');
      }
    });
  }

  /**
   * Determines if spacing in curly braces is valid.
   * @param {ASTNode} node The AST node to check.
   * @param {Token} first The first token to check (should be the opening brace)
   * @param {Token} second The second token to check (should be first after the opening brace)
   * @param {Token} penultimate The penultimate token to check (should be last before closing brace)
   * @param {Token} last The last token to check (should be closing brace)
   * @returns {void}
   */
  function validateBraceSpacing(node, first, second, penultimate, last) {
    if (spaced) {
      if (!isSpaced(first, second)) {
        reportRequiredBeginningSpace(node, first);
      } else if (!multiline && isMultiline(first, second)) {
        reportNoBeginningNewline(node, first);
      }

      if (!isSpaced(penultimate, last)) {
        reportRequiredEndingSpace(node, last);
      } else if (!multiline && isMultiline(penultimate, last)) {
        reportNoEndingNewline(node, last);
      }

      return;
    }

    // "never" setting if we get here.
    if (isSpaced(first, second) && !(multiline && isMultiline(first, second))) {
      reportNoBeginningSpace(node, first);
    }

    if (isSpaced(penultimate, last) && !(multiline && isMultiline(penultimate, last))) {
      reportNoEndingSpace(node, last);
    }
  }

  // --------------------------------------------------------------------------
  // Public
  // --------------------------------------------------------------------------

  return {
    JSXExpressionContainer: function(node) {
      var first = context.getFirstToken(node);
      var second = context.getFirstToken(node, 1);
      var penultimate = context.getLastToken(node, 1);
      var last = context.getLastToken(node);

      if (first === penultimate && second === last) {
        return;
      }

      validateBraceSpacing(node, first, second, penultimate, last);
    }
  };
};

module.exports.schema = [{
  enum: ['always', 'never']
}, {
  type: 'object',
  properties: {
    allowMultiline: {
      type: 'boolean'
    }
  },
  additionalProperties: false
}];