From e0f2872da70284c8afcdfff2a6ff3247535871e3 Mon Sep 17 00:00:00 2001 From: Simon Schick Date: Tue, 2 Apr 2024 20:47:46 -0700 Subject: [PATCH] [New] add `jsx-props-no-spread-multi` --- README.md | 1 + docs/rules/jsx-props-no-spread-multi.md | 25 +++++++ lib/rules/index.js | 1 + lib/rules/jsx-props-no-spread-multi.js | 63 ++++++++++++++++ lib/types.d.ts | 3 +- tests/lib/rules/jsx-props-no-spread-multi.js | 78 ++++++++++++++++++++ 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 docs/rules/jsx-props-no-spread-multi.md create mode 100644 lib/rules/jsx-props-no-spread-multi.js create mode 100644 tests/lib/rules/jsx-props-no-spread-multi.js diff --git a/README.md b/README.md index 64e57912a1..a96c5445cc 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,7 @@ module.exports = [ | [jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md) | Require one JSX element per line | | | 🔧 | | | | [jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components | | | | | | | [jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props | | | 🔧 | | | +| [jsx-props-no-spread-multi](docs/rules/jsx-props-no-spread-multi.md) | Disallow JSX prop spreading the same expression multiple times | | | | | | | [jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Disallow JSX prop spreading | | | | | | | [jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting | | | | | ❌ | | [jsx-sort-props](docs/rules/jsx-sort-props.md) | Enforce props alphabetical sorting | | | 🔧 | | | diff --git a/docs/rules/jsx-props-no-spread-multi.md b/docs/rules/jsx-props-no-spread-multi.md new file mode 100644 index 0000000000..eec439afa1 --- /dev/null +++ b/docs/rules/jsx-props-no-spread-multi.md @@ -0,0 +1,25 @@ +# Disallow JSX prop spreading the same expression multiple times (`react/jsx-props-no-spread-multi`) + + + +Enforces that any unique express is only spread once. Generally spreading the same expression twice is an indicator of a mistake since any attribute between the spreads may be overridden when +the intent was not to. Even when that is not the case this will lead to unnecessary computations to be performed. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```jsx + +``` + +Examples of **correct** code for this rule: + +```jsx + + +``` + +## When Not To Use It + +When spreading the same expression yields different values. diff --git a/lib/rules/index.js b/lib/rules/index.js index 784831bba7..f1e2b43930 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -50,6 +50,7 @@ module.exports = { 'jsx-fragments': require('./jsx-fragments'), 'jsx-props-no-multi-spaces': require('./jsx-props-no-multi-spaces'), 'jsx-props-no-spreading': require('./jsx-props-no-spreading'), + 'jsx-props-no-spread-multi': require('./jsx-props-no-spread-multi'), 'jsx-sort-default-props': require('./jsx-sort-default-props'), 'jsx-sort-props': require('./jsx-sort-props'), 'jsx-space-before-closing': require('./jsx-space-before-closing'), diff --git a/lib/rules/jsx-props-no-spread-multi.js b/lib/rules/jsx-props-no-spread-multi.js new file mode 100644 index 0000000000..b79e296927 --- /dev/null +++ b/lib/rules/jsx-props-no-spread-multi.js @@ -0,0 +1,63 @@ +/** + * @fileoverview Prevent JSX prop spreading the same expression multiple times + * @author Simon Schick + */ + +'use strict'; + +const docsUrl = require('../util/docsUrl'); +const report = require('../util/report'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const messages = { + noMultiSpreading: 'Spreading the same expression multiple times is forbidden', +}; + +const ignoredAstProperties = new Set(['parent', 'range', 'loc', 'start', 'end', '_babelType']); + +/** + * Filter for JSON.stringify that omits circular and position structures. + * + * @param {string} key + * @param {*} value + * @returns {*} + */ +const propertyFilter = (key, value) => (ignoredAstProperties.has(key) ? undefined : value); + +module.exports = { + meta: { + docs: { + description: 'Disallow JSX prop spreading the same expression multiple times', + category: 'Best Practices', + recommended: false, + url: docsUrl('jsx-props-no-spread-multi'), + }, + messages, + }, + + create(context) { + return { + JSXOpeningElement(node) { + const spreads = node.attributes.filter((attr) => attr.type === 'JSXSpreadAttribute'); + if (spreads.length < 2) { + return; + } + // We detect duplicate expressions by hashing the ast nodes + const argumentHashes = new Set(); + for (const spread of spreads) { + // TODO: Deep compare ast function? + const hash = JSON.stringify(spread.argument, propertyFilter); + if (argumentHashes.has(hash)) { + report(context, messages.noMultiSpreading, 'noMultiSpreading', { + node: spread, + }); + } + argumentHashes.add(hash); + } + }, + }; + }, +}; diff --git a/lib/types.d.ts b/lib/types.d.ts index e13e204524..30df9c02c1 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -11,9 +11,10 @@ declare global { type JSXAttribute = ASTNode; type JSXElement = ASTNode; type JSXFragment = ASTNode; + type JSXOpeningElement = ASTNode; type JSXSpreadAttribute = ASTNode; - type Context = eslint.Rule.RuleContext + type Context = eslint.Rule.RuleContext; type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set) => object; diff --git a/tests/lib/rules/jsx-props-no-spread-multi.js b/tests/lib/rules/jsx-props-no-spread-multi.js new file mode 100644 index 0000000000..f4cba254f1 --- /dev/null +++ b/tests/lib/rules/jsx-props-no-spread-multi.js @@ -0,0 +1,78 @@ +/** + * @fileoverview Tests for jsx-props-no-spread-multi + */ + +'use strict'; + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/jsx-props-no-spread-multi'); + +const parsers = require('../../helpers/parsers'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const ruleTester = new RuleTester({ parserOptions }); +const expectedError = { messageId: 'noMultiSpreading' }; + +ruleTester.run('jsx-props-no-spread-multi', rule, { + valid: parsers.all([ + { + code: ` + const a = {}; + + `, + }, + { + code: ` + const a = {}; + const b = {}; + + `, + }, + ]), + + invalid: parsers.all([ + { + code: ` + const props = {}; + + `, + errors: [expectedError], + }, + { + code: ` + const props = {}; +
+ `, + errors: [expectedError], + }, + { + code: ` + const props = {}; +
+ `, + errors: [expectedError, expectedError], + }, + { + code: ` + const func = () => ({}); +
+ `, + errors: [expectedError], + }, + ]), +});