From 347f1a38ef3c7065f240a9c572a319a8115bc37e Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Mon, 4 Aug 2025 00:54:34 +0800 Subject: [PATCH] fix: improve logic for detecting significant children in JSX elements, closes #1163 --- ...rously-set-innerhtml-with-children.spec.ts | 28 ++++++++++++++++ ...dangerously-set-innerhtml-with-children.ts | 32 +++++++++++++++---- pnpm-lock.yaml | 2 ++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.spec.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.spec.ts index 5be92153a3..aef1dd127a 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.spec.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.spec.ts @@ -39,6 +39,30 @@ ruleTester.run(RULE_NAME, rule, { code: tsx` `, errors: [{ messageId: "noDangerouslySetInnerhtmlWithChildren" }], }, + { + // https://github.com/Rel1cx/eslint-react/issues/1163 + code: tsx` + function Abc() { + return ( + <> + {/* Error on div 1: A DOM component cannot use both 'children' and 'dangerouslySetInnerHTML'. eslint @eslint-react/dom/no-dangerously-set-innerhtml-with-children */} +
+ Goodbye World +
+ + {/* No error on div 2 */} +
+

Goodbye World

+
+ + ); + } + `, + errors: [ + { messageId: "noDangerouslySetInnerhtmlWithChildren" }, + { messageId: "noDangerouslySetInnerhtmlWithChildren" }, + ], + }, ], valid: [ ...allValid, @@ -60,6 +84,10 @@ ruleTester.run(RULE_NAME, rule, { const { a, b, ...props } = otherProps const div =
`, + tsx` + + + `, "Children", '', '\n', diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts index e4c8212bef..87a2dfe754 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts @@ -1,10 +1,9 @@ import type { RuleContext, RuleFeature } from "@eslint-react/kit"; -import type { TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; -import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; +import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; import { createRule } from "../utils"; export const RULE_NAME = "no-dangerously-set-innerhtml-with-children"; @@ -40,7 +39,8 @@ export function create(context: RuleContext): RuleListener { JSXElement(node) { const attributes = node.openingElement.attributes; const initialScope = context.sourceCode.getScope(node); - const hasChildren = hasChildrenWithin(node) || ER.hasAttribute(context, "children", attributes, initialScope); + const hasChildren = node.children.some(isSignificantChildren) + || ER.hasAttribute(context, "children", attributes, initialScope); if (hasChildren && ER.hasAttribute(context, dangerouslySetInnerHTML, attributes, initialScope)) { context.report({ messageId: "noDangerouslySetInnerhtmlWithChildren", @@ -51,8 +51,26 @@ export function create(context: RuleContext): RuleListener { }; } -function hasChildrenWithin(node: TSESTree.JSXElement): boolean { - return node.children.length > 0 - && node.children[0] != null - && !AST.isLineBreak(node.children[0]); +/** + * Check if a Literal or JSXText node is whitespace + * @param node The AST node to check + * @returns boolean `true` if the node is whitespace + */ +function isWhiteSpace(node: TSESTree.JSXText | TSESTree.Literal) { + return typeof node.value === "string" && node.raw.trim() === ""; +} + +/** + * Check if a Literal or JSXText node is padding spaces + * @param node The AST node to check + * @returns boolean + */ +function isPaddingSpaces(node: TSESTree.Node) { + return ER.isJsxText(node) + && isWhiteSpace(node) + && node.raw.includes("\n"); +} + +function isSignificantChildren(node: TSESTree.JSXElement["children"][number]) { + return node.type !== AST_NODE_TYPES.JSXText || !isPaddingSpaces(node); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd20058a9d..fa52fe1da5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4151,6 +4151,7 @@ packages: '@zod/mini@4.0.0-beta.20250505T195954': resolution: {integrity: sha512-ioybPtU4w4TqwHvJv0gkAiYNaBkZ/BaGHBpK7viCIRSE8BiiZucVZ8vS0YE04Qy1R120nAnFy1d+tD9ByMO0yw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -6923,6 +6924,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}