From 89cc6179c706b06d70cf19bc6008ac3a089e5355 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 5 Jun 2017 18:26:33 +0800 Subject: [PATCH] Add new rule: jsx-max-depth, fix #1219 --- docs/rules/jsx-max-depth.md | 84 ++++++++++++++++++ index.js | 1 + lib/rules/jsx-max-depth.js | 146 +++++++++++++++++++++++++++++++ tests/lib/rules/jsx-max-depth.js | 127 +++++++++++++++++++++++++++ 4 files changed, 358 insertions(+) create mode 100644 docs/rules/jsx-max-depth.md create mode 100644 lib/rules/jsx-max-depth.js create mode 100644 tests/lib/rules/jsx-max-depth.js diff --git a/docs/rules/jsx-max-depth.md b/docs/rules/jsx-max-depth.md new file mode 100644 index 0000000000..6440c0d445 --- /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 + + + + + + + + +``` + +## Rule Options + +It takes an option as the second parameter which can be a positive number for depth count. + +```js +... +"react/jsx-no-depth": [, { "max": }] +... +``` + +The following patterns are considered warnings: + +```jsx +// [2, { "max": 2 }] + + + + + + +// [2, { "max": 2 }] +const foobar = ; + + {foobar} + + +// [2, { "max": 3 }] + + + + + + + +``` + +The following patterns are not warnings: + +```jsx + +// [2, { "max": 2 }] + + + + +// [2,{ "max": 3 }] + + + + + + +// [2, { "max": 4 }] + + + + + + + +``` + +## 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 d9c1df21d1..1cd39b6c8a 100644 --- a/index.js +++ b/index.js @@ -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'), diff --git a/lib/rules/jsx-max-depth.js b/lib/rules/jsx-max-depth.js new file mode 100644 index 0000000000..1a537194db --- /dev/null +++ b/lib/rules/jsx-max-depth.js @@ -0,0 +1,146 @@ +/** + * @fileoverview Validate JSX maximum depth + * @author Chris + */ +'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); + } + } + }; + } +}; diff --git a/tests/lib/rules/jsx-max-depth.js b/tests/lib/rules/jsx-max-depth.js new file mode 100644 index 0000000000..a36a03aaad --- /dev/null +++ b/tests/lib/rules/jsx-max-depth.js @@ -0,0 +1,127 @@ +/** + * @fileoverview Validate JSX maximum depth + * @author Chris + */ +'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: [ + '' + ].join('\n') + }, { + code: [ + '', + ' ', + '' + ].join('\n'), + options: [{max: 2}] + }, { + code: [ + '', + ' ', + ' ', + ' ', + '' + ].join('\n'), + options: [{}] + }, { + code: [ + '', + ' ', + ' ', + ' ', + '' + ].join('\n'), + options: [{max: 3}] + }, { + code: [ + 'const x =
x
;', + '
{x}
' + ].join('\n'), + options: [{max: 3}] + }, { + code: 'const foo = (x) =>
{x}
;', + options: [{max: 3}] + }], + + invalid: [{ + code: [ + '', + ' ', + '' + ].join('\n'), + options: [{max: 1}], + errors: [{message: 'Expected the depth of JSX Elements nested should be 1 but found 2.'}] + }, { + code: [ + '', + ' {bar}', + '' + ].join('\n'), + options: [{max: 1}], + errors: [{message: 'Expected the depth of JSX Elements nested should be 1 but found 2.'}] + }, { + code: [ + '', + ' ', + ' ', + ' ', + '' + ].join('\n'), + options: [{max: 2}], + errors: [{message: 'Expected the depth of JSX Elements nested should be 2 but found 3.'}] + }, { + code: [ + 'const x =
;', + '
{x}
' + ].join('\n'), + options: [{max: 2}], + errors: [{message: 'Expected the depth of JSX Elements nested should be 2 but found 3.'}] + }, { + code: [ + 'const x =
;', + 'let y = x;', + '
{y}
' + ].join('\n'), + options: [{max: 2}], + errors: [{message: 'Expected the depth of JSX Elements nested should be 2 but found 3.'}] + }, { + code: [ + 'const x =
;', + 'let y = x;', + '
{x}-{y}
' + ].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: [ + '
', + '{
}', + '
' + ].join('\n'), + errors: [{message: 'Expected the depth of JSX Elements nested should be 3 but found 4.'}] + }] +});