add noUselessFragment rule and tests
MODIFY
packages/@romejs/compiler/lint/rules/index.ts
packages/@romejs/compiler/lint/rules/index.ts
@@ -90,6 +90,7 @@ import noFindDOMNode from "./react/noFindDOMNode";
import noRedundantShouldComponentUpdate from "./react/noRedundantShouldComponentUpdate";
import noStringRefs from "./react/noStringRefs";
import noUnsafe from "./react/noUnsafe";
+import noUselessFragment from "./react/noUselessFragment";
import noWillUpdateSetState from "./react/noWillUpdateSetState";
import reactInJsxScope from "./react/reactInJsxScope";
import stylePropObject from "./react/stylePropObject";
@@ -181,6 +182,7 @@ export const lintTransforms = [
noRedundantShouldComponentUpdate,
noStringRefs,
noUnsafe,
+ noUselessFragment,
noWillUpdateSetState,
reactInJsxScope,
stylePropObject,

MODIFY
packages/@romejs/compiler/lint/rules/react/jsxFragments.test.ts
packages/@romejs/compiler/lint/rules/react/jsxFragments.test.ts
import {test} from "rome";
-import {testLintMultiple} from "../testHelpers";
+import {testLint} from "../testHelpers";
test(
"react jsx fragments",
async (t) => {
- await testLintMultiple(
+ await testLint(
t,
- [
- // INVALID
- "<Fragment></Fragment>",
- "<React.Fragment></React.Fragment>",
- "<Fragment><Foo /></Fragment>",
- "<React.Fragment><Foo /></React.Fragment>",
- "const Hello = <div><Fragment><Foo/></Fragment></div>",
- "const Hello = <div><React.Fragment><Foo/></React.Fragment></div>",
- "const Hello = <React.Fragment><Foo/></React.Fragment>",
- "const Hello = <Fragment><Foo/></Fragment>",
+ {
+ invalid: [
+ "<Fragment><Foo /><Foo /></Fragment>",
+ "<React.Fragment><Foo /><Foo /></React.Fragment>",
+ "const Hello = <div><Fragment><Foo /><Foo /></Fragment></div>",
+ "const Hello = <div><React.Fragment><Foo /><Foo /></React.Fragment></div>",
+ "const Hello = <React.Fragment><Foo /><Foo /></React.Fragment>",
+ "const Hello = <Fragment><Foo /><Foo /></Fragment>",
`
function Foo() {
- let bar = <React.Fragment><Foo /></React.Fragment>;
+ let bar = <React.Fragment><Foo /><Foo /></React.Fragment>;
return bar;
}
`,
`
function Foo() {
- let bar = <Fragment><Foo/></Fragment>;
+ let bar = <Fragment><Foo /><Foo /></Fragment>;
return bar;
}
`,
`
function Hello() {
- return <React.Fragment><Foo /></React.Fragment>
+ return <React.Fragment><Foo /><Foo /></React.Fragment>
}
`,
`function Hello() {
- return <Fragment><Foo /></Fragment>
+ return <Fragment><Foo /><Foo /></Fragment>
}
`,
- "const Hello = () => <React.Fragment></React.Fragment>",
- "const Hello = () => <Fragment></Fragment>",
- // VALID
- "<></>",
- "<><Foo /></>",
+ "const Hello = () => <React.Fragment><Foo /><Foo /></React.Fragment>",
+ "const Hello = () => <Fragment><Foo /><Foo /></Fragment>",
+ ],
+ valid: [
+ "<><Foo /><Foo /></>",
+
"<Fragment key='id'></Fragment>",
"<React.Fragment key='id'><Foo /></React.Fragment>",
"const Hello = <div><Fragment key='word'><Foo /></Fragment></div>",
@@ -59,7 +58,8 @@ test(
return <React.Fragment key='id'><Foo /></React.Fragment>
}
`,
- `function Hello() {
+ `
+ function Hello() {
return <Fragment key='id'><Foo /></Fragment>
}
`,
@@ -67,6 +67,7 @@ test(
"const Hello = () => <React.Fragment key='id'></React.Fragment>",
"const Hello = () => <Fragment key='id'></Fragment>",
],
+ },
{category: "lint/react/jsxFragments"},
);
},

ADD
packages/@romejs/compiler/lint/rules/react/noUselessFragment.test.ts
+import {test} from "rome";
+import {testLint} from "../testHelpers";
+
+test(
+ "react no useless fragment",
+ async (t) => {
+ await testLint(
+ t,
+ {
+ invalid: [
+ "<>{foo}</>",
+ "<p><>foo</></p>",
+ "<></>",
+ "<React.Fragment>foo</React.Fragment>",
+ "<Fragment>foo</Fragment>",
+ `<section>
+ <>
+ <div />
+ <div />
+ </>
+ </section>
+ `,
+ ],
+ valid: [
+ `
+ <>
+ <Foo />
+ <Bar />
+ </>
+ `,
+ "<>foo {bar}</>",
+ "<> {foo}</>",
+ "const cat = <>meow</>",
+ "const cat = () => <>meow</>",
+ `
+ function cat() {
+ return <>meow</>;
+ }
+ `,
+ `
+ function cat() {
+ const foo = <>meow</>;
+ return foo;
+ }
+ `,
+ `
+ <SomeComponent>
+ <>
+ <div />
+ <div />
+ </>
+ </SomeComponent>
+ `,
+ "<Fragment key={item.id}>{item.value}</Fragment>",
+ ],
+ },
+ {category: "lint/react/noUselessFragment"},
+ );
+ },
+);

ADD
packages/@romejs/compiler/lint/rules/react/noUselessFragment.ts
+import {Path, TransformExitResult} from "@romejs/compiler";
+import {descriptions} from "@romejs/diagnostics";
+import {
+ doesNodeMatchPattern,
+ hasJSXAttribute,
+ isFunctionNode,
+ isJSXElement,
+} from "@romejs/js-ast-utils";
+import {AnyNode, JSXElement, JSXFragment} from "@romejs/ast";
+import {REDUCE_REMOVE} from "@romejs/compiler/constants";
+
+function isChildOfHtmlElement(path: Path): boolean {
+ const parentNode = path.parent;
+ return (
+ parentNode.type === "JSXElement" &&
+ parentNode.name.type === "JSXIdentifier" &&
+ /^[a-z]+$/.test(parentNode.name.name)
+ );
+}
+
+function isFragment(node: JSXFragment | JSXElement): boolean {
+ return (
+ node.type === "JSXFragment" ||
+ (isJSXElement(node) &&
+ (doesNodeMatchPattern(node.name, "Fragment") ||
+ doesNodeMatchPattern(node.name, "React.Fragment")))
+ );
+}
+
+function hasLessThanTwoChildren(node: JSXFragment | JSXElement): boolean {
+ return node.children.length < 2;
+}
+
+function getChildrenNode(
+ node: JSXFragment | JSXElement,
+): AnyNode | typeof REDUCE_REMOVE | Array<AnyNode> {
+ if (node.children.length === 0) {
+ return REDUCE_REMOVE;
+ }
+ if (node.children.length === 1) {
+ return node.children[0];
+ }
+ return node.children;
+}
+
+export default {
+ name: "noUselessFragment",
+ enter(path: Path): TransformExitResult {
+ const {node, context} = path;
+
+ if (node.type !== "JSXFragment" && !isJSXElement(node)) {
+ return node;
+ }
+
+ if (
+ path.parent.type !== "JSReturnStatement" &&
+ path.parent.type !== "JSVariableDeclarator" &&
+ !isFunctionNode(path.parent) &&
+ isFragment(node) &&
+ !(isJSXElement(node) && hasJSXAttribute(node, "key")) &&
+ (hasLessThanTwoChildren(node) || isChildOfHtmlElement(path))
+ ) {
+ return context.addFixableDiagnostic(
+ {
+ old: node,
+ fixed: getChildrenNode(node),
+ },
+ descriptions.LINT.REACT_NO_USELESS_FRAGMENT,
+ );
+ }
+
+ return node;
+ },
+};

MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -176,6 +176,7 @@ type LintDiagnosticCategory =
| "lint/react/noRedundantShouldComponentUpdate"
| "lint/react/noStringRefs"
| "lint/react/noUnsafe"
+ | "lint/react/noUselessFragment"
| "lint/react/noWillUpdateSetState"
| "lint/react/reactInJsxScope"
| "lint/react/stylePropObject"

MODIFY
packages/@romejs/diagnostics/descriptions/lint.ts
packages/@romejs/diagnostics/descriptions/lint.ts
@@ -16,6 +16,17 @@ import {buildSuggestionAdvice} from "../helpers";
import {createDiagnosticsCategory, orJoin} from "./index";
export const lint = createDiagnosticsCategory({
+ REACT_NO_USELESS_FRAGMENT: {
+ category: "lint/react/noUselessFragment",
+ message: "Avoid using unnecessary <emphasis>Fragment</emphasis>.",
+ advice: [
+ {
+ type: "log",
+ category: "info",
+ text: "A <emphasis>Fragment</emphasis> is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment.",
+ },
+ ],
+ },
JSX_A11Y_ANCHOR_IS_VALID: (message: string) => ({
category: "lint/jsx-a11y/anchorIsValid",
message,

add noUselessFragment rule and tests (#541)