Skip to content

Commit

Permalink
Add new rule: jsx-max-depth, fix #1219
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswong committed Jun 22, 2017
1 parent 61b65a0 commit 89cc617
Show file tree
Hide file tree
Showing 4 changed files with 358 additions and 0 deletions.
84 changes: 84 additions & 0 deletions 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": 2 }]
<App>
<Foo>
<Bar />
</Foo>
</App>

// [2, { "max": 2 }]
const foobar = <Foo><Bar /></Foo>;
<App>
{foobar}
</App>

// [2, { "max": 3 }]
<App>
<Foo>
<Bar>
<Baz />
</Bar>
</Foo>
</App>
```

The following patterns are not warnings:

```jsx

// [2, { "max": 2 }]
<App>
<Hello />
</App>

// [2,{ "max": 3 }]
<App>
<Foo>
<Bar />
</Foo>
</App>

// [2, { "max": 4 }]
<App>
<Foo>
<Bar>
<Baz />
</Bar>
</Foo>
</App>
```

## When not to use

If you are not using JSX then you can disable this rule.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -50,6 +50,7 @@ var allRules = {
'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'),
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
'jsx-key': require('./lib/rules/jsx-key'),
'jsx-max-depth': require('./lib/rules/jsx-max-depth'),
'no-string-refs': require('./lib/rules/no-string-refs'),
'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'),
'require-render-return': require('./lib/rules/require-render-return'),
Expand Down
146 changes: 146 additions & 0 deletions 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: 1
}
},
additionalProperties: false
}
]
},
create: function(context) {
const MESSAGE = 'Expected the depth of JSX Elements nested should be {{needed}} but found {{found}}.';
const DEFAULT_DEPTH = 3;

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 = 1;

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(function(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);
}
}
};
}
};
127 changes: 127 additions & 0 deletions tests/lib/rules/jsx-max-depth.js
@@ -0,0 +1,127 @@
/**
* @fileoverview Validate JSX maximum depth
* @author Chris<wfsr@foxmail.com>
*/
'use strict';

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

var rule = require('../../../lib/rules/jsx-max-depth');
var RuleTester = require('eslint').RuleTester;

var parserOptions = {
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
};

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

var ruleTester = new RuleTester({parserOptions});
ruleTester.run('jsx-max-depth', rule, {
valid: [{
code: [
'<App />'
].join('\n')
}, {
code: [
'<App>',
' <foo />',
'</App>'
].join('\n'),
options: [{max: 2}]
}, {
code: [
'<App>',
' <foo>',
' <bar />',
' </foo>',
'</App>'
].join('\n'),
options: [{}]
}, {
code: [
'<App>',
' <foo>',
' <bar />',
' </foo>',
'</App>'
].join('\n'),
options: [{max: 3}]
}, {
code: [
'const x = <div><em>x</em></div>;',
'<div>{x}</div>'
].join('\n'),
options: [{max: 3}]
}, {
code: 'const foo = (x) => <div><em>{x}</em></div>;',
options: [{max: 3}]
}],

invalid: [{
code: [
'<App>',
' <foo />',
'</App>'
].join('\n'),
options: [{max: 1}],
errors: [{message: 'Expected the depth of JSX Elements nested should be 1 but found 2.'}]
}, {
code: [
'<App>',
' <foo>{bar}</foo>',
'</App>'
].join('\n'),
options: [{max: 1}],
errors: [{message: 'Expected the depth of JSX Elements nested should be 1 but found 2.'}]
}, {
code: [
'<App>',
' <foo>',
' <bar />',
' </foo>',
'</App>'
].join('\n'),
options: [{max: 2}],
errors: [{message: 'Expected the depth of JSX Elements nested should be 2 but found 3.'}]
}, {
code: [
'const x = <div><span /></div>;',
'<div>{x}</div>'
].join('\n'),
options: [{max: 2}],
errors: [{message: 'Expected the depth of JSX Elements nested should be 2 but found 3.'}]
}, {
code: [
'const x = <div><span /></div>;',
'let y = x;',
'<div>{y}</div>'
].join('\n'),
options: [{max: 2}],
errors: [{message: 'Expected the depth of JSX Elements nested should be 2 but found 3.'}]
}, {
code: [
'const x = <div><span /></div>;',
'let y = x;',
'<div>{x}-{y}</div>'
].join('\n'),
options: [{max: 2}],
errors: [
{message: 'Expected the depth of JSX Elements nested should be 2 but found 3.'},
{message: 'Expected the depth of JSX Elements nested should be 2 but found 3.'}
]
}, {
code: [
'<div>',
'{<div><div><span /></div></div>}',
'</div>'
].join('\n'),
errors: [{message: 'Expected the depth of JSX Elements nested should be 3 but found 4.'}]
}]
});

0 comments on commit 89cc617

Please sign in to comment.