Skip to content

[New] jsx-max-depth: validate a specific depth for JSX #1260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/rules/jsx-max-depth.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -24,14 +25,14 @@ 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'),
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
'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'),
Expand Down
146 changes: 146 additions & 0 deletions lib/rules/jsx-max-depth.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
};
}
};
126 changes: 126 additions & 0 deletions tests/lib/rules/jsx-max-depth.js
Original file line number Diff line number Diff line change
@@ -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.'}]
}]
});