Repository: yannickcr/eslint-plugin-react
Pull request #1676 (2018-02-04 17:05:11)

[Rebased] `jsx-max-depth`: validate a specific depth for JSX
Originally proposed by @chriswong (https://github.com/yannickcr/eslint-plugin-react/pull/1260)



Patch:
diff --git a/docs/rules/jsx-max-depth.md b/docs/rules/jsx-max-depth.md
new file mode 100644
index 00000000..492d055e
--- /dev/null
+++ b/docs/rules/jsx-max-depth.md
@@ -0,0 +1,84 @@
+# Validate JSX maximum depth (react/jsx-max-depth)
+
+This option validates a specific depth for JSX.
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```jsx
+<App>
+  <Foo>
+    <Bar>
+      <Baz />
+    </Bar>
+  </Foo>
+</App>
+
+```
+
+## Rule Options
+
+It takes an option as the second parameter which can be a positive number for depth count.
+
+```js
+...
+"react/jsx-no-depth": [<enabled>, { "max": <number> }]
+...
+```
+
+The following patterns are considered warnings:
+
+```jsx
+// [2, { "max": 1 }]
+<App>
+  <Foo>
+    <Bar />
+  </Foo>
+</App>
+
+// [2, { "max": 1 }]
+const foobar = <Foo><Bar /></Foo>;
+<App>
+  {foobar}
+</App>
+
+// [2, { "max": 2 }]
+<App>
+  <Foo>
+    <Bar>
+      <Baz />
+    </Bar>
+  </Foo>
+</App>
+```
+
+The following patterns are not warnings:
+
+```jsx
+
+// [2, { "max": 1 }]
+<App>
+  <Hello />
+</App>
+
+// [2,{ "max": 2 }]
+<App>
+  <Foo>
+    <Bar />
+  </Foo>
+</App>
+
+// [2, { "max": 3 }]
+<App>
+  <Foo>
+    <Bar>
+      <Baz />
+    </Bar>
+  </Foo>
+</App>
+```
+
+## When not to use
+
+If you are not using JSX then you can disable this rule.
diff --git a/index.js b/index.js
index 7b539e26..e0b12d3f 100644
--- a/index.js
+++ b/index.js
@@ -4,6 +4,7 @@ const has = require('has');
 
 const allRules = {
   'boolean-prop-naming': require('./lib/rules/boolean-prop-naming'),
+  'button-has-type': require('./lib/rules/button-has-type'),
   'default-props-match-prop-types': require('./lib/rules/default-props-match-prop-types'),
   'destructuring-assignment': require('./lib/rules/destructuring-assignment'),
   'display-name': require('./lib/rules/display-name'),
@@ -24,6 +25,7 @@ const allRules = {
   'jsx-indent': require('./lib/rules/jsx-indent'),
   'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
   'jsx-key': require('./lib/rules/jsx-key'),
+  'jsx-max-depth': require('./lib/rules/jsx-max-depth'),
   'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
   'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
   'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
@@ -31,7 +33,6 @@ const allRules = {
   'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
   'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
   'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
-  'button-has-type': require('./lib/rules/button-has-type'),
   'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
   'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
   'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
diff --git a/lib/rules/jsx-max-depth.js b/lib/rules/jsx-max-depth.js
new file mode 100644
index 00000000..b5b37237
--- /dev/null
+++ b/lib/rules/jsx-max-depth.js
@@ -0,0 +1,146 @@
+/**
+ * @fileoverview Validate JSX maximum depth
+ * @author Chris<wfsr@foxmail.com>
+ */
+'use strict';
+
+const has = require('has');
+const variableUtil = require('../util/variable');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Validate JSX maximum depth',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          max: {
+            type: 'integer',
+            minimum: 0
+          }
+        },
+        additionalProperties: false
+      }
+    ]
+  },
+  create: function(context) {
+    const MESSAGE = 'Expected the depth of nested jsx elements to be <= {{needed}}, but found {{found}}.';
+    const DEFAULT_DEPTH = 2;
+
+    const option = context.options[0] || {};
+    const maxDepth = has(option, 'max') ? option.max : DEFAULT_DEPTH;
+
+    function isJSXElement(node) {
+      return node.type === 'JSXElement';
+    }
+
+    function isExpression(node) {
+      return node.type === 'JSXExpressionContainer';
+    }
+
+    function hasJSX(node) {
+      return isJSXElement(node) || isExpression(node) && isJSXElement(node.expression);
+    }
+
+    function isLeaf(node) {
+      const children = node.children;
+
+      return !children.length || !children.some(hasJSX);
+    }
+
+    function getDepth(node) {
+      let count = 0;
+
+      while (isJSXElement(node.parent) || isExpression(node.parent)) {
+        node = node.parent;
+        if (isJSXElement(node)) {
+          count++;
+        }
+      }
+
+      return count;
+    }
+
+
+    function report(node, depth) {
+      context.report({
+        node: node,
+        message: MESSAGE,
+        data: {
+          found: depth,
+          needed: maxDepth
+        }
+      });
+    }
+
+    function findJSXElement(variables, name) {
+      function find(refs) {
+        let i = refs.length;
+
+        while (--i >= 0) {
+          if (has(refs[i], 'writeExpr')) {
+            const writeExpr = refs[i].writeExpr;
+
+            return isJSXElement(writeExpr)
+              && writeExpr
+              || writeExpr.type === 'Identifier'
+              && findJSXElement(variables, writeExpr.name);
+          }
+        }
+
+        return null;
+      }
+
+      const variable = variableUtil.getVariable(variables, name);
+      return variable && variable.references && find(variable.references);
+    }
+
+    function checkDescendant(baseDepth, children) {
+      children.forEach(node => {
+        if (!hasJSX(node)) {
+          return;
+        }
+
+        baseDepth++;
+        if (baseDepth > maxDepth) {
+          report(node, baseDepth);
+        } else if (!isLeaf(node)) {
+          checkDescendant(baseDepth, node.children);
+        }
+      });
+    }
+
+    return {
+      JSXElement: function(node) {
+        if (!isLeaf(node)) {
+          return;
+        }
+
+        const depth = getDepth(node);
+        if (depth > maxDepth) {
+          report(node, depth);
+        }
+      },
+      JSXExpressionContainer: function(node) {
+        if (node.expression.type !== 'Identifier') {
+          return;
+        }
+
+        const variables = variableUtil.variablesInScope(context);
+        const element = findJSXElement(variables, node.expression.name);
+
+        if (element) {
+          const baseDepth = getDepth(node);
+          checkDescendant(baseDepth, element.children);
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/jsx-max-depth.js b/tests/lib/rules/jsx-max-depth.js
new file mode 100644
index 00000000..0b2ceb1a
--- /dev/null
+++ b/tests/lib/rules/jsx-max-depth.js
@@ -0,0 +1,126 @@
+/**
+ * @fileoverview Validate JSX maximum depth
+ * @author Chris<wfsr@foxmail.com>
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/jsx-max-depth');
+const RuleTester = require('eslint').RuleTester;
+
+const parserOptions = {
+  sourceType: 'module',
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('jsx-max-depth', rule, {
+  valid: [{
+    code: [
+      '<App />'
+    ].join('\n')
+  }, {
+    code: [
+      '<App>',
+      '  <foo />',
+      '</App>'
+    ].join('\n'),
+    options: [{max: 1}]
+  }, {
+    code: [
+      '<App>',
+      '  <foo>',
+      '    <bar />',
+      '  </foo>',
+      '</App>'
+    ].join('\n')
+  }, {
+    code: [
+      '<App>',
+      '  <foo>',
+      '    <bar />',
+      '  </foo>',
+      '</App>'
+    ].join('\n'),
+    options: [{max: 2}]
+  }, {
+    code: [
+      'const x = <div><em>x</em></div>;',
+      '<div>{x}</div>'
+    ].join('\n'),
+    options: [{max: 2}]
+  }, {
+    code: 'const foo = (x) => <div><em>{x}</em></div>;',
+    options: [{max: 2}]
+  }],
+
+  invalid: [{
+    code: [
+      '<App>',
+      '  <foo />',
+      '</App>'
+    ].join('\n'),
+    options: [{max: 0}],
+    errors: [{message: 'Expected the depth of nested jsx elements to be <= 0, but found 1.'}]
+  }, {
+    code: [
+      '<App>',
+      '  <foo>{bar}</foo>',
+      '</App>'
+    ].join('\n'),
+    options: [{max: 0}],
+    errors: [{message: 'Expected the depth of nested jsx elements to be <= 0, but found 1.'}]
+  }, {
+    code: [
+      '<App>',
+      '  <foo>',
+      '    <bar />',
+      '  </foo>',
+      '</App>'
+    ].join('\n'),
+    options: [{max: 1}],
+    errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}]
+  }, {
+    code: [
+      'const x = <div><span /></div>;',
+      '<div>{x}</div>'
+    ].join('\n'),
+    options: [{max: 1}],
+    errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}]
+  }, {
+    code: [
+      'const x = <div><span /></div>;',
+      'let y = x;',
+      '<div>{y}</div>'
+    ].join('\n'),
+    options: [{max: 1}],
+    errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}]
+  }, {
+    code: [
+      'const x = <div><span /></div>;',
+      'let y = x;',
+      '<div>{x}-{y}</div>'
+    ].join('\n'),
+    options: [{max: 1}],
+    errors: [
+      {message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'},
+      {message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}
+    ]
+  }, {
+    code: [
+      '<div>',
+      '{<div><div><span /></div></div>}',
+      '</div>'
+    ].join('\n'),
+    errors: [{message: 'Expected the depth of nested jsx elements to be <= 2, but found 3.'}]
+  }]
+});