Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new rule: jsx-max-depth, fix #1219
- Loading branch information
Showing
4 changed files
with
358 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: 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); | ||
} | ||
} | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.'}] | ||
}] | ||
}); |