Skip to content

Commit 34f83a5

Browse files
chriswongljharb
authored andcommitted
[New] Add jsx-max-depth
Fixes #1219.
1 parent e4de360 commit 34f83a5

File tree

4 files changed

+358
-1
lines changed

4 files changed

+358
-1
lines changed

docs/rules/jsx-max-depth.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Validate JSX maximum depth (react/jsx-max-depth)
2+
3+
This option validates a specific depth for JSX.
4+
5+
## Rule Details
6+
7+
The following patterns are considered warnings:
8+
9+
```jsx
10+
<App>
11+
<Foo>
12+
<Bar>
13+
<Baz />
14+
</Bar>
15+
</Foo>
16+
</App>
17+
18+
```
19+
20+
## Rule Options
21+
22+
It takes an option as the second parameter which can be a positive number for depth count.
23+
24+
```js
25+
...
26+
"react/jsx-no-depth": [<enabled>, { "max": <number> }]
27+
...
28+
```
29+
30+
The following patterns are considered warnings:
31+
32+
```jsx
33+
// [2, { "max": 1 }]
34+
<App>
35+
<Foo>
36+
<Bar />
37+
</Foo>
38+
</App>
39+
40+
// [2, { "max": 1 }]
41+
const foobar = <Foo><Bar /></Foo>;
42+
<App>
43+
{foobar}
44+
</App>
45+
46+
// [2, { "max": 2 }]
47+
<App>
48+
<Foo>
49+
<Bar>
50+
<Baz />
51+
</Bar>
52+
</Foo>
53+
</App>
54+
```
55+
56+
The following patterns are not warnings:
57+
58+
```jsx
59+
60+
// [2, { "max": 1 }]
61+
<App>
62+
<Hello />
63+
</App>
64+
65+
// [2,{ "max": 2 }]
66+
<App>
67+
<Foo>
68+
<Bar />
69+
</Foo>
70+
</App>
71+
72+
// [2, { "max": 3 }]
73+
<App>
74+
<Foo>
75+
<Bar>
76+
<Baz />
77+
</Bar>
78+
</Foo>
79+
</App>
80+
```
81+
82+
## When not to use
83+
84+
If you are not using JSX then you can disable this rule.

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const has = require('has');
44

