From c5ab00fb1d871c611be9c8793ae4ee757d71da47 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Wed, 1 Nov 2023 14:56:24 +0800 Subject: [PATCH 1/4] feat: add no-useless-fragment --- README.md | 77 +++--- packages/eslint-plugin-jsx/src/index.ts | 2 + .../src/rules/no-useless-fragment.md | 83 +++++++ .../src/rules/no-useless-fragment.spec.ts | 222 ++++++++++++++++++ .../src/rules/no-useless-fragment.ts | 160 +++++++++++++ packages/jsx/docs/README.md | 179 ++++++++++++++ packages/jsx/src/attribute.ts | 12 + packages/jsx/src/children.ts | 16 +- packages/jsx/src/element/index.ts | 1 + packages/jsx/src/element/is-keyed-element.ts | 9 + packages/jsx/src/fragment.ts | 61 +++++ packages/jsx/src/index.ts | 2 + packages/jsx/src/textnode.ts | 11 + 13 files changed, 797 insertions(+), 38 deletions(-) create mode 100644 packages/eslint-plugin-jsx/src/rules/no-useless-fragment.md create mode 100644 packages/eslint-plugin-jsx/src/rules/no-useless-fragment.spec.ts create mode 100644 packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts create mode 100644 packages/jsx/src/attribute.ts create mode 100644 packages/jsx/src/element/is-keyed-element.ts create mode 100644 packages/jsx/src/fragment.ts diff --git a/README.md b/README.md index d2a7c78f4..ee74cc12a 100644 --- a/README.md +++ b/README.md @@ -88,56 +88,59 @@ export default [ +🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). + ### debug -| Name | Description | -| :--------------------------------------------------------------------------------------- | :-------------------------------------------------------- | -| [debug/class-component](packages/eslint-plugin-debug/src/rules/class-component.md) | reports all class components, including anonymous ones | -| [debug/function-component](packages/eslint-plugin-debug/src/rules/function-component.md) | reports all function components, including anonymous ones | -| [debug/hooks](packages/eslint-plugin-debug/src/rules/hooks.md) | reports all react hooks | +| Name | Description | 🔧 | +| :--------------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | +| [debug/class-component](packages/eslint-plugin-debug/src/rules/class-component.md) | reports all class components, including anonymous ones | | +| [debug/function-component](packages/eslint-plugin-debug/src/rules/function-component.md) | reports all function components, including anonymous ones | | +| [debug/hooks](packages/eslint-plugin-debug/src/rules/hooks.md) | reports all react hooks | | ### hooks -| Name | Description | -| :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- | -| [hooks/ensure-custom-hooks-using-other-hooks](packages/eslint-plugin-hooks/src/rules/ensure-custom-hooks-using-other-hooks.md) | enforce custom hooks using other hooks | +| Name | Description | 🔧 | +| :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- | :- | +| [hooks/ensure-custom-hooks-using-other-hooks](packages/eslint-plugin-hooks/src/rules/ensure-custom-hooks-using-other-hooks.md) | enforce custom hooks using other hooks | | ### jsx -| Name                                | Description | -| :------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | -| [jsx/no-array-index-key](packages/eslint-plugin-jsx/src/rules/no-array-index-key.md) | disallow using Array index as key | -| [jsx/no-duplicate-key](packages/eslint-plugin-jsx/src/rules/no-duplicate-key.md) | disallow duplicate keys in `key` prop when rendering list | -| [jsx/no-leaked-conditional-rendering](packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.md) | disallow problematic leaked values from being rendered | -| [jsx/no-missing-key](packages/eslint-plugin-jsx/src/rules/no-missing-key.md) | require `key` prop when rendering list | -| [jsx/no-misused-comment-in-textnode](packages/eslint-plugin-jsx/src/rules/no-misused-comment-in-textnode.md) | disallow comments from being inserted as text nodes | -| [jsx/no-script-url](packages/eslint-plugin-jsx/src/rules/no-script-url.md) | disallow `javascript:` URLs as JSX event handler prop's value | -| [jsx/prefer-shorthand-boolean](packages/eslint-plugin-jsx/src/rules/prefer-shorthand-boolean.md) | enforce boolean attributes notation in JSX | +| Name                                | Description | 🔧 | +| :------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | :- | +| [jsx/no-array-index-key](packages/eslint-plugin-jsx/src/rules/no-array-index-key.md) | disallow using Array index as key | | +| [jsx/no-duplicate-key](packages/eslint-plugin-jsx/src/rules/no-duplicate-key.md) | disallow duplicate keys in `key` prop when rendering list | | +| [jsx/no-leaked-conditional-rendering](packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.md) | disallow problematic leaked values from being rendered | | +| [jsx/no-missing-key](packages/eslint-plugin-jsx/src/rules/no-missing-key.md) | require `key` prop when rendering list | | +| [jsx/no-misused-comment-in-textnode](packages/eslint-plugin-jsx/src/rules/no-misused-comment-in-textnode.md) | disallow comments from being inserted as text nodes | | +| [jsx/no-script-url](packages/eslint-plugin-jsx/src/rules/no-script-url.md) | disallow `javascript:` URLs as JSX event handler prop's value | | +| [jsx/no-useless-fragment](packages/eslint-plugin-jsx/src/rules/no-useless-fragment.md) | disallow unnecessary fragments | 🔧 | +| [jsx/prefer-shorthand-boolean](packages/eslint-plugin-jsx/src/rules/prefer-shorthand-boolean.md) | enforce boolean attributes notation in JSX | | ### naming-convention -| Name | Description | -| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------ | -| [naming-convention/filename](packages/eslint-plugin-naming-convention/src/rules/filename.md) | enforce naming convention for JSX file names | -| [naming-convention/filename-extension](packages/eslint-plugin-naming-convention/src/rules/filename-extension.md) | enforce naming convention for JSX file extensions | +| Name | Description | 🔧 | +| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------ | :- | +| [naming-convention/filename](packages/eslint-plugin-naming-convention/src/rules/filename.md) | enforce naming convention for JSX file names | | +| [naming-convention/filename-extension](packages/eslint-plugin-naming-convention/src/rules/filename-extension.md) | enforce naming convention for JSX file extensions | | ### react -| Name                                             | Description | -| :--------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ | -| [react/no-children-in-void-dom-elements](packages/eslint-plugin-react/src/rules/no-children-in-void-dom-elements.md) | disallow passing children to void DOM elements | -| [react/no-class-component](packages/eslint-plugin-react/src/rules/no-class-component.md) | enforce that there are no class components | -| [react/no-clone-element](packages/eslint-plugin-react/src/rules/no-clone-element.md) | disallow `cloneElement` | -| [react/no-constructed-context-value](packages/eslint-plugin-react/src/rules/no-constructed-context-value.md) | disallow passing constructed values to context providers | -| [react/no-create-ref](packages/eslint-plugin-react/src/rules/no-create-ref.md) | disallow `createRef` in function components | -| [react/no-dangerously-set-innerhtml](packages/eslint-plugin-react/src/rules/no-dangerously-set-innerhtml.md) | disallow when a DOM element is using both children and dangerouslySetInnerHTML' | -| [react/no-dangerously-set-innerhtml-with-children](packages/eslint-plugin-react/src/rules/no-dangerously-set-innerhtml-with-children.md) | disallow when a DOM element is using both children and dangerouslySetInnerHTML' | -| [react/no-namespace](packages/eslint-plugin-react/src/rules/no-namespace.md) | enforce that namespaces are not used in React elements | -| [react/no-string-refs](packages/eslint-plugin-react/src/rules/no-string-refs.md) | disallow using deprecated string refs | -| [react/no-string-style-props](packages/eslint-plugin-react/src/rules/no-string-style-props.md) | disallow using string as style props value | -| [react/no-unstable-default-props](packages/eslint-plugin-react/src/rules/no-unstable-default-props.md) | disallow usage of unstable value as default param in function component | -| [react/no-unstable-nested-components](packages/eslint-plugin-react/src/rules/no-unstable-nested-components.md) | disallow usage of unstable nested components | -| [react/prefer-destructuring-assignment](packages/eslint-plugin-react/src/rules/prefer-destructuring-assignment.md) | enforce using destructuring assignment in component props and context | +| Name                                             | Description | 🔧 | +| :--------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ | :- | +| [react/no-children-in-void-dom-elements](packages/eslint-plugin-react/src/rules/no-children-in-void-dom-elements.md) | disallow passing children to void DOM elements | | +| [react/no-class-component](packages/eslint-plugin-react/src/rules/no-class-component.md) | enforce that there are no class components | | +| [react/no-clone-element](packages/eslint-plugin-react/src/rules/no-clone-element.md) | disallow `cloneElement` | | +| [react/no-constructed-context-value](packages/eslint-plugin-react/src/rules/no-constructed-context-value.md) | disallow passing constructed values to context providers | | +| [react/no-create-ref](packages/eslint-plugin-react/src/rules/no-create-ref.md) | disallow `createRef` in function components | | +| [react/no-dangerously-set-innerhtml](packages/eslint-plugin-react/src/rules/no-dangerously-set-innerhtml.md) | disallow when a DOM element is using both children and dangerouslySetInnerHTML' | | +| [react/no-dangerously-set-innerhtml-with-children](packages/eslint-plugin-react/src/rules/no-dangerously-set-innerhtml-with-children.md) | disallow when a DOM element is using both children and dangerouslySetInnerHTML' | | +| [react/no-namespace](packages/eslint-plugin-react/src/rules/no-namespace.md) | enforce that namespaces are not used in React elements | | +| [react/no-string-refs](packages/eslint-plugin-react/src/rules/no-string-refs.md) | disallow using deprecated string refs | | +| [react/no-string-style-props](packages/eslint-plugin-react/src/rules/no-string-style-props.md) | disallow using string as style props value | | +| [react/no-unstable-default-props](packages/eslint-plugin-react/src/rules/no-unstable-default-props.md) | disallow usage of unstable value as default param in function component | | +| [react/no-unstable-nested-components](packages/eslint-plugin-react/src/rules/no-unstable-nested-components.md) | disallow usage of unstable nested components | | +| [react/prefer-destructuring-assignment](packages/eslint-plugin-react/src/rules/prefer-destructuring-assignment.md) | enforce using destructuring assignment in component props and context | | @@ -156,7 +159,7 @@ export default [ - [x] `jsx/no-script-url` - [ ] `jsx/no-target-blank` - [ ] `jsx/no-unknown-property` -- [ ] `jsx/no-useless-fragment` +- [x] `jsx/no-useless-fragment` - [ ] `jsx/prefer-fragment-syntax` - [x] `jsx/prefer-shorthand-boolean` - [x] `naming-convention/filename-extension` diff --git a/packages/eslint-plugin-jsx/src/index.ts b/packages/eslint-plugin-jsx/src/index.ts index 9ef8cfd51..ed64618ad 100644 --- a/packages/eslint-plugin-jsx/src/index.ts +++ b/packages/eslint-plugin-jsx/src/index.ts @@ -7,6 +7,7 @@ import jsxNoLeakedConditionalRendering from "./rules/no-leaked-conditional-rende import jsxNoMissingKey from "./rules/no-missing-key"; import jsxNoMisusedCommentInTextNode from "./rules/no-misused-comment-in-textnode"; import jsxNoScriptUrl from "./rules/no-script-url"; +import jsxNoUselessFragment from "./rules/no-useless-fragment"; import jsxPreferShorthandJsxBoolean from "./rules/prefer-shorthand-boolean"; export { name } from "../package.json"; @@ -18,5 +19,6 @@ export const rules = { "no-missing-key": jsxNoMissingKey, "no-misused-comment-in-textnode": jsxNoMisusedCommentInTextNode, "no-script-url": jsxNoScriptUrl, + "no-useless-fragment": jsxNoUselessFragment, "prefer-shorthand-boolean": jsxPreferShorthandJsxBoolean, } as const; diff --git a/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.md b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.md new file mode 100644 index 000000000..b09c41763 --- /dev/null +++ b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.md @@ -0,0 +1,83 @@ +# jsx/no-useless-fragment + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a [keyed fragment](https://react.dev/reference/react/Fragment#caveats). + +## Rule Details + +### ❌ Incorrect + +```tsx +<>{foo} + +<> + +

