add jsxFragments rule and tests
MODIFY
packages/@romejs/compiler/lint/rules/index.ts
packages/@romejs/compiler/lint/rules/index.ts
@@ -75,6 +75,7 @@ import noTargetBlank from "./jsx-a11y/noTargetBlank";
import scope from "./jsx-a11y/scope";
import tabindexNoPositive from "./jsx-a11y/tabindexNoPositive";
import buttonHasType from "./react/buttonHasType";
+import jsxFragments from "./react/jsxFragments";
import jsxKey from "./react/jsxKey";
import jsxNoCommentText from "./react/jsxNoCommentText";
import jsxNoDuplicateProps from "./react/jsxNoDuplicateProps";
@@ -163,6 +164,7 @@ export const lintTransforms = [
scope,
tabindexNoPositive,
buttonHasType,
+ jsxFragments,
jsxKey,
jsxNoCommentText,
jsxNoDuplicateProps,

ADD
packages/@romejs/compiler/lint/rules/react/jsxFragments.test.ts
+import {test} from "rome";
+import {testLintMultiple} from "../testHelpers";
+
+test(
+ "react jsx fragments",
+ async (t) => {
+ await testLintMultiple(
+ 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>",
+ `
+ function Foo() {
+ let bar = <React.Fragment><Foo /></React.Fragment>;
+ return bar;
+ }
+ `,
+ `
+ function Foo() {
+ let bar = <Fragment><Foo/></Fragment>;
+ return bar;
+ }
+ `,
+ `
+ function Hello() {
+ return <React.Fragment><Foo /></React.Fragment>
+ }
+ `,
+ `function Hello() {
+ return <Fragment><Foo /></Fragment>
+ }
+ `,
+ "const Hello = () => <React.Fragment></React.Fragment>",
+ "const Hello = () => <Fragment></Fragment>",
+ // VALID
+ "<></>",
+ "<><Foo /></>",
+ "<Fragment key='id'></Fragment>",
+ "<React.Fragment key='id'><Foo /></React.Fragment>",
+ "const Hello = <div><Fragment key='word'><Foo /></Fragment></div>",
+ "const Hello = <>hello</>",
+ "const Hello = <Fragment key='id'><Foo/></Fragment>",
+ "const Hello = <React.Fragment key='id'><Foo/></React.Fragment>",
+ `
+ function Foo() {
+ let bar = <React.Fragment key='word'></React.Fragment>;
+ return bar;
+ }
+ `,
+ `
+ function Hello() {
+ return <React.Fragment key='id'><Foo /></React.Fragment>
+ }
+ `,
+ `function Hello() {
+ return <Fragment key='id'><Foo /></Fragment>
+ }
+ `,
+ "const Hello = () => <></>",
+ "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/jsxFragments.ts
+import {Path, TransformExitResult} from "@romejs/compiler";
+import {descriptions} from "@romejs/diagnostics";
+import {jsxFragment} from "@romejs/ast";
+import {
+ doesNodeMatchPattern,
+ hasJSXAttribute,
+ isJSXElement,
+} from "@romejs/js-ast-utils";
+
+export default {
+ name: "reactJsxFragments",
+ enter(path: Path): TransformExitResult {
+ const {node, context} = path;
+
+ if (
+ isJSXElement(node) &&
+ (doesNodeMatchPattern(node.name, "Fragment") ||
+ doesNodeMatchPattern(node.name, "React.Fragment")) &&
+ !hasJSXAttribute(node, "key")
+ ) {
+ return context.addFixableDiagnostic(
+ {
+ old: node,
+ fixed: jsxFragment.create({
+ children: node.children,
+ }),
+ },
+ descriptions.LINT.REACT_JSX_FRAGMENTS,
+ );
+ }
+
+ return node;
+ },
+};

MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -161,6 +161,7 @@ type LintDiagnosticCategory =
| "lint/jsx-a11y/scope"
| "lint/jsx-a11y/tabindexNoPositive"
| "lint/react/buttonHasType"
+ | "lint/react/jsxFragments"
| "lint/react/jsxKey"
| "lint/react/jsxNoCommentText"
| "lint/react/jsxNoDuplicateProps"

MODIFY
packages/@romejs/diagnostics/descriptions/lint.ts
packages/@romejs/diagnostics/descriptions/lint.ts
@@ -46,6 +46,17 @@ export const lint = createDiagnosticsCategory({
},
],
}),
+ REACT_JSX_FRAGMENTS: {
+ category: "lint/react/jsxFragments",
+ message: "Use shorthand syntax for <emphasis>Fragment</emphasis> elements instead of standard syntax.",
+ advice: [
+ {
+ type: "log",
+ category: "info",
+ text: "Shorthand fragment syntax saves keystrokes and is only unapplicable when keys are required.",
+ },
+ ],
+ },
REACT_NO_REDUNDANT_SHOULD_COMPONENT_UPDATE: {
category: "lint/react/noRedundantShouldComponentUpdate",
message: "Do not implement <emphasis>shouldComponentUpdate</emphasis> when extending <emphasis>React.PureComponent</emphasis>.",

MODIFY
packages/@romejs/js-ast-utils/doesNodeMatchPattern.ts
packages/@romejs/js-ast-utils/doesNodeMatchPattern.ts
@@ -46,7 +46,11 @@ export default function doesNodeMatchPattern(
}
// Not a member expression
- if (node.type !== "JSMemberExpression" && !isIdentifierish(node)) {
+ if (
+ node.type !== "JSMemberExpression" &&
+ node.type !== "JSXMemberExpression" &&
+ !isIdentifierish(node)
+ ) {
return false;
}

MODIFY
packages/@romejs/js-ast-utils/getNodeReferenceParts.ts
packages/@romejs/js-ast-utils/getNodeReferenceParts.ts
@@ -51,7 +51,10 @@ export default function getNodeReferenceParts(node: undefined | AnyNode): Result
parts.push({node, value: node.meta.name});
parts.push({node, value: node.property.name});
return false;
- } else if (node.type === "JSMemberExpression") {
+ } else if (
+ node.type === "JSMemberExpression" ||
+ node.type === "JSXMemberExpression"
+ ) {
const stop = add(node.object);
if (stop) {
return true;

add jsxFragments rule and tests (#511)