55
const allRules = {
66
'boolean-prop-naming': require('./lib/rules/boolean-prop-naming'),
7+
'button-has-type': require('./lib/rules/button-has-type'),
78
'default-props-match-prop-types': require('./lib/rules/default-props-match-prop-types'),
89
'destructuring-assignment': require('./lib/rules/destructuring-assignment'),
910
'display-name': require('./lib/rules/display-name'),
@@ -24,14 +25,14 @@ const allRules = {
2425
'jsx-indent': require('./lib/rules/jsx-indent'),
2526
'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
2627
'jsx-key': require('./lib/rules/jsx-key'),
28+
'jsx-max-depth': require('./lib/rules/jsx-max-depth'),
2729
'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
2830
'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
2931
'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
3032
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
3133
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
3234
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
3335
'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
34-
'button-has-type': require('./lib/rules/button-has-type'),
3536
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
3637
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
3738
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),

lib/rules/jsx-max-depth.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @fileoverview Validate JSX maximum depth
3+
* @author Chris<[email protected]>
4+
*/
5+
'use strict';
6+
7+
const has = require('has');
8+
const variableUtil = require('../util/variable');
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
module.exports = {
14+
meta: {
15+
docs: {
16+
description: 'Validate JSX maximum depth',
17+
category: 'Stylistic Issues',
18+
recommended: false
19+
},
20+
schema: [
21+
{
22+
type: 'object',
23+
properties: {
24+
max: {
25+
type: 'integer',
26+
minimum: 0
27+
}
28+
},
29+
additionalProperties: false
30+
}
31+
]
32+
},
33+
create: function(context) {
34+
const MESSAGE = 'Expected the depth of nested jsx elements to be <= {{needed}}, but found {{found}}.';
35+
const DEFAULT_DEPTH = 2;
36+
37+
const option = context.options[0] || {};
38+
const maxDepth = has(option, 'max') ? option.max : DEFAULT_DEPTH;
39+
40+
function isJSXElement(node) {
41+
return node.type === 'JSXElement';
42+
}
43+
44+
function isExpression(node) {
45+
return node.type === 'JSXExpressionContainer';
46+
}
47+
48+
function hasJSX(node) {
49+
return isJSXElement(node) || isExpression(node) && isJSXElement(node.expression);
50+
}
51+
52+
function isLeaf(node) {
53+
const children = node.children;
54+
55+
return !children.length || !children.some(hasJSX);
56+
}
57+
58+
function getDepth(node) {
59+
let count = 0;
60+
61+
while (isJSXElement(node.parent) || isExpression(node.parent)) {
62+
node = node.parent;
63+
if (isJSXElement(node)) {
64+
count++;
65+
}
66+
}
67+
68+
return count;
69+
}
70+
71+
72+
function report(node, depth) {
73+
context.report({
74+
node: node,
75+
message: MESSAGE,
76+
data: {
77+
found: depth,
78+
needed: maxDepth
79+
}
80+
});
81+
}
82+
83+
function findJSXElement(variables, name) {
84+
function find(refs) {
85+
let i = refs.length;
86+
87+
while (--i >= 0) {
88+
if (has(refs[i], 'writeExpr')) {
89+
const writeExpr = refs[i].writeExpr;
90+
91+
return isJSXElement(writeExpr)
92+
&& writeExpr
93+
|| writeExpr.type === 'Identifier'
94+
&& findJSXElement(variables, writeExpr.name);
95+
}
96+
}
97+
98+
return null;
99+
}
100+
101+
const variable = variableUtil.getVariable(variables, name);
102+
return variable && variable.references && find(variable.references);
103+
}
104+
105+
function checkDescendant(baseDepth, children) {
106+
children.forEach(node => {
107+
if (!hasJSX(node)) {
108+
return;
109+
}
110+
111+
baseDepth++;
112+
if (baseDepth > maxDepth) {
113+
report(node, baseDepth);
114+
} else if (!isLeaf(node)) {
115+
checkDescendant(baseDepth, node.children);
116+
}
117+
});
118+
}
119+
120+
return {
121+
JSXElement: function(node) {
122+
if (!isLeaf(node)) {
123+
return;
124+
}
125+
126+
const depth = getDepth(node);
127+
if (depth > maxDepth) {
128+
report(node, depth);
129+
}
130+
},
131+
JSXExpressionContainer: function(node) {
132+
if (node.expression.type !== 'Identifier') {
133+
return;
134+
}
135+
136+
const variables = variableUtil.variablesInScope(context);
137+
const element = findJSXElement(variables, node.expression.name);
138+
139+
if (element) {
140+
const baseDepth = getDepth(node);
141+
checkDescendant(baseDepth, element.children);
142+
}
143+
}
144+
};
145+
}
146+
};

tests/lib/rules/jsx-max-depth.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* @fileoverview Validate JSX maximum depth
3+
* @author Chris<[email protected]>
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const rule = require('../../../lib/rules/jsx-max-depth');
12+
const RuleTester = require('eslint').RuleTester;
13+
14+
const parserOptions = {
15+
sourceType: 'module',
16+
ecmaFeatures: {
17+
jsx: true
18+
}
19+
};
20+
21+
// ------------------------------------------------------------------------------
22+
// Tests
23+
// ------------------------------------------------------------------------------
24+
25+
const ruleTester = new RuleTester({parserOptions});
26+
ruleTester.run('jsx-max-depth', rule, {
27+
valid: [{
28+
code: [
29+
'<App />'
30+
].join('\n')
31+
}, {
32+
code: [
33+
'<App>',
34+
' <foo />',
35+
'</App>'
36+
].join('\n'),
37+
options: [{max: 1}]
38+
}, {
39+
code: [
40+
'<App>',
41+
' <foo>',
42+
' <bar />',
43+
' </foo>',
44+
'</App>'
45+
].join('\n')
46+
}, {
47+
code: [
48+
'<App>',
49+
' <foo>',
50+
' <bar />',
51+
' </foo>',
52+
'</App>'
53+
].join('\n'),
54+
options: [{max: 2}]
55+
}, {
56+
code: [
57+
'const x = <div><em>x</em></div>;',
58+
'<div>{x}</div>'
59+
].join('\n'),
60+
options: [{max: 2}]
61+
}, {
62+
code: 'const foo = (x) => <div><em>{x}</em></div>;',
63+
options: [{max: 2}]
64+
}],
65+
66+
invalid: [{
67+
code: [
68+
'<App>',
69+
' <foo />',
70+
'</App>'
71+
].join('\n'),
72+
options: [{max: 0}],
73+
errors: [{message: 'Expected the depth of nested jsx elements to be <= 0, but found 1.'}]
74+
}, {
75+
code: [
76+
'<App>',
77+
' <foo>{bar}</foo>',
78+
'</App>'
79+
].join('\n'),
80+
options: [{max: 0}],
81+
errors: [{message: 'Expected the depth of nested jsx elements to be <= 0, but found 1.'}]
82+
}, {
83+
code: [
84+
'<App>',
85+
' <foo>',
86+
' <bar />',
87+
' </foo>',
88+
'</App>'
89+
].join('\n'),
90+
options: [{max: 1}],
91+
errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}]
92+
}, {
93+
code: [
94+
'const x = <div><span /></div>;',
95+
'<div>{x}</div>'
96+
].join('\n'),
97+
options: [{max: 1}],
98+
errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}]
99+
}, {
100+
code: [
101+
'const x = <div><span /></div>;',
102+
'let y = x;',
103+
'<div>{y}</div>'
104+
].join('\n'),
105+
options: [{max: 1}],
106+
errors: [{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}]
107+
}, {
108+
code: [
109+
'const x = <div><span /></div>;',
110+
'let y = x;',
111+
'<div>{x}-{y}</div>'
112+
].join('\n'),
113+
options: [{max: 1}],
114+
errors: [
115+
{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'},
116+
{message: 'Expected the depth of nested jsx elements to be <= 1, but found 2.'}
117+
]
118+
}, {
119+
code: [
120+
'<div>',
121+
'{<div><div><span /></div></div>}',
122+
'</div>'
123+
].join('\n'),
124+
errors: [{message: 'Expected the depth of nested jsx elements to be <= 2, but found 3.'}]
125+
}]
126+
});

0 commit comments

Comments
 (0)