diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts index c2e8b15e9..7e741c0f8 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts @@ -118,6 +118,34 @@ ruleTester.run(RULE_NAME, rule, { code: /* tsx */ `<>{moo}`, errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }], }, + { + code: /* tsx */ `<>{moo}`, + errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }], + options: [{ allowExpressions: false }], + }, + { + code: /* tsx */ `<>{moo}`, + errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }], + options: [{ allowExpressions: false }], + }, + { + code: /* tsx */ `<>{moo}`, + errors: [{ type: AST_NODE_TYPES.JSXElement, messageId: "noUselessFragment" }, { + type: AST_NODE_TYPES.JSXFragment, + messageId: "noUselessFragment", + }], + options: [{ allowExpressions: false }], + }, + { + code: /* tsx */ `baz}/>`, + errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }], + options: [{ allowExpressions: false }], + }, + { + code: /* tsx */ `<>`, + errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }], + options: [{ allowExpressions: false }], + }, ], valid: [ ...allValid, @@ -178,5 +206,13 @@ ruleTester.run(RULE_NAME, rule, { }, }, }, + { + code: /* tsx */ `{foo}`, + options: [{ allowExpressions: false }], + }, + { + code: /* tsx */ `} />`, + options: [{ allowExpressions: false }], + }, ], }); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts index b55e2ebf7..2f3a43ec0 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts @@ -18,6 +18,7 @@ export type MessageID = function check( node: TSESTree.JSXElement | TSESTree.JSXFragment, context: RuleContext, + allowExpressions: boolean, ) { const initialScope = context.sourceCode.getScope(node); if (JSX.isKeyedElement(node, initialScope)) return; @@ -26,7 +27,15 @@ function check( const isChildren = AST.isOneOf([AST_NODE_TYPES.JSXElement, AST_NODE_TYPES.JSXFragment])(node.parent); const [firstChildren] = node.children; // ee eeee eeee ...} /> - if (node.children.length === 1 && JSX.isLiteral(firstChildren) && !isChildren) return; + if (allowExpressions && node.children.length === 1 && JSX.isLiteral(firstChildren) && !isChildren) return; + if (!allowExpressions && isChildren) { + // <>hello, world + return context.report({ messageId: "noUselessFragment", node }); + } else if (!allowExpressions && !isChildren && node.children.length === 1) { + // const foo = <>{children}; + // return <>{children}; + return context.report({ messageId: "noUselessFragment", node }); + } const nonPaddingChildren = node.children.filter((child) => !JSX.isPaddingSpaces(child)); if (nonPaddingChildren.length > 1) return; if (nonPaddingChildren.length === 0) return context.report({ messageId: "noUselessFragment", node }); @@ -37,7 +46,13 @@ function check( context.report({ messageId: "noUselessFragment", node }); } -export default createRule<[], MessageID>({ +type Options = [ + { + allowExpressions: boolean; + }, +]; + +export default createRule({ meta: { type: "problem", docs: { @@ -47,19 +62,30 @@ export default createRule<[], MessageID>({ noUselessFragment: "A fragment contains less than two children is unnecessary.", noUselessFragmentInBuiltIn: "A fragment placed inside a built-in component is unnecessary.", }, - schema: [], + schema: [{ + type: "object", + additionalProperties: false, + properties: { + allowExpressions: { + type: "boolean", + }, + }, + }], }, name: RULE_NAME, - create(context) { + create(context, [option]) { + const { allowExpressions = true } = option; return { JSXElement(node) { if (!isFragmentElement(node, context)) return; - check(node, context); + check(node, context, allowExpressions); }, JSXFragment(node) { - check(node, context); + check(node, context, allowExpressions); }, }; }, - defaultOptions: [], + defaultOptions: [{ + allowExpressions: true, + }], }); diff --git a/website/pages/docs/rules/no-useless-fragment.mdx b/website/pages/docs/rules/no-useless-fragment.mdx index 5a5ffdc3c..f4be93f2c 100644 --- a/website/pages/docs/rules/no-useless-fragment.mdx +++ b/website/pages/docs/rules/no-useless-fragment.mdx @@ -67,10 +67,10 @@ const cat = <>meow ## Note -[This rule always allows single expressions in a fragment](https://github.com/Rel1cx/eslint-react/pull/188). This is useful in +[By default, this rule always allows single expressions in a fragment](https://github.com/Rel1cx/eslint-react/pull/188). This is useful in places like Typescript where `string` does not satisfy the expected return type of `JSX.Element`. A common workaround is to wrap the variable holding a string -in a fragment and expression. +in a fragment and expression. To change this behaviour, use the `allowExpressions` option. ### Examples of correct code for single expressions in fragments: @@ -80,6 +80,59 @@ in a fragment and expression. {foo} ``` +## Examples with `allowExpressions: false` + +### Failing + +```tsx +<> + +

<>foo

+ +<> + +baz} /> + +
+ <> +
+
+ +
+ +const cat = <>meow + +<>{children} + +<>{props.children} + +<> {foo} + + + <> +
+
+ + +``` + +### Passing + +```tsx +{foo} + + + +<> + + + + +<>foo {bar} + +{item.value} +``` + ## Further Reading - [React: Fragment](https://react.dev/reference/react/Fragment)