Repository: yannickcr/eslint-plugin-react
Pull request #883 (2016-10-04 05:51:34)

jsx-first-prop-new-line: add multiline-multiprop option
addresses another part of https://github.com/yannickcr/eslint-plugin-react/issues/878



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bd297cc..f18e2f44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
 
+## [6.4.1] - 2016-10-10
+### Fixed
+* Fix `jsx-indent` for arrays ([#897][], [#898][])
+* Fix `jsx-indent` to allow multi-line logical expressions with one level of indent ([#896][])
+
+[6.4.1]: https://github.com/yannickcr/eslint-plugin-react/compare/v6.4.0...v6.4.1
+[#897]: https://github.com/yannickcr/eslint-plugin-react/issues/897
+[#898]: https://github.com/yannickcr/eslint-plugin-react/issues/898
+[#896]: https://github.com/yannickcr/eslint-plugin-react/pull/896
+
 ## [6.4.0] - 2016-10-09
 ### Added
 * Add `skipUndeclared` option to `prop-types` ([#846][] @pfhayes)
diff --git a/docs/rules/jsx-first-prop-new-line.md b/docs/rules/jsx-first-prop-new-line.md
index 0a6a6213..309efb96 100644
--- a/docs/rules/jsx-first-prop-new-line.md
+++ b/docs/rules/jsx-first-prop-new-line.md
@@ -7,11 +7,12 @@ Ensure correct position of the first property.
 This rule checks whether the first property of all JSX elements is correctly placed. There are three possible configurations:
 * `always`: The first property should always be placed on a new line.
 * `never` : The first property should never be placed on a new line, e.g. should always be on the same line as the Component opening tag.
-* `multiline`: The first property should always be placed on a new line when the properties are spread over multiple lines.
+* `multiline`: The first property should always be placed on a new line when the JSX tag takes up multiple lines.
+* `multiline-multiprop`: The first property should always be placed on a new line if the JSX tag takes up multiple lines and there are multiple properties.
 
 The following patterns are considered warnings when configured `"always"`:
 
-```js
+```jsx
 <Hello personal={true} />
 
 <Hello personal={true}
@@ -21,7 +22,7 @@ The following patterns are considered warnings when configured `"always"`:
 
 The following patterns are not considered warnings when configured `"always"`:
 
-```js
+```jsx
 <Hello
     personal />
 
@@ -32,7 +33,7 @@ The following patterns are not considered warnings when configured `"always"`:
 
 The following patterns are considered warnings when configured `"never"`:
 
-```js
+```jsx
 <Hello
     personal />
 
@@ -43,7 +44,7 @@ The following patterns are considered warnings when configured `"never"`:
 
 The following patterns are not considered warnings when configured `"never"`:
 
-```js
+```jsx
 <Hello personal={true} />
 
 <Hello personal={true}
@@ -53,14 +54,19 @@ The following patterns are not considered warnings when configured `"never"`:
 
 The following patterns are considered warnings when configured `"multiline"`:
 
-```js
+```jsx
 <Hello personal
     prop />
 ```
 
+```jsx
+<Hello foo={{
+}} />
+```
+
 The following patterns are not considered warnings when configured `"multiline"`:
 
-```js
+```jsx
 <Hello personal={true} />
 
 <Hello
@@ -69,6 +75,27 @@ The following patterns are not considered warnings when configured `"multiline"`
 />
 ```
 
+The following patterns are considered warnings when configured `"multiline-multiprop"`:
+
+```jsx
+<Hello foo={{
+    }}
+    bar />
+```
+
+The following patterns are not considered warnings when configured `"multiline-multiprop"`:
+
+```jsx
+<Hello foo={{
+}} />
+
+<Hello
+    foo={{
+    }}
+    bar
+/>
+```
+
 ## When not to use
 
 If you are not using JSX then you can disable this rule.
diff --git a/docs/rules/no-render-return-value.md b/docs/rules/no-render-return-value.md
index d028ec09..03379623 100644
--- a/docs/rules/no-render-return-value.md
+++ b/docs/rules/no-render-return-value.md
@@ -8,14 +8,14 @@ Source: [React Top-Level API documentation](http://facebook.github.io/react/docs
 
 This rule will warn you if you try to use the `ReactDOM.render()` return value.
 
-The following pattern is considered warning:
+The following pattern is considered a warning:
 
 ```js
 const inst = ReactDOM.render(<App />, document.body);
 doSomethingWithInst(inst);
 ```
 
-The following patterns are not considered warning:
+The following patterns are not considered warnings:
 
 ```js
 ReactDOM.render(<App ref={doSomethingWithInst} />, document.body);
diff --git a/docs/rules/prefer-stateless-function.md b/docs/rules/prefer-stateless-function.md
index 3efee0ca..aa8285e5 100644
--- a/docs/rules/prefer-stateless-function.md
+++ b/docs/rules/prefer-stateless-function.md
@@ -8,7 +8,7 @@ This rule will check your class based React components for
 
 * methods/properties other than `displayName`, `propTypes`, `render` and useless constructor (same detection as ESLint [no-useless-constructor rule](http://eslint.org/docs/rules/no-useless-constructor))
 * instance property other than `this.props` and `this.context`
-* extension of `React.PureComponent` ()
+* extension of `React.PureComponent` (if the `ignorePureComponents` flag is true)
 * presence of `ref` attribute in JSX
 * `render` method that return anything but JSX: `undefined`, `null`, etc. (only in React <15.0.0, see [shared settings](https://github.com/yannickcr/eslint-plugin-react/blob/master/README.md#configuration) for React version configuration)
 
diff --git a/index.js b/index.js
index 6bc26713..758e8d84 100644
--- a/index.js
+++ b/index.js
@@ -1,12 +1,6 @@
 'use strict';
 
-var deprecatedRules = {
-  'no-comment-textnodes': require('./lib/rules/no-comment-textnodes'),
-  'require-extension': require('./lib/rules/require-extension'),
-  'wrap-multilines': require('./lib/rules/wrap-multilines')
-};
-
-var rules = {
+var allRules = {
   'jsx-uses-react': require('./lib/rules/jsx-uses-react'),
   'no-multi-comp': require('./lib/rules/no-multi-comp'),
   'prop-types': require('./lib/rules/prop-types'),
@@ -58,34 +52,50 @@ var rules = {
   'no-danger-with-children': require('./lib/rules/no-danger-with-children'),
   'style-prop-object': require('./lib/rules/style-prop-object'),
   'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
-  'no-children-prop': require('./lib/rules/no-children-prop')
+  'no-children-prop': require('./lib/rules/no-children-prop'),
+  'no-comment-textnodes': require('./lib/rules/no-comment-textnodes'),
+  'require-extension': require('./lib/rules/require-extension'),
+  'wrap-multilines': require('./lib/rules/wrap-multilines')
 };
 
-var ruleNames = Object.keys(rules);
-var allRules = {};
-for (var i = 0; i < ruleNames.length; i++) {
-  allRules['react/' + ruleNames[i]] = 2;
-}
-
-var exportedRules = {};
-for (var key in rules) {
-  if (!rules.hasOwnProperty(key)) {
-    continue;
+function filterRules(rules, predicate) {
+  var result = {};
+  for (var key in rules) {
+    if (rules.hasOwnProperty(key) && predicate(rules[key])) {
+      result[key] = rules[key];
+    }
   }
-  exportedRules[key] = rules[key];
+  return result;
 }
-for (var deprecatedKey in deprecatedRules) {
-  if (!deprecatedRules.hasOwnProperty(deprecatedKey)) {
-    continue;
+
+function configureAsError(rules) {
+  var result = {};
+  for (var key in rules) {
+    if (!rules.hasOwnProperty(key)) {
+      continue;
+    }
+    result['react/' + key] = 2;
   }
-  exportedRules[deprecatedKey] = deprecatedRules[deprecatedKey];
+  return result;
 }
 
+var activeRules = filterRules(allRules, function(rule) {
+  return !rule.meta.deprecated;
+});
+var activeRulesConfig = configureAsError(activeRules);
+
+var deprecatedRules = filterRules(allRules, function(rule) {
+  return rule.meta.deprecated;
+});
+
 module.exports = {
   deprecatedRules: deprecatedRules,
-  rules: exportedRules,
+  rules: allRules,
   configs: {
     recommended: {
+      plugin: [
+        'react'
+      ],
       parserOptions: {
         ecmaFeatures: {
           jsx: true
@@ -109,12 +119,15 @@ module.exports = {
       }
     },
     all: {
+      plugin: [
+        'react'
+      ],
       parserOptions: {
         ecmaFeatures: {
           jsx: true
         }
       },
-      rules: allRules
+      rules: activeRulesConfig
     }
   }
 };
diff --git a/lib/rules/forbid-component-props.js b/lib/rules/forbid-component-props.js
index 4218112c..8d6ba9f9 100644
--- a/lib/rules/forbid-component-props.js
+++ b/lib/rules/forbid-component-props.js
@@ -48,7 +48,7 @@ module.exports = {
     return {
       JSXAttribute: function(node) {
         var tag = node.parent.name.name;
-        if (tag[0] !== tag[0].toUpperCase()) {
+        if (tag && tag[0] !== tag[0].toUpperCase()) {
           // This is a DOM node, not a Component, so exit.
           return;
         }
diff --git a/lib/rules/jsx-closing-bracket-location.js b/lib/rules/jsx-closing-bracket-location.js
index fb25c4f4..9da18146 100644
--- a/lib/rules/jsx-closing-bracket-location.js
+++ b/lib/rules/jsx-closing-bracket-location.js
@@ -146,6 +146,33 @@ module.exports = {
       }
     }
 
+    /**
+     * Get the characters used for indentation on the line to be matched
+     * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
+     * @param {String} expectedLocation Expected location for the closing bracket
+     * @param {Number} correctColumn Expected column for the closing bracket
+     * @return {String} The characters used for indentation
+     */
+    function getIndentation(tokens, expectedLocation, correctColumn) {
+      var indentation, spaces = [];
+      switch (expectedLocation) {
+        case 'props-aligned':
+          indentation = /^\s*/.exec(sourceCode.lines[tokens.lastProp.firstLine - 1])[0];
+          break;
+        case 'tag-aligned':
+        case 'line-aligned':
+          indentation = /^\s*/.exec(sourceCode.lines[tokens.opening.line - 1])[0];
+          break;
+        default:
+          indentation = '';
+      }
+      if (indentation.length + 1 < correctColumn) {
+        // Non-whitespace characters were included in the column offset
+        spaces = new Array(+correctColumn + 1 - indentation.length);
+      }
+      return indentation + spaces.join(' ');
+    }
+
     /**
      * Get the locations of the opening bracket, closing bracket, last prop, and
      * start of opening line.
@@ -244,9 +271,8 @@ module.exports = {
               case 'props-aligned':
               case 'tag-aligned':
               case 'line-aligned':
-                var spaces = new Array(+correctColumn + 1);
                 return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end],
-                  '\n' + spaces.join(' ') + closingTag);
+                  '\n' + getIndentation(tokens, expectedLocation, correctColumn) + closingTag);
               default:
                 return true;
             }
diff --git a/lib/rules/jsx-first-prop-new-line.js b/lib/rules/jsx-first-prop-new-line.js
index 4a377465..981dc62e 100644
--- a/lib/rules/jsx-first-prop-new-line.js
+++ b/lib/rules/jsx-first-prop-new-line.js
@@ -17,7 +17,7 @@ module.exports = {
     },
 
     schema: [{
-      enum: ['always', 'never', 'multiline']
+      enum: ['always', 'never', 'multiline', 'multiline-multiprop']
     }]
   },
 
@@ -30,7 +30,11 @@ module.exports = {
 
     return {
       JSXOpeningElement: function (node) {
-        if ((configuration === 'multiline' && isMultilineJSX(node)) || (configuration === 'always')) {
+        if (
+          (configuration === 'multiline' && isMultilineJSX(node)) ||
+          (configuration === 'multiline-multiprop' && isMultilineJSX(node) && node.attributes.length > 1) ||
+          (configuration === 'always')
+        ) {
           node.attributes.forEach(function(decl) {
             if (decl.loc.start.line === node.loc.start.line) {
               context.report({
diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js
index ef023dde..14c1a640 100644
--- a/lib/rules/jsx-indent.js
+++ b/lib/rules/jsx-indent.js
@@ -84,7 +84,6 @@ module.exports = {
     function getFixerFunction(node, needed, gotten) {
       if (needed > gotten) {
         var spaces = indentChar.repeat(needed - gotten);
-
         return function(fixer) {
           return fixer.insertTextBeforeRange([node.range[0], node.range[0]], spaces);
         };
@@ -169,7 +168,7 @@ module.exports = {
       var token = node;
       do {
         token = sourceCode.getTokenBefore(token);
-      } while (token.type === 'JSXText');
+      } while (token.type === 'JSXText' && /^\s*$/.test(token.value));
       var startLine = node.loc.start.line;
       var endLine = token ? token.loc.end.line : -1;
 
@@ -190,16 +189,6 @@ module.exports = {
       );
     }
 
-  /**
-   * Check if the node is the parenthesized right member of a logical expression
-   * @param {ASTNode} node The node to check
-   * @return {Boolean} true if its the case, false if not
-   */
-    function isParenthesizedInLogicalExp(node) {
-      var token = sourceCode.getTokenBefore(node);
-      return isRightInLogicalExp(node) && token.type === 'Punctuator' && token.value === '(';
-    }
-
     /**
      * Check indent for nodes list
      * @param {ASTNode} node The node to check
@@ -208,7 +197,8 @@ module.exports = {
      */
     function checkNodesIndent(node, indent, excludeCommas) {
       var nodeIndent = getNodeIndent(node, false, excludeCommas);
-      if (nodeIndent !== indent && isNodeFirstInLine(node) && !isParenthesizedInLogicalExp(node)) {
+      var isCorrectRightInLogicalExp = isRightInLogicalExp(node) && (nodeIndent - indent) === indentSize;
+      if (nodeIndent !== indent && isNodeFirstInLine(node) && !isCorrectRightInLogicalExp) {
         report(node, indent, nodeIndent);
       }
     }
@@ -219,7 +209,8 @@ module.exports = {
         if (!prevToken) {
           return;
         }
-        if (prevToken.type === 'JSXText') {
+        // Use the parent in a list or an array
+        if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
           prevToken = sourceCode.getNodeByRangeIndex(prevToken.start).parent;
         }
         var parentElementIndent = getNodeIndent(prevToken);
diff --git a/lib/rules/no-comment-textnodes.js b/lib/rules/no-comment-textnodes.js
index 92e7f482..83ae7450 100644
--- a/lib/rules/no-comment-textnodes.js
+++ b/lib/rules/no-comment-textnodes.js
@@ -15,6 +15,7 @@ var isWarnedForDeprecation = false;
 
 module.exports = {
   meta: {
+    deprecated: true,
     docs: {
       description: 'Comments inside children section of tag should be placed inside braces',
       category: 'Possible Errors',
diff --git a/lib/rules/require-extension.js b/lib/rules/require-extension.js
index 1a8a2ed2..273d3563 100644
--- a/lib/rules/require-extension.js
+++ b/lib/rules/require-extension.js
@@ -24,6 +24,7 @@ var PKG_REGEX = /^[^\.]((?!\/).)*$/;
 
 module.exports = {
   meta: {
+    deprecated: true,
     docs: {
       description: 'Restrict file extensions that may be required',
       category: 'Stylistic Issues',
diff --git a/lib/rules/wrap-multilines.js b/lib/rules/wrap-multilines.js
index 2b6900ee..439acbbe 100644
--- a/lib/rules/wrap-multilines.js
+++ b/lib/rules/wrap-multilines.js
@@ -15,6 +15,7 @@ var isWarnedForDeprecation = false;
 
 module.exports = {
   meta: {
+    deprecated: true,
     docs: {
       description: 'Prevent missing parentheses around multilines JSX',
       category: 'Stylistic Issues',
diff --git a/package.json b/package.json
index 3c2e75fd..97668331 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-react",
-  "version": "6.4.0",
+  "version": "6.4.1",
   "author": "Yannick Croissant <yannick.croissant+npm@gmail.com>",
   "description": "React specific linting rules for ESLint",
   "main": "index.js",


Test patch:
diff --git a/tests/index.js b/tests/index.js
index a85b4cb6..8e058037 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -23,6 +23,20 @@ describe('all rule files should be exported by the plugin', function() {
   });
 });
 
+describe('deprecated rules', function() {
+  it('marks all deprecated rules as deprecated', function() {
+    ruleFiles.forEach(function(ruleName) {
+      var inDeprecatedRules = Boolean(plugin.deprecatedRules[ruleName]);
+      var isDeprecated = plugin.rules[ruleName].meta.deprecated;
+      if (inDeprecatedRules) {
+        assert(isDeprecated, ruleName + ' metadata should mark it as deprecated');
+      } else {
+        assert(!isDeprecated, ruleName + ' metadata should not mark it as deprecated');
+      }
+    });
+  });
+});
+
 describe('configurations', function() {
   it('should export a \'recommended\' configuration', function() {
     assert(plugin.configs.recommended);
diff --git a/tests/lib/rules/forbid-component-props.js b/tests/lib/rules/forbid-component-props.js
index 9483f80e..227e4e90 100644
--- a/tests/lib/rules/forbid-component-props.js
+++ b/tests/lib/rules/forbid-component-props.js
@@ -81,6 +81,33 @@ ruleTester.run('forbid-component-props', rule, {
     ].join('\n'),
     options: [{forbid: ['style', 'foo']}],
     parserOptions: parserOptions
+  }, {
+    code: [
+      'var First = React.createClass({',
+      '  propTypes: externalPropTypes,',
+      '  render: function() {',
+      '    return <this.Foo bar="baz" />;',
+      '  }',
+      '});'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'class First extends React.createClass {',
+      '  render() {',
+      '    return <this.foo className="bar" />;',
+      '  }',
+      '}'
+    ].join('\n'),
+    options: [{forbid: ['style']}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'const First = (props) => (',
+      '  <this.Foo {...props} />',
+      ');'
+    ].join('\n'),
+    parserOptions: parserOptions
   }],
 
   invalid: [{
diff --git a/tests/lib/rules/jsx-closing-bracket-location.js b/tests/lib/rules/jsx-closing-bracket-location.js
index 9a2f2a14..7571a1e5 100644
--- a/tests/lib/rules/jsx-closing-bracket-location.js
+++ b/tests/lib/rules/jsx-closing-bracket-location.js
@@ -398,6 +398,334 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
     ].join('\n'),
     options: [{location: 'tag-aligned'}],
     parserOptions: parserOptions
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App ',
+      '\tfoo />'
+    ].join('\n'),
+    options: ['after-props'],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '\t/>'
+    ].join('\n'),
+    options: ['props-aligned'],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App ',
+      '\tfoo />'
+    ].join('\n'),
+    options: [{location: 'after-props'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '\t/>'
+    ].join('\n'),
+    options: [{location: 'props-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '\tfoo',
+      '></App>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '\tfoo',
+      '></App>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '\tfoo',
+      '\t></App>'
+    ].join('\n'),
+    options: [{location: 'props-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '\tfoo={function() {',
+      '\t\tconsole.log(\'bar\');',
+      '\t}} />'
+    ].join('\n'),
+    options: [{location: 'after-props'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '\tfoo={function() {',
+      '\t\tconsole.log(\'bar\');',
+      '\t}}',
+      '\t/>'
+    ].join('\n'),
+    options: [{location: 'props-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '\tfoo={function() {',
+      '\t\tconsole.log(\'bar\');',
+      '\t}}',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '\tfoo={function() {',
+      '\t\tconsole.log(\'bar\');',
+      '\t}}',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={function() {',
+      '\tconsole.log(\'bar\');',
+      '}}/>'
+    ].join('\n'),
+    options: [{location: 'after-props'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={function() {',
+      '\t\t\tconsole.log(\'bar\');',
+      '\t\t}}',
+      '     />'
+    ].join('\n'),
+    options: [{location: 'props-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={function() {',
+      '\tconsole.log(\'bar\');',
+      '}}',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={function() {',
+      '\tconsole.log(\'bar\');',
+      '}}',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Provider store>',
+      '\t<App',
+      '\t\tfoo />',
+      '</Provider>'
+    ].join('\n'),
+    options: [{selfClosing: 'after-props'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Provider ',
+      '\tstore',
+      '>',
+      '\t<App',
+      '\t\tfoo />',
+      '</Provider>'
+    ].join('\n'),
+    options: [{selfClosing: 'after-props'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Provider ',
+      '\tstore>',
+      '\t<App ',
+      '\t\tfoo',
+      '\t/>',
+      '</Provider>'
+    ].join('\n'),
+    options: [{nonEmpty: 'after-props'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Provider store>',
+      '\t<App ',
+      '\t\tfoo',
+      '\t\t/>',
+      '</Provider>'
+    ].join('\n'),
+    options: [{selfClosing: 'props-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Provider',
+      '\tstore',
+      '\t>',
+      '\t<App ',
+      '\t\tfoo',
+      '\t/>',
+      '</Provider>'
+    ].join('\n'),
+    options: [{nonEmpty: 'props-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'var x = function() {',
+      '\treturn <App',
+      '\t\tfoo',
+      '\t       >',
+      '\t\t\tbar',
+      '\t       </App>',
+      '}'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'var x = function() {',
+      '\treturn <App',
+      '\t\tfoo',
+      '\t       />',
+      '}'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'var x = <App',
+      '\tfoo',
+      '        />'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'var x = function() {',
+      '\treturn <App',
+      '\t\tfoo={function() {',
+      '\t\t\tconsole.log(\'bar\');',
+      '\t\t}}',
+      '\t/>',
+      '}'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'var x = <App',
+      '\tfoo={function() {',
+      '\t\tconsole.log(\'bar\');',
+      '\t}}',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Provider',
+      '\tstore',
+      '>',
+      '\t<App',
+      '\t\tfoo={function() {',
+      '\t\t\tconsole.log(\'bar\');',
+      '\t\t}}',
+      '\t/>',
+      '</Provider>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Provider',
+      '\tstore',
+      '>',
+      '\t{baz && <App',
+      '\t\tfoo={function() {',
+      '\t\t\tconsole.log(\'bar\');',
+      '\t\t}}',
+      '\t/>}',
+      '</Provider>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '\t<Foo',
+      '\t\tbar',
+      '\t>',
+      '\t</Foo>',
+      '\t<Foo',
+      '\t\tbar />',
+      '</App>'
+    ].join('\n'),
+    options: [{
+      nonEmpty: false,
+      selfClosing: 'after-props'
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '\t<Foo',
+      '\t\tbar>',
+      '\t</Foo>',
+      '\t<Foo',
+      '\t\tbar',
+      '\t/>',
+      '</App>'
+    ].join('\n'),
+    options: [{
+      nonEmpty: 'after-props',
+      selfClosing: false
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div className={[',
+      '\t"some",',
+      '\t"stuff",',
+      '\t2 ]}',
+      '>',
+      '\tSome text',
+      '</div>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
   }],
 
   invalid: [{
@@ -959,5 +1287,534 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
       line: 4,
       column: 7
     }]
+  }, {
+    code: [
+      '<App ',
+      '\tfoo />'
+    ].join('\n'),
+    output: [
+      '<App ',
+      '\tfoo',
+      '\t/>'
+    ].join('\n'),
+    options: [{location: 'props-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_PROPS_ALIGNED, 2, true),
+      line: 2,
+      column: 6
+    }]
+  }, {
+    code: [
+      '<App ',
+      '\tfoo />'
+    ].join('\n'),
+    output: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 1, true),
+      line: 2,
+      column: 6
+    }]
+  }, {
+    code: [
+      '<App ',
+      '\tfoo />'
+    ].join('\n'),
+    output: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_LINE_ALIGNED, 1, true),
+      line: 2,
+      column: 6
+    }]
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    output: [
+      '<App ',
+      '\tfoo/>'
+    ].join('\n'),
+    options: [{location: 'after-props'}],
+    parserOptions: parserOptions,
+    errors: MESSAGE_AFTER_PROPS
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    output: [
+      '<App ',
+      '\tfoo',
+      '\t/>'
+    ].join('\n'),
+    options: [{location: 'props-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_PROPS_ALIGNED, 2, false),
+      line: 3,
+      column: 1
+    }]
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '\t/>'
+    ].join('\n'),
+    output: [
+      '<App ',
+      '\tfoo/>'
+    ].join('\n'),
+    options: [{location: 'after-props'}],
+    parserOptions: parserOptions,
+    errors: MESSAGE_AFTER_PROPS
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '\t/>'
+    ].join('\n'),
+    output: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 1, false),
+      line: 3,
+      column: 2
+    }]
+  }, {
+    code: [
+      '<App ',
+      '\tfoo',
+      '\t/>'
+    ].join('\n'),
+    output: [
+      '<App ',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_LINE_ALIGNED, 1, false),
+      line: 3,
+      column: 2
+    }]
+  }, {
+    code: [
+      '<App',
+      '\tfoo',
+      '></App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      '\tfoo></App>'
+    ].join('\n'),
+    options: [{location: 'after-props'}],
+    parserOptions: parserOptions,
+    errors: MESSAGE_AFTER_PROPS
+  }, {
+    code: [
+      '<App',
+      '\tfoo',
+      '></App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      '\tfoo',
+      '\t></App>'
+    ].join('\n'),
+    options: [{location: 'props-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_PROPS_ALIGNED, 2, false),
+      line: 3,
+      column: 1
+    }]
+  }, {
+    code: [
+      '<App',
+      '\tfoo',
+      '\t></App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      '\tfoo></App>'
+    ].join('\n'),
+    options: [{location: 'after-props'}],
+    parserOptions: parserOptions,
+    errors: MESSAGE_AFTER_PROPS
+  }, {
+    code: [
+      '<App',
+      '\tfoo',
+      '\t></App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      '\tfoo',
+      '></App>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 1, false),
+      line: 3,
+      column: 2
+    }]
+  }, {
+    code: [
+      '<App',
+      '\tfoo',
+      '\t></App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      '\tfoo',
+      '></App>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_LINE_ALIGNED, 1, false),
+      line: 3,
+      column: 2
+    }]
+  }, {
+    code: [
+      '<Provider ',
+      '\tstore>', // <--
+      '\t<App ',
+      '\t\tfoo',
+      '\t\t/>',
+      '</Provider>'
+    ].join('\n'),
+    output: [
+      '<Provider ',
+      '\tstore',
+      '>',
+      '\t<App ',
+      '\t\tfoo',
+      '\t\t/>',
+      '</Provider>'
+    ].join('\n'),
+    options: [{selfClosing: 'props-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 1, true),
+      line: 2,
+      column: 7
+    }]
+  }, {
+    code: [
+      'const Button = function(props) {',
+      '\treturn (',
+      '\t\t<Button',
+      '\t\t\tsize={size}',
+      '\t\t\tonClick={onClick}',
+      '\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>',
+      '\t\t\tButton Text',
+      '\t\t</Button>',
+      '\t);',
+      '};'
+    ].join('\n'),
+    output: [
+      'const Button = function(props) {',
+      '\treturn (',
+      '\t\t<Button',
+      '\t\t\tsize={size}',
+      '\t\t\tonClick={onClick}',
+      '\t\t\t>',
+      '\t\t\tButton Text',
+      '\t\t</Button>',
+      '\t);',
+      '};'
+    ].join('\n'),
+    options: ['props-aligned'],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_PROPS_ALIGNED, 4, false),
+      line: 6,
+      column: 19
+    }]
+  }, {
+    code: [
+      'const Button = function(props) {',
+      '\treturn (',
+      '\t\t<Button',
+      '\t\t\tsize={size}',
+      '\t\t\tonClick={onClick}',
+      '\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>',
+      '\t\t\tButton Text',
+      '\t\t</Button>',
+      '\t);',
+      '};'
+    ].join('\n'),
+    output: [
+      'const Button = function(props) {',
+      '\treturn (',
+      '\t\t<Button',
+      '\t\t\tsize={size}',
+      '\t\t\tonClick={onClick}',
+      '\t\t>',
+      '\t\t\tButton Text',
+      '\t\t</Button>',
+      '\t);',
+      '};'
+    ].join('\n'),
+    options: ['tag-aligned'],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 3, false),
+      line: 6,
+      column: 19
+    }]
+  }, {
+    code: [
+      'const Button = function(props) {',
+      '\treturn (',
+      '\t\t<Button',
+      '\t\t\tsize={size}',
+      '\t\t\tonClick={onClick}',
+      '\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>',
+      '\t\t\tButton Text',
+      '\t\t</Button>',
+      '\t);',
+      '};'
+    ].join('\n'),
+    output: [
+      'const Button = function(props) {',
+      '\treturn (',
+      '\t\t<Button',
+      '\t\t\tsize={size}',
+      '\t\t\tonClick={onClick}',
+      '\t\t>',
+      '\t\t\tButton Text',
+      '\t\t</Button>',
+      '\t);',
+      '};'
+    ].join('\n'),
+    options: ['line-aligned'],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_LINE_ALIGNED, 3, false),
+      line: 6,
+      column: 19
+    }]
+  }, {
+    code: [
+      '<Provider',
+      '\tstore',
+      '\t>',
+      '\t<App ',
+      '\t\tfoo',
+      '\t\t/>', // <--
+      '</Provider>'
+    ].join('\n'),
+    output: [
+      '<Provider',
+      '\tstore',
+      '\t>',
+      '\t<App ',
+      '\t\tfoo',
+      '\t/>',
+      '</Provider>'
+    ].join('\n'),
+    options: [{nonEmpty: 'props-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 2, false),
+      line: 6,
+      column: 3
+    }]
+  }, {
+    code: [
+      '<Provider ',
+      '\tstore>', // <--
+      '\t<App',
+      '\t\tfoo />',
+      '</Provider>'
+    ].join('\n'),
+    output: [
+      '<Provider ',
+      '\tstore',
+      '>',
+      '\t<App',
+      '\t\tfoo />',
+      '</Provider>'
+    ].join('\n'),
+    options: [{selfClosing: 'after-props'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 1, true),
+      line: 2,
+      column: 7
+    }]
+  }, {
+    code: [
+      '<Provider ',
+      '\tstore>',
+      '\t<App ',
+      '\t\tfoo',
+      '\t\t/>', // <--
+      '</Provider>'
+    ].join('\n'),
+    output: [
+      '<Provider ',
+      '\tstore>',
+      '\t<App ',
+      '\t\tfoo',
+      '\t/>', // <--
+      '</Provider>'
+    ].join('\n'),
+    options: [{nonEmpty: 'after-props'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 2, false),
+      line: 5,
+      column: 3
+    }]
+  }, {
+    code: [
+      'var x = function() {',
+      '\treturn <App',
+      '\t\tfoo',
+      '\t\t\t\t />',
+      '}'
+    ].join('\n'),
+    output: [
+      'var x = function() {',
+      '\treturn <App',
+      '\t\tfoo',
+      '\t/>',
+      '}'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_LINE_ALIGNED, 2, false),
+      line: 4,
+      column: 6
+    }]
+  }, {
+    code: [
+      'var x = <App',
+      '\tfoo',
+      '        />'
+    ].join('\n'),
+    output: [
+      'var x = <App',
+      '\tfoo',
+      '/>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_LINE_ALIGNED, 1, false),
+      line: 3,
+      column: 9
+    }]
+  }, {
+    code: [
+      'var x = (',
+      '\t<div',
+      '\t\tclassName="MyComponent"',
+      '\t\t{...props} />',
+      ')'
+    ].join('\n'),
+    output: [
+      'var x = (',
+      '\t<div',
+      '\t\tclassName="MyComponent"',
+      '\t\t{...props}',
+      '\t/>',
+      ')'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_LINE_ALIGNED, 2, true),
+      line: 4,
+      column: 14
+    }]
+  }, {
+    code: [
+      'var x = (',
+      '\t<Something',
+      '\t\tcontent={<Foo />} />',
+      ')'
+    ].join('\n'),
+    output: [
+      'var x = (',
+      '\t<Something',
+      '\t\tcontent={<Foo />}',
+      '\t/>',
+      ')'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_LINE_ALIGNED, 2, true),
+      line: 3,
+      column: 21
+    }]
+  }, {
+    code: [
+      'var x = (',
+      '\t<Something ',
+      '\t\t/>',
+      ')'
+    ].join('\n'),
+    output: [
+      'var x = (',
+      '\t<Something />',
+      ')'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions,
+    errors: [MESSAGE_AFTER_TAG]
+  }, {
+    code: [
+      '<div className={[',
+      '\t"some",',
+      '\t"stuff",',
+      '\t2 ]}>',
+      '\tSome text',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div className={[',
+      '\t"some",',
+      '\t"stuff",',
+      '\t2 ]}',
+      '>',
+      '\tSome text',
+      '</div>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions,
+    errors: [{
+      message: messageWithDetails(MESSAGE_TAG_ALIGNED, 1, true),
+      line: 4,
+      column: 6
+    }]
   }]
 });
diff --git a/tests/lib/rules/jsx-first-prop-new-line.js b/tests/lib/rules/jsx-first-prop-new-line.js
index 2fd2dab2..9feb9cfd 100644
--- a/tests/lib/rules/jsx-first-prop-new-line.js
+++ b/tests/lib/rules/jsx-first-prop-new-line.js
@@ -90,6 +90,39 @@ ruleTester.run('jsx-first-prop-new-line', rule, {
       options: ['multiline'],
       parser: parserOptions
     },
+    {
+      code: [
+        '<Foo bar />'
+      ].join('\n'),
+      options: ['multiline-multiprop'],
+      parser: parserOptions
+    },
+    {
+      code: [
+        '<Foo bar baz />'
+      ].join('\n'),
+      options: ['multiline-multiprop'],
+      parser: parserOptions
+    },
+    {
+      code: [
+        '<Foo prop={{',
+        '}} />'
+      ].join('\n'),
+      options: ['multiline-multiprop'],
+      parser: parserOptions
+    },
+    {
+      code: [
+        '<Foo ',
+        '  foo={{',
+        '  }}',
+        '  bar',
+        '/>'
+      ].join('\n'),
+      options: ['multiline-multiprop'],
+      parser: parserOptions
+    },
     {
       code: '<Foo />',
       options: ['always'],
@@ -144,6 +177,24 @@ ruleTester.run('jsx-first-prop-new-line', rule, {
       options: ['never'],
       errors: [{message: 'Property should be placed on the same line as the component declaration'}],
       parser: parserOptions
+    },
+    {
+      code: [
+        '<Foo prop={{',
+        '}} />'
+      ].join('\n'),
+      options: ['multiline'],
+      errors: [{message: 'Property should be placed on a new line'}],
+      parser: parserOptions
+    },
+    {
+      code: [
+        '<Foo bar={{',
+        '}} baz />'
+      ].join('\n'),
+      options: ['multiline-multiprop'],
+      errors: [{message: 'Property should be placed on a new line'}],
+      parser: parserOptions
     }
   ]
 });
diff --git a/tests/lib/rules/jsx-indent.js b/tests/lib/rules/jsx-indent.js
index e487f11d..04af0d1c 100644
--- a/tests/lib/rules/jsx-indent.js
+++ b/tests/lib/rules/jsx-indent.js
@@ -144,6 +144,17 @@ ruleTester.run('jsx-indent', rule, {
     ].join('\n'),
     parserOptions: parserOptions,
     options: [2]
+  }, {
+    code: [
+      '{',
+      '  head.title &&',
+      '    <h1>',
+      '      {head.title}',
+      '    </h1>',
+      '}'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    options: [2]
   }, {
     code: [
       '{',
@@ -167,6 +178,26 @@ ruleTester.run('jsx-indent', rule, {
     ].join('\n'),
     parserOptions: parserOptions,
     options: [2]
+  }, {
+    code: [
+      '[',
+      '  <div />,',
+      '  <div />',
+      ']'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    options: [2]
+  }, {
+    // Literals indentation is not touched
+    code: [
+      '<div>',
+      'bar <div>',
+      '   bar',
+      '   bar {foo}',
+      'bar </div>',
+      '</div>'
+    ].join('\n'),
+    parserOptions: parserOptions
   }],
 
   invalid: [{
@@ -320,6 +351,24 @@ ruleTester.run('jsx-indent', rule, {
     errors: [
       {message: 'Expected indentation of 1 tab character but found 0.'}
     ]
+  }, {
+    code: [
+      '[',
+      '  <div />,',
+      '    <div />',
+      ']'
+    ].join('\n'),
+    output: [
+      '[',
+      '  <div />,',
+      '  <div />',
+      ']'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    options: [2],
+    errors: [
+      {message: 'Expected indentation of 2 space characters but found 4.'}
+    ]
   }
   // Tests for future work. See the comment on line 42-43 in the rule near to fixable: 'whitespace' meta property,
   // Right now fixer function doesn't support replacing tabs with whitespaces and vice-versa.