<>foo

+ +<> + +foo + +foo + +
+ <> +
+
+ +
+ +{showFullName ? <>{fullName} : <>{firstName}} +``` + +### ✅ Correct + +```tsx +{foo} + + + +<> + + + + +<>foo {bar} + +<> {foo} + +const cat = <>meow + + + <> +
+
+ + + +{item.value} + +{showFullName ? fullName : firstName} +``` + +## Rule Options + +### `allowExpressions` + +When `true` single expressions in a fragment will be allowed. 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. + +Examples of **correct** code for the rule, when `"allowExpressions"` is `true`: + +```jsx +<>{foo} + +<> + {foo} + +``` diff --git a/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.spec.ts b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.spec.ts new file mode 100644 index 000000000..be85646dc --- /dev/null +++ b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.spec.ts @@ -0,0 +1,222 @@ +import { allValid } from "@eslint-react/shared"; +import { AST_NODE_TYPES } from "@typescript-eslint/types"; + +import RuleTester, { getFixturesRootDir } from "../../../../test/rule-tester"; +import rule, { RULE_NAME } from "./no-useless-fragment"; + +const rootDir = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2021, + sourceType: "module", + project: "./tsconfig.json", + tsconfigRootDir: rootDir, + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ...allValid, + "<>", + "<>foo
", + "<>
", + '<>{"moo"} ', + "", + "", + "", + "<>
", + '
{"a"}{"b"}} />', + "{item.value}", + "eeee ee eeeeeee eeeeeeee} />", + "<>{foos.map(foo => foo)}", + { + code: "<>{moo}", + options: [{ allowExpressions: true }], + }, + { + code: ` + <> + {moo} + + `, + options: [{ allowExpressions: true }], + }, + ], + invalid: [ + { + code: "<>", + output: null, + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }], + }, + { + code: "<>{}", + output: null, + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }], + }, + { + code: "

moo<>foo

", + output: "

moofoo

", + errors: [ + { messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }, + { messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXFragment }, + ], + }, + { + code: "<>{meow}", + output: null, + errors: [{ messageId: "NeedsMoreChildren" }], + }, + { + code: "

<>{meow}

", + output: "

{meow}

", + errors: [ + { messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }, + { messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXFragment }, + ], + }, + { + code: "<>
", + output: "
", + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }], + }, + { + code: ` + <> +
+ + `, + output: ` +
+ `, + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }], + }, + { + code: "", + output: null, + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXElement }], + }, + { + code: ` + + + + `, + output: ` + + `, + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXElement }], + }, + { + code: ` + + {foo} + + `, + output: null, + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXElement }], + settings: { + react: { + pragma: "SomeReact", + fragment: "SomeFragment", + }, + }, + }, + { + // Not safe to fix this case because `Eeee` might require child be ReactElement + code: "<>foo", + output: null, + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }], + }, + { + code: "
<>foo
", + output: "
foo
", + errors: [ + { messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }, + { messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXFragment }, + ], + }, + { + code: '
<>{"a"}{"b"}
', + output: '
{"a"}{"b"}
', + errors: [{ messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXFragment }], + }, + { + code: ` +
+ + + <>{"a"}{"b"} +
`, + output: ` +
+ + + {"a"}{"b"} +
`, + errors: [{ messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXFragment }], + }, + { + code: '
{"a"}{"b"}
', + output: '
{"a"}{"b"}
', + errors: [{ messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXElement }], + }, + { + // whitepace tricky case + code: ` +
+ git<> + hub. + + + git<> hub +
`, + output: ` +
+ github. + + git hub +
`, + errors: [ + { messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXFragment, line: 3 }, + { messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXFragment, line: 7 }, + ], + }, + { + code: '
a <>{""}{""} a
', + output: '
a {""}{""} a
', + errors: [{ messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXFragment }], + }, + { + code: ` + const Comp = () => ( + + + + ); + `, + output: ` + const Comp = () => ( + + ${/* dprint-ignore the trailing whitespace here is intentional */ ""} + + ); + `, + errors: [ + { messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXElement, line: 4 }, + { messageId: "ChildOfHtmlElement", type: AST_NODE_TYPES.JSXElement, line: 4 }, + ], + }, + // Ensure allowExpressions still catches expected violations + { + code: "<>{moo}", + output: "{moo}", + options: [{ allowExpressions: true }], + errors: [{ messageId: "NeedsMoreChildren", type: AST_NODE_TYPES.JSXFragment }], + }, + ], +}); diff --git a/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts new file mode 100644 index 000000000..fa70e5090 --- /dev/null +++ b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts @@ -0,0 +1,160 @@ +import { + getFragmentFromContext, + getPragmaFromContext, + isChildOfComponentElement, + isChildOfHtmlElement, + isFragment, + isFragmentHasLessThanTwoChildren, + isFragmentWithOnlyTextAndIsNotChild, + isFragmentWithSingleExpression, + isKeyedElement, + isLiteral, + isWhiteSpace, +} from "@eslint-react/jsx"; +import type { TSESTree } from "@typescript-eslint/utils"; +import type { ESLintUtils } from "@typescript-eslint/utils"; +import { AST_NODE_TYPES } from "@typescript-eslint/utils"; +import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; +import type { RuleFixer } from "@typescript-eslint/utils/ts-eslint"; + +import { createRule } from "../utils"; + +function trimLikeReact(text: string) { + const leadingSpaces = /^\s*/u.exec(text)?.[0]; + const trailingSpaces = /\s*$/u.exec(text)?.[0]; + + const start = leadingSpaces?.includes("\n") ? leadingSpaces.length : 0; + const end = trailingSpaces?.includes("\n") ? text.length - trailingSpaces.length : text.length; + + return text.slice(start, end); +} + +export const RULE_NAME = "no-useless-fragment"; + +type MessageID = "ChildOfHtmlElement" | "NeedsMoreChildren"; + +type Options = readonly [ + { + allowExpressions?: boolean; + }?, +]; + +const defaultOptions = [ + { + allowExpressions: false, + }, +] as const; + +const schema = [{ + type: "object", + properties: { + allowExpressions: { + type: "boolean", + }, + }, +}] satisfies [JSONSchema4]; + +export default createRule({ + name: RULE_NAME, + meta: { + type: "suggestion", + docs: { + description: "disallow unnecessary fragments", + requiresTypeChecking: false, + }, + fixable: "code", + // eslint-disable-next-line perfectionist/sort-objects + schema, + messages: { + ChildOfHtmlElement: "Passing a fragment to an HTML element is useless.", + NeedsMoreChildren: + "Fragments should contain more than one child - otherwise, there’s no need for a Fragment at all.", + }, + }, + defaultOptions, + create(context) { + const config = context.options[0] || {}; + const allowExpressions = config.allowExpressions || false; + + const reactPragma = getPragmaFromContext(context); + const fragmentPragma = getFragmentFromContext(context); + + function canFix(node: TSESTree.JSXElement | TSESTree.JSXFragment) { + // Not safe to fix fragments without a jsx parent. + if (!(node.parent.type === AST_NODE_TYPES.JSXElement || node.parent.type === AST_NODE_TYPES.JSXFragment)) { + // const a = <> + if (node.children.length === 0) { + return false; + } + + // const a = <>cat {meow} + if ( + node.children.some( + (child) => + // eslint-disable-next-line @typescript-eslint/no-extra-parens + (isLiteral(child) && !isWhiteSpace(child)) + || child.type === AST_NODE_TYPES.JSXExpressionContainer, + ) + ) { + return false; + } + } + + // Not safe to fix `<>foo` because `Eeee` might require its children be a ReactElement. + return !isChildOfComponentElement(node, reactPragma, fragmentPragma); + } + + function getFix(node: TSESTree.JSXElement | TSESTree.JSXFragment) { + if (!canFix(node)) { + return null; + } + + return function fix(fixer: RuleFixer) { + const opener = node.type === AST_NODE_TYPES.JSXFragment ? node.openingFragment : node.openingElement; + const closer = node.type === AST_NODE_TYPES.JSXFragment ? node.closingFragment : node.closingElement; + + const childrenText = opener.type === AST_NODE_TYPES.JSXOpeningElement + && opener.selfClosing + ? "" + : context.getSourceCode().getText().slice(opener.range[1], closer?.range[0]); + + return fixer.replaceText(node, trimLikeReact(childrenText)); + }; + } + + function checkNode(node: TSESTree.JSXElement | TSESTree.JSXFragment) { + if (isKeyedElement(node)) { + return; + } + + if ( + isFragmentHasLessThanTwoChildren(node) + && !isFragmentWithOnlyTextAndIsNotChild(node) + && !(allowExpressions && isFragmentWithSingleExpression(node)) + ) { + context.report({ + fix: getFix(node), + messageId: "NeedsMoreChildren", + node, + }); + } + + if (isChildOfHtmlElement(node)) { + context.report({ + fix: getFix(node), + messageId: "ChildOfHtmlElement", + node, + }); + } + } + + return { + JSXElement(node) { + if (isFragment(node, reactPragma, fragmentPragma)) { + checkNode(node); + } + }, + JSXFragment: checkNode, + }; + }, +}); diff --git a/packages/jsx/docs/README.md b/packages/jsx/docs/README.md index c94d5307d..e06192749 100644 --- a/packages/jsx/docs/README.md +++ b/packages/jsx/docs/README.md @@ -42,16 +42,25 @@ - [hasEveryProp](README.md#haseveryprop) - [hasProp](README.md#hasprop) - [isCallFromPragma](README.md#iscallfrompragma) +- [isChildOfComponentElement](README.md#ischildofcomponentelement) +- [isChildOfHtmlElement](README.md#ischildofhtmlelement) - [isChildrenOfCreateElement](README.md#ischildrenofcreateelement) - [isCloneElementCall](README.md#iscloneelementcall) - [isCreateElementCall](README.md#iscreateelementcall) +- [isFragment](README.md#isfragment) +- [isFragmentHasLessThanTwoChildren](README.md#isfragmenthaslessthantwochildren) +- [isFragmentWithOnlyTextAndIsNotChild](README.md#isfragmentwithonlytextandisnotchild) +- [isFragmentWithSingleExpression](README.md#isfragmentwithsingleexpression) - [isFunctionReturningJSXValue](README.md#isfunctionreturningjsxvalue) - [isInitializedFromPragma](README.md#isinitializedfrompragma) - [isInsideCreateElementProps](README.md#isinsidecreateelementprops) - [isInsidePropValue](README.md#isinsidepropvalue) +- [isJSXAttributeKey](README.md#isjsxattributekey) - [isJSXValue](README.md#isjsxvalue) +- [isKeyedElement](README.md#iskeyedelement) - [isLineBreak](README.md#islinebreak) - [isLiteral](README.md#isliteral) +- [isPaddingSpaces](README.md#ispaddingspaces) - [isPropertyOfPragma](README.md#ispropertyofpragma) - [isWhiteSpace](README.md#iswhitespace) - [traverseUpProp](README.md#traverseupprop) @@ -496,6 +505,40 @@ node is CallExpression --- +### isChildOfComponentElement + +▸ **isChildOfComponentElement**(`node`, `reactPragma`, `fragmentPragma`): `boolean` + +#### Parameters + +| Name | Type | +| :--------------- | :------- | +| `node` | `Node` | +| `reactPragma` | `string` | +| `fragmentPragma` | `string` | + +#### Returns + +`boolean` + +--- + +### isChildOfHtmlElement + +▸ **isChildOfHtmlElement**(`node`): `boolean` + +#### Parameters + +| Name | Type | +| :----- | :----- | +| `node` | `Node` | + +#### Returns + +`boolean` + +--- + ### isChildrenOfCreateElement ▸ **isChildrenOfCreateElement**(`node`, `context`): `boolean` @@ -555,6 +598,86 @@ node is CallExpression --- +### isFragment + +▸ **isFragment**(`node`, `reactPragma`, `fragmentPragma`): `boolean` + +#### Parameters + +| Name | Type | +| :--------------- | :----------- | +| `node` | `JSXElement` | +| `reactPragma` | `string` | +| `fragmentPragma` | `string` | + +#### Returns + +`boolean` + +--- + +### isFragmentHasLessThanTwoChildren + +▸ **isFragmentHasLessThanTwoChildren**(`node`): `boolean` + +Check if a JSXElement or JSXFragment has less than two non-padding children and the first child is not a call expression + +#### Parameters + +| Name | Type | Description | +| :----- | :---------------------------- | :-------------------- | +| `node` | `JSXElement` \| `JSXFragment` | The AST node to check | + +#### Returns + +`boolean` + +boolean + +--- + +### isFragmentWithOnlyTextAndIsNotChild + +▸ **isFragmentWithOnlyTextAndIsNotChild**(`node`): `boolean` + +Check if a JSXElement or JSXFragment has only one literal child and is not a child + +#### Parameters + +| Name | Type | Description | +| :----- | :---------------------------- | :-------------------- | +| `node` | `JSXElement` \| `JSXFragment` | The AST node to check | + +#### Returns + +`boolean` + +`true` if the node has only one literal child and is not a child + +**`Example`** + +```ts +Somehow fragment like this is useful: ee eeee eeee ...} /> +``` + +--- + +### isFragmentWithSingleExpression + +▸ **isFragmentWithSingleExpression**(`node`): `boolean` + +#### Parameters + +| Name | Type | +| :----- | :---------------------------- | +| `node` | `JSXElement` \| `JSXFragment` | + +#### Returns + +`boolean` + +--- + ### isFunctionReturningJSXValue ▸ **isFunctionReturningJSXValue**(`node`, `context`, `options?`): `boolean` @@ -636,6 +759,26 @@ Checks if the node is inside a prop's value --- +### isJSXAttributeKey + +▸ **isJSXAttributeKey**(`node`): `boolean` + +Check if node is like `key={...}` as in `` + +#### Parameters + +| Name | Type | Description | +| :----- | :----- | :-------------------- | +| `node` | `Node` | The AST node to check | + +#### Returns + +`boolean` + +`true` if the node is like `key={...}` + +--- + ### isJSXValue ▸ **isJSXValue**(`node`, `context`, `options?`): `boolean` @@ -658,6 +801,22 @@ boolean --- +### isKeyedElement + +▸ **isKeyedElement**(`node`): `boolean` + +#### Parameters + +| Name | Type | +| :----- | :----- | +| `node` | `Node` | + +#### Returns + +`boolean` + +--- + ### isLineBreak ▸ **isLineBreak**(`node`): `boolean` @@ -698,6 +857,26 @@ boolean `true` if the node is a Literal or JSXText --- +### isPaddingSpaces + +▸ **isPaddingSpaces**(`node`): `boolean` + +Check if a Literal or JSXText node is padding spaces + +#### Parameters + +| Name | Type | Description | +| :----- | :----- | :-------------------- | +| `node` | `Node` | The AST node to check | + +#### Returns + +`boolean` + +boolean + +--- + ### isPropertyOfPragma ▸ **isPropertyOfPragma**(`name`, `context`, `pragma?`): (`node`: `Node`) => `boolean` diff --git a/packages/jsx/src/attribute.ts b/packages/jsx/src/attribute.ts new file mode 100644 index 000000000..4a068d4c0 --- /dev/null +++ b/packages/jsx/src/attribute.ts @@ -0,0 +1,12 @@ +import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; + +/** + * Check if node is like `key={...}` as in `` + * @param node The AST node to check + * @returns `true` if the node is like `key={...}` + */ +export function isJSXAttributeKey(node: TSESTree.Node) { + return node.type === AST_NODE_TYPES.JSXAttribute + && node.name.type === AST_NODE_TYPES.JSXIdentifier + && node.name.name === "key"; +} diff --git a/packages/jsx/src/children.ts b/packages/jsx/src/children.ts index c2444507c..1b5b62de7 100644 --- a/packages/jsx/src/children.ts +++ b/packages/jsx/src/children.ts @@ -1,4 +1,6 @@ -import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; + +import { isFragment } from "./fragment"; /** * Check if a JSXElement or JSXFragment has children @@ -8,3 +10,15 @@ import type { TSESTree } from "@typescript-eslint/types"; export function hasChildren(node: TSESTree.JSXElement | TSESTree.JSXFragment) { return node.children.length > 0; } + +export function isChildOfHtmlElement(node: TSESTree.Node) { + return node.parent?.type === AST_NODE_TYPES.JSXElement + && node.parent.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier + && /^[a-z]+$/u.test(node.parent.openingElement.name.name); +} + +export function isChildOfComponentElement(node: TSESTree.Node, reactPragma: string, fragmentPragma: string) { + return node.parent?.type === AST_NODE_TYPES.JSXElement + && !isChildOfHtmlElement(node) + && !isFragment(node.parent, reactPragma, fragmentPragma); +} diff --git a/packages/jsx/src/element/index.ts b/packages/jsx/src/element/index.ts index 9a1e51cbd..e0d6ea2d6 100644 --- a/packages/jsx/src/element/index.ts +++ b/packages/jsx/src/element/index.ts @@ -1,3 +1,4 @@ export * from "./is-children-of-create-element"; export * from "./is-element-call"; export * from "./is-inside-create-element-props"; +export * from "./is-keyed-element"; diff --git a/packages/jsx/src/element/is-keyed-element.ts b/packages/jsx/src/element/is-keyed-element.ts new file mode 100644 index 000000000..f1bc310b0 --- /dev/null +++ b/packages/jsx/src/element/is-keyed-element.ts @@ -0,0 +1,9 @@ +import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES } from "@typescript-eslint/types"; + +import { isJSXAttributeKey } from "../attribute"; + +export function isKeyedElement(node: TSESTree.Node) { + return node.type === AST_NODE_TYPES.JSXElement + && node.openingElement.attributes.some(isJSXAttributeKey); +} diff --git a/packages/jsx/src/fragment.ts b/packages/jsx/src/fragment.ts new file mode 100644 index 000000000..d20e5574d --- /dev/null +++ b/packages/jsx/src/fragment.ts @@ -0,0 +1,61 @@ +import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; + +import { isLiteral, isPaddingSpaces } from "./textnode"; + +export function isFragment(node: TSESTree.JSXElement, reactPragma: string, fragmentPragma: string) { + const { name } = node.openingElement; + + // + if (name.type === AST_NODE_TYPES.JSXIdentifier && name.name === fragmentPragma) { + return true; + } + + // + return name.type === AST_NODE_TYPES.JSXMemberExpression + && name.object.type === AST_NODE_TYPES.JSXIdentifier + && name.object.name === reactPragma + && name.property.name === fragmentPragma; +} + +/** + * Check if a JSXElement or JSXFragment has only one literal child and is not a child + * @param node The AST node to check + * @returns `true` if the node has only one literal child and is not a child + * @example Somehow fragment like this is useful: ee eeee eeee ...} /> + */ +export function isFragmentWithOnlyTextAndIsNotChild(node: TSESTree.JSXElement | TSESTree.JSXFragment) { + return node.children.length === 1 + && isLiteral(node.children[0]) + && !(node.parent.type === AST_NODE_TYPES.JSXElement || node.parent.type === AST_NODE_TYPES.JSXFragment); +} + +function containsCallExpression(node: TSESTree.Node) { + return node.type === AST_NODE_TYPES.JSXExpressionContainer + && node.expression.type === AST_NODE_TYPES.CallExpression; +} + +/** + * Check if a JSXElement or JSXFragment has less than two non-padding children and the first child is not a call expression + * @param node The AST node to check + * @returns boolean + */ +export function isFragmentHasLessThanTwoChildren(node: TSESTree.JSXElement | TSESTree.JSXFragment) { + const nonPaddingChildren = node.children.filter( + (child) => !isPaddingSpaces(child), + ); + + if (nonPaddingChildren.length === 1) { + return !containsCallExpression(nonPaddingChildren[0] as TSESTree.Node); + } + + return nonPaddingChildren.length === 0; +} + +export function isFragmentWithSingleExpression(node: TSESTree.JSXElement | TSESTree.JSXFragment) { + const children = node.children.filter((child) => !isPaddingSpaces(child)); + + return ( + children.length === 1 + && children[0]?.type === AST_NODE_TYPES.JSXExpressionContainer + ); +} diff --git a/packages/jsx/src/index.ts b/packages/jsx/src/index.ts index 1664eb22d..688a91228 100644 --- a/packages/jsx/src/index.ts +++ b/packages/jsx/src/index.ts @@ -1,7 +1,9 @@ +export * from "./attribute"; export * from "./children"; export * from "./element"; export * from "./element-type"; export * from "./event-handler"; +export * from "./fragment"; export * from "./misc"; export * from "./pragma"; export * from "./prop"; diff --git a/packages/jsx/src/textnode.ts b/packages/jsx/src/textnode.ts index dde8ce86e..4821e420b 100644 --- a/packages/jsx/src/textnode.ts +++ b/packages/jsx/src/textnode.ts @@ -26,3 +26,14 @@ export function isWhiteSpace(node: TSESTree.JSXText | TSESTree.Literal) { export function isLineBreak(node: TSESTree.Node) { return isLiteral(node) && isWhiteSpace(node) && isMultiLine(node); } + +/** + * Check if a Literal or JSXText node is padding spaces + * @param node The AST node to check + * @returns boolean + */ +export function isPaddingSpaces(node: TSESTree.Node) { + return isLiteral(node) + && isWhiteSpace(node) + && node.raw.includes("\n"); +} From 0975f684fb8109f5c7aa50a7f1177e21e8a2d427 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Wed, 1 Nov 2023 19:47:10 +0800 Subject: [PATCH 2/4] refactor: use hasProp --- .../src/rules/no-useless-fragment.ts | 11 +++++- packages/jsx/docs/README.md | 38 ------------------- packages/jsx/src/attribute.ts | 12 ------ packages/jsx/src/element/index.ts | 1 - packages/jsx/src/element/is-keyed-element.ts | 9 ----- packages/jsx/src/index.ts | 1 - 6 files changed, 9 insertions(+), 63 deletions(-) delete mode 100644 packages/jsx/src/attribute.ts delete mode 100644 packages/jsx/src/element/is-keyed-element.ts diff --git a/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts index fa70e5090..60732612d 100644 --- a/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts +++ b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts @@ -1,13 +1,13 @@ import { getFragmentFromContext, getPragmaFromContext, + hasProp, isChildOfComponentElement, isChildOfHtmlElement, isFragment, isFragmentHasLessThanTwoChildren, isFragmentWithOnlyTextAndIsNotChild, isFragmentWithSingleExpression, - isKeyedElement, isLiteral, isWhiteSpace, } from "@eslint-react/jsx"; @@ -123,7 +123,14 @@ export default createRule({ } function checkNode(node: TSESTree.JSXElement | TSESTree.JSXFragment) { - if (isKeyedElement(node)) { + if ( + node.type === AST_NODE_TYPES.JSXElement + && hasProp( + node.openingElement.attributes, + "key", + context, + ) + ) { return; } diff --git a/packages/jsx/docs/README.md b/packages/jsx/docs/README.md index e06192749..cbedc9a92 100644 --- a/packages/jsx/docs/README.md +++ b/packages/jsx/docs/README.md @@ -55,9 +55,7 @@ - [isInitializedFromPragma](README.md#isinitializedfrompragma) - [isInsideCreateElementProps](README.md#isinsidecreateelementprops) - [isInsidePropValue](README.md#isinsidepropvalue) -- [isJSXAttributeKey](README.md#isjsxattributekey) - [isJSXValue](README.md#isjsxvalue) -- [isKeyedElement](README.md#iskeyedelement) - [isLineBreak](README.md#islinebreak) - [isLiteral](README.md#isliteral) - [isPaddingSpaces](README.md#ispaddingspaces) @@ -759,26 +757,6 @@ Checks if the node is inside a prop's value --- -### isJSXAttributeKey - -▸ **isJSXAttributeKey**(`node`): `boolean` - -Check if node is like `key={...}` as in `` - -#### Parameters - -| Name | Type | Description | -| :----- | :----- | :-------------------- | -| `node` | `Node` | The AST node to check | - -#### Returns - -`boolean` - -`true` if the node is like `key={...}` - ---- - ### isJSXValue ▸ **isJSXValue**(`node`, `context`, `options?`): `boolean` @@ -801,22 +779,6 @@ boolean --- -### isKeyedElement - -▸ **isKeyedElement**(`node`): `boolean` - -#### Parameters - -| Name | Type | -| :----- | :----- | -| `node` | `Node` | - -#### Returns - -`boolean` - ---- - ### isLineBreak ▸ **isLineBreak**(`node`): `boolean` diff --git a/packages/jsx/src/attribute.ts b/packages/jsx/src/attribute.ts deleted file mode 100644 index 4a068d4c0..000000000 --- a/packages/jsx/src/attribute.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; - -/** - * Check if node is like `key={...}` as in `` - * @param node The AST node to check - * @returns `true` if the node is like `key={...}` - */ -export function isJSXAttributeKey(node: TSESTree.Node) { - return node.type === AST_NODE_TYPES.JSXAttribute - && node.name.type === AST_NODE_TYPES.JSXIdentifier - && node.name.name === "key"; -} diff --git a/packages/jsx/src/element/index.ts b/packages/jsx/src/element/index.ts index e0d6ea2d6..9a1e51cbd 100644 --- a/packages/jsx/src/element/index.ts +++ b/packages/jsx/src/element/index.ts @@ -1,4 +1,3 @@ export * from "./is-children-of-create-element"; export * from "./is-element-call"; export * from "./is-inside-create-element-props"; -export * from "./is-keyed-element"; diff --git a/packages/jsx/src/element/is-keyed-element.ts b/packages/jsx/src/element/is-keyed-element.ts deleted file mode 100644 index f1bc310b0..000000000 --- a/packages/jsx/src/element/is-keyed-element.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES } from "@typescript-eslint/types"; - -import { isJSXAttributeKey } from "../attribute"; - -export function isKeyedElement(node: TSESTree.Node) { - return node.type === AST_NODE_TYPES.JSXElement - && node.openingElement.attributes.some(isJSXAttributeKey); -} diff --git a/packages/jsx/src/index.ts b/packages/jsx/src/index.ts index 688a91228..2feb4d0b1 100644 --- a/packages/jsx/src/index.ts +++ b/packages/jsx/src/index.ts @@ -1,4 +1,3 @@ -export * from "./attribute"; export * from "./children"; export * from "./element"; export * from "./element-type"; From fde354918abb36fe67a0303753f93b93492b2bc1 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Wed, 1 Nov 2023 19:55:11 +0800 Subject: [PATCH 3/4] docs: remove fixable column for now --- README.md | 76 +++++++++++++++++----------------- eslint-doc-generator.config.ts | 1 + 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index ee74cc12a..79c95fc06 100644 --- a/README.md +++ b/README.md @@ -88,59 +88,57 @@ export default [ -🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). - ### debug -| Name | Description | 🔧 | -| :--------------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | -| [debug/class-component](packages/eslint-plugin-debug/src/rules/class-component.md) | reports all class components, including anonymous ones | | -| [debug/function-component](packages/eslint-plugin-debug/src/rules/function-component.md) | reports all function components, including anonymous ones | | -| [debug/hooks](packages/eslint-plugin-debug/src/rules/hooks.md) | reports all react hooks | | +| Name | Description | +| :--------------------------------------------------------------------------------------- | :-------------------------------------------------------- | +| [debug/class-component](packages/eslint-plugin-debug/src/rules/class-component.md) | reports all class components, including anonymous ones | +| [debug/function-component](packages/eslint-plugin-debug/src/rules/function-component.md) | reports all function components, including anonymous ones | +| [debug/hooks](packages/eslint-plugin-debug/src/rules/hooks.md) | reports all react hooks | ### hooks -| Name | Description | 🔧 | -| :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- | :- | -| [hooks/ensure-custom-hooks-using-other-hooks](packages/eslint-plugin-hooks/src/rules/ensure-custom-hooks-using-other-hooks.md) | enforce custom hooks using other hooks | | +| Name | Description | +| :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- | +| [hooks/ensure-custom-hooks-using-other-hooks](packages/eslint-plugin-hooks/src/rules/ensure-custom-hooks-using-other-hooks.md) | enforce custom hooks using other hooks | ### jsx -| Name                                | Description | 🔧 | -| :------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | :- | -| [jsx/no-array-index-key](packages/eslint-plugin-jsx/src/rules/no-array-index-key.md) | disallow using Array index as key | | -| [jsx/no-duplicate-key](packages/eslint-plugin-jsx/src/rules/no-duplicate-key.md) | disallow duplicate keys in `key` prop when rendering list | | -| [jsx/no-leaked-conditional-rendering](packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.md) | disallow problematic leaked values from being rendered | | -| [jsx/no-missing-key](packages/eslint-plugin-jsx/src/rules/no-missing-key.md) | require `key` prop when rendering list | | -| [jsx/no-misused-comment-in-textnode](packages/eslint-plugin-jsx/src/rules/no-misused-comment-in-textnode.md) | disallow comments from being inserted as text nodes | | -| [jsx/no-script-url](packages/eslint-plugin-jsx/src/rules/no-script-url.md) | disallow `javascript:` URLs as JSX event handler prop's value | | -| [jsx/no-useless-fragment](packages/eslint-plugin-jsx/src/rules/no-useless-fragment.md) | disallow unnecessary fragments | 🔧 | -| [jsx/prefer-shorthand-boolean](packages/eslint-plugin-jsx/src/rules/prefer-shorthand-boolean.md) | enforce boolean attributes notation in JSX | | +| Name                                | Description | +| :------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | +| [jsx/no-array-index-key](packages/eslint-plugin-jsx/src/rules/no-array-index-key.md) | disallow using Array index as key | +| [jsx/no-duplicate-key](packages/eslint-plugin-jsx/src/rules/no-duplicate-key.md) | disallow duplicate keys in `key` prop when rendering list | +| [jsx/no-leaked-conditional-rendering](packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.md) | disallow problematic leaked values from being rendered | +| [jsx/no-missing-key](packages/eslint-plugin-jsx/src/rules/no-missing-key.md) | require `key` prop when rendering list | +| [jsx/no-misused-comment-in-textnode](packages/eslint-plugin-jsx/src/rules/no-misused-comment-in-textnode.md) | disallow comments from being inserted as text nodes | +| [jsx/no-script-url](packages/eslint-plugin-jsx/src/rules/no-script-url.md) | disallow `javascript:` URLs as JSX event handler prop's value | +| [jsx/no-useless-fragment](packages/eslint-plugin-jsx/src/rules/no-useless-fragment.md) | disallow unnecessary fragments | +| [jsx/prefer-shorthand-boolean](packages/eslint-plugin-jsx/src/rules/prefer-shorthand-boolean.md) | enforce boolean attributes notation in JSX | ### naming-convention -| Name | Description | 🔧 | -| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------ | :- | -| [naming-convention/filename](packages/eslint-plugin-naming-convention/src/rules/filename.md) | enforce naming convention for JSX file names | | -| [naming-convention/filename-extension](packages/eslint-plugin-naming-convention/src/rules/filename-extension.md) | enforce naming convention for JSX file extensions | | +| Name | Description | +| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------ | +| [naming-convention/filename](packages/eslint-plugin-naming-convention/src/rules/filename.md) | enforce naming convention for JSX file names | +| [naming-convention/filename-extension](packages/eslint-plugin-naming-convention/src/rules/filename-extension.md) | enforce naming convention for JSX file extensions | ### react -| Name                                             | Description | 🔧 | -| :--------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ | :- | -| [react/no-children-in-void-dom-elements](packages/eslint-plugin-react/src/rules/no-children-in-void-dom-elements.md) | disallow passing children to void DOM elements | | -| [react/no-class-component](packages/eslint-plugin-react/src/rules/no-class-component.md) | enforce that there are no class components | | -| [react/no-clone-element](packages/eslint-plugin-react/src/rules/no-clone-element.md) | disallow `cloneElement` | | -| [react/no-constructed-context-value](packages/eslint-plugin-react/src/rules/no-constructed-context-value.md) | disallow passing constructed values to context providers | | -| [react/no-create-ref](packages/eslint-plugin-react/src/rules/no-create-ref.md) | disallow `createRef` in function components | | -| [react/no-dangerously-set-innerhtml](packages/eslint-plugin-react/src/rules/no-dangerously-set-innerhtml.md) | disallow when a DOM element is using both children and dangerouslySetInnerHTML' | | -| [react/no-dangerously-set-innerhtml-with-children](packages/eslint-plugin-react/src/rules/no-dangerously-set-innerhtml-with-children.md) | disallow when a DOM element is using both children and dangerouslySetInnerHTML' | | -| [react/no-namespace](packages/eslint-plugin-react/src/rules/no-namespace.md) | enforce that namespaces are not used in React elements | | -| [react/no-string-refs](packages/eslint-plugin-react/src/rules/no-string-refs.md) | disallow using deprecated string refs | | -| [react/no-string-style-props](packages/eslint-plugin-react/src/rules/no-string-style-props.md) | disallow using string as style props value | | -| [react/no-unstable-default-props](packages/eslint-plugin-react/src/rules/no-unstable-default-props.md) | disallow usage of unstable value as default param in function component | | -| [react/no-unstable-nested-components](packages/eslint-plugin-react/src/rules/no-unstable-nested-components.md) | disallow usage of unstable nested components | | -| [react/prefer-destructuring-assignment](packages/eslint-plugin-react/src/rules/prefer-destructuring-assignment.md) | enforce using destructuring assignment in component props and context | | +| Name                                             | Description | +| :--------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ | +| [react/no-children-in-void-dom-elements](packages/eslint-plugin-react/src/rules/no-children-in-void-dom-elements.md) | disallow passing children to void DOM elements | +| [react/no-class-component](packages/eslint-plugin-react/src/rules/no-class-component.md) | enforce that there are no class components | +| [react/no-clone-element](packages/eslint-plugin-react/src/rules/no-clone-element.md) | disallow `cloneElement` | +| [react/no-constructed-context-value](packages/eslint-plugin-react/src/rules/no-constructed-context-value.md) | disallow passing constructed values to context providers | +| [react/no-create-ref](packages/eslint-plugin-react/src/rules/no-create-ref.md) | disallow `createRef` in function components | +| [react/no-dangerously-set-innerhtml](packages/eslint-plugin-react/src/rules/no-dangerously-set-innerhtml.md) | disallow when a DOM element is using both children and dangerouslySetInnerHTML' | +| [react/no-dangerously-set-innerhtml-with-children](packages/eslint-plugin-react/src/rules/no-dangerously-set-innerhtml-with-children.md) | disallow when a DOM element is using both children and dangerouslySetInnerHTML' | +| [react/no-namespace](packages/eslint-plugin-react/src/rules/no-namespace.md) | enforce that namespaces are not used in React elements | +| [react/no-string-refs](packages/eslint-plugin-react/src/rules/no-string-refs.md) | disallow using deprecated string refs | +| [react/no-string-style-props](packages/eslint-plugin-react/src/rules/no-string-style-props.md) | disallow using string as style props value | +| [react/no-unstable-default-props](packages/eslint-plugin-react/src/rules/no-unstable-default-props.md) | disallow usage of unstable value as default param in function component | +| [react/no-unstable-nested-components](packages/eslint-plugin-react/src/rules/no-unstable-nested-components.md) | disallow usage of unstable nested components | +| [react/prefer-destructuring-assignment](packages/eslint-plugin-react/src/rules/prefer-destructuring-assignment.md) | enforce using destructuring assignment in component props and context | diff --git a/eslint-doc-generator.config.ts b/eslint-doc-generator.config.ts index 42e2ee82f..f0b098345 100644 --- a/eslint-doc-generator.config.ts +++ b/eslint-doc-generator.config.ts @@ -30,6 +30,7 @@ export default { pathRuleList: "README.md", ruleDocSectionInclude: ["Rule Details"], ruleDocTitleFormat: "name", + ruleListColumns: ["name", "description"], ruleListSplit(rules) { const record = rules.reduce>((acc, [name, rule]) => { const title = /^([\w-]+)\/[\w-]+/iu.exec(name)?.[1] ?? defaultTitle; From 8cb014b23a7acd0439ba27858553131d7b937c62 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Wed, 1 Nov 2023 20:55:08 +0800 Subject: [PATCH 4/4] refactor: rename --- .../src/rules/no-useless-fragment.ts | 8 ++-- packages/jsx/docs/README.md | 40 +++++++++---------- packages/jsx/src/children.ts | 8 ++-- packages/jsx/src/fragment.ts | 8 ++-- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts index 60732612d..8b4417312 100644 --- a/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts +++ b/packages/eslint-plugin-jsx/src/rules/no-useless-fragment.ts @@ -2,8 +2,8 @@ import { getFragmentFromContext, getPragmaFromContext, hasProp, - isChildOfComponentElement, - isChildOfHtmlElement, + isChildOfBuiltinComponentElement, + isChildOfUserDefinedComponentElement, isFragment, isFragmentHasLessThanTwoChildren, isFragmentWithOnlyTextAndIsNotChild, @@ -101,7 +101,7 @@ export default createRule({ } // Not safe to fix `<>foo` because `Eeee` might require its children be a ReactElement. - return !isChildOfComponentElement(node, reactPragma, fragmentPragma); + return !isChildOfUserDefinedComponentElement(node, reactPragma, fragmentPragma); } function getFix(node: TSESTree.JSXElement | TSESTree.JSXFragment) { @@ -146,7 +146,7 @@ export default createRule({ }); } - if (isChildOfHtmlElement(node)) { + if (isChildOfBuiltinComponentElement(node)) { context.report({ fix: getFix(node), messageId: "ChildOfHtmlElement", diff --git a/packages/jsx/docs/README.md b/packages/jsx/docs/README.md index cbedc9a92..66217d3fe 100644 --- a/packages/jsx/docs/README.md +++ b/packages/jsx/docs/README.md @@ -42,8 +42,8 @@ - [hasEveryProp](README.md#haseveryprop) - [hasProp](README.md#hasprop) - [isCallFromPragma](README.md#iscallfrompragma) -- [isChildOfComponentElement](README.md#ischildofcomponentelement) -- [isChildOfHtmlElement](README.md#ischildofhtmlelement) +- [isChildOfBuiltinComponentElement](README.md#ischildofbuiltincomponentelement) +- [isChildOfUserDefinedComponentElement](README.md#ischildofuserdefinedcomponentelement) - [isChildrenOfCreateElement](README.md#ischildrenofcreateelement) - [isCloneElementCall](README.md#iscloneelementcall) - [isCreateElementCall](README.md#iscreateelementcall) @@ -503,17 +503,15 @@ node is CallExpression --- -### isChildOfComponentElement +### isChildOfBuiltinComponentElement -▸ **isChildOfComponentElement**(`node`, `reactPragma`, `fragmentPragma`): `boolean` +▸ **isChildOfBuiltinComponentElement**(`node`): `boolean` #### Parameters -| Name | Type | -| :--------------- | :------- | -| `node` | `Node` | -| `reactPragma` | `string` | -| `fragmentPragma` | `string` | +| Name | Type | +| :----- | :----- | +| `node` | `Node` | #### Returns @@ -521,15 +519,17 @@ node is CallExpression --- -### isChildOfHtmlElement +### isChildOfUserDefinedComponentElement -▸ **isChildOfHtmlElement**(`node`): `boolean` +▸ **isChildOfUserDefinedComponentElement**(`node`, `pragma`, `fragment`): `boolean` #### Parameters -| Name | Type | -| :----- | :----- | -| `node` | `Node` | +| Name | Type | +| :--------- | :------- | +| `node` | `Node` | +| `pragma` | `string` | +| `fragment` | `string` | #### Returns @@ -598,15 +598,15 @@ node is CallExpression ### isFragment -▸ **isFragment**(`node`, `reactPragma`, `fragmentPragma`): `boolean` +▸ **isFragment**(`node`, `pragma`, `fragment`): `boolean` #### Parameters -| Name | Type | -| :--------------- | :----------- | -| `node` | `JSXElement` | -| `reactPragma` | `string` | -| `fragmentPragma` | `string` | +| Name | Type | +| :--------- | :----------- | +| `node` | `JSXElement` | +| `pragma` | `string` | +| `fragment` | `string` | #### Returns diff --git a/packages/jsx/src/children.ts b/packages/jsx/src/children.ts index 1b5b62de7..41ab52369 100644 --- a/packages/jsx/src/children.ts +++ b/packages/jsx/src/children.ts @@ -11,14 +11,14 @@ export function hasChildren(node: TSESTree.JSXElement | TSESTree.JSXFragment) { return node.children.length > 0; } -export function isChildOfHtmlElement(node: TSESTree.Node) { +export function isChildOfBuiltinComponentElement(node: TSESTree.Node) { return node.parent?.type === AST_NODE_TYPES.JSXElement && node.parent.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]+$/u.test(node.parent.openingElement.name.name); } -export function isChildOfComponentElement(node: TSESTree.Node, reactPragma: string, fragmentPragma: string) { +export function isChildOfUserDefinedComponentElement(node: TSESTree.Node, pragma: string, fragment: string) { return node.parent?.type === AST_NODE_TYPES.JSXElement - && !isChildOfHtmlElement(node) - && !isFragment(node.parent, reactPragma, fragmentPragma); + && !isChildOfBuiltinComponentElement(node) + && !isFragment(node.parent, pragma, fragment); } diff --git a/packages/jsx/src/fragment.ts b/packages/jsx/src/fragment.ts index d20e5574d..a417703a6 100644 --- a/packages/jsx/src/fragment.ts +++ b/packages/jsx/src/fragment.ts @@ -2,19 +2,19 @@ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; import { isLiteral, isPaddingSpaces } from "./textnode"; -export function isFragment(node: TSESTree.JSXElement, reactPragma: string, fragmentPragma: string) { +export function isFragment(node: TSESTree.JSXElement, pragma: string, fragment: string) { const { name } = node.openingElement; // - if (name.type === AST_NODE_TYPES.JSXIdentifier && name.name === fragmentPragma) { + if (name.type === AST_NODE_TYPES.JSXIdentifier && name.name === fragment) { return true; } // return name.type === AST_NODE_TYPES.JSXMemberExpression && name.object.type === AST_NODE_TYPES.JSXIdentifier - && name.object.name === reactPragma - && name.property.name === fragmentPragma; + && name.object.name === pragma + && name.property.name === fragment; } /**