diff --git a/apps/website/content/docs/faq.mdx b/apps/website/content/docs/faq.mdx index ed01e49117..e061137df4 100644 --- a/apps/website/content/docs/faq.mdx +++ b/apps/website/content/docs/faq.mdx @@ -8,15 +8,13 @@ import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; -**ESLint React addresses critical gaps of the existing `eslint-plugin-react` in modern React ecosystems**. The current plugin assumes a DOM-centric model, which creates friction when working with alternative renderers like React Native, React Three Fiber, or custom renderers. +**ESLint React addresses critical gaps of the existing `eslint-plugin-react` in modern React ecosystems**. While named "react", the current plugin implementation specifically targets React DOM and maintains DOM-centric assumptions, creating friction when used with alternative renderers like React Native, React Three Fiber, or custom renderers. -Key goals of ESLint React include: +Our solution treating DOM as one of many supported targets rather than the default assumption. This paradigm shift enables: -- **Context-aware linting**: It adapts to different runtime environments, ensuring that linting rules are relevant to the specific platform you’re targeting. -- **Future-proof architecture**: It is designed to be extensible and adaptable, allowing for easy integration of new features and support for emerging React patterns. -- **Unified code quality standards**: It enables consistent linting and code quality enforcement across various React-based projects, regardless of the rendering target. - -This makes ESLint React a more flexible and modern alternative for working across diverse React ecosystems. +- **Context-aware linting**: Adapting to different runtime environments +- **Future-proof architecture**: Compatibility with emerging React platforms +- **Unified code quality standards**: Consistent linting across diverse projects 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 b22ee5d1c3..e4c8212bef 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 @@ -32,14 +32,16 @@ export default createRule<[], MessageID>({ defaultOptions: [], }); +const dangerouslySetInnerHTML = "dangerouslySetInnerHTML"; + export function create(context: RuleContext): RuleListener { - if (!context.sourceCode.text.includes("dangerouslySetInnerHTML")) return {}; + if (!context.sourceCode.text.includes(dangerouslySetInnerHTML)) return {}; return { JSXElement(node) { const attributes = node.openingElement.attributes; const initialScope = context.sourceCode.getScope(node); const hasChildren = hasChildrenWithin(node) || ER.hasAttribute(context, "children", attributes, initialScope); - if (hasChildren && ER.hasAttribute(context, "dangerouslySetInnerHTML", attributes, initialScope)) { + if (hasChildren && ER.hasAttribute(context, dangerouslySetInnerHTML, attributes, initialScope)) { context.report({ messageId: "noDangerouslySetInnerhtmlWithChildren", node, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts index b8d070d1af..5b18df3dde 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts @@ -29,15 +29,16 @@ export default createRule<[], MessageID>({ defaultOptions: [], }); +const dangerouslySetInnerHTML = "dangerouslySetInnerHTML"; + export function create(context: RuleContext): RuleListener { - if (!context.sourceCode.text.includes("dangerouslySetInnerHTML")) return {}; + if (!context.sourceCode.text.includes(dangerouslySetInnerHTML)) return {}; return { JSXElement(node) { - const attributes = node.openingElement.attributes; const attribute = ER.getAttribute( context, - "dangerouslySetInnerHTML", - attributes, + dangerouslySetInnerHTML, + node.openingElement.attributes, context.sourceCode.getScope(node), ); if (attribute == null) return; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts index 9a71073330..1e7fedb589 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts @@ -28,19 +28,21 @@ export default createRule<[], MessageID>({ defaultOptions: [], }); +const findDOMNode = "findDOMNode"; + export function create(context: RuleContext): RuleListener { - if (!context.sourceCode.text.includes("findDOMNode")) return {}; + if (!context.sourceCode.text.includes(findDOMNode)) return {}; return { CallExpression(node) { const { callee } = node; switch (callee.type) { case T.Identifier: - if (callee.name === "findDOMNode") { + if (callee.name === findDOMNode) { context.report({ messageId: "noFindDomNode", node }); } return; case T.MemberExpression: - if (callee.property.type === T.Identifier && callee.property.name === "findDOMNode") { + if (callee.property.type === T.Identifier && callee.property.name === findDOMNode) { context.report({ messageId: "noFindDomNode", node }); } return; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts index d8cfc8a5b8..d3697a55df 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts @@ -28,19 +28,21 @@ export default createRule<[], MessageID>({ defaultOptions: [], }); +const flushSync = "flushSync"; + export function create(context: RuleContext): RuleListener { - if (!context.sourceCode.text.includes("flushSync")) return {}; + if (!context.sourceCode.text.includes(flushSync)) return {}; return { CallExpression(node) { const { callee } = node; switch (callee.type) { case T.Identifier: - if (callee.name === "flushSync") { + if (callee.name === flushSync) { context.report({ messageId: "noFlushSync", node }); } return; case T.MemberExpression: - if (callee.property.type === T.Identifier && callee.property.name === "flushSync") { + if (callee.property.type === T.Identifier && callee.property.name === flushSync) { context.report({ messageId: "noFlushSync", node }); } return; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts index 72747bf2d1..d3a6a7e23e 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts @@ -34,8 +34,10 @@ export default createRule<[], MessageID>({ defaultOptions: [], }); +const hydrate = "hydrate"; + export function create(context: RuleContext): RuleListener { - if (!context.sourceCode.text.includes("hydrate")) return {}; + if (!context.sourceCode.text.includes(hydrate)) return {}; const settings = getSettingsFromContext(context); if (compare(settings.version, "18.0.0", "<")) return {}; @@ -56,7 +58,7 @@ export function create(context: RuleContext): RuleListener { case node.callee.type === T.MemberExpression && node.callee.object.type === T.Identifier && node.callee.property.type === T.Identifier - && node.callee.property.name === "hydrate" + && node.callee.property.name === hydrate && reactDomNames.has(node.callee.object.name): context.report({ messageId: "noHydrate", @@ -73,7 +75,7 @@ export function create(context: RuleContext): RuleListener { switch (specifier.type) { case T.ImportSpecifier: if (specifier.imported.type !== T.Identifier) continue; - if (specifier.imported.name === "hydrate") { + if (specifier.imported.name === hydrate) { hydrateNames.add(specifier.local.name); } continue; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-render-return-value.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-render-return-value.ts index f682d1421f..d160c65beb 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-render-return-value.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-render-return-value.ts @@ -1,7 +1,6 @@ import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; -import * as AST from "@eslint-react/ast"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { createRule } from "../utils"; @@ -18,7 +17,7 @@ const banParentTypes = [ T.ReturnStatement, T.ArrowFunctionExpression, T.AssignmentExpression, -] as const; +]; export default createRule<[], MessageID>({ meta: { @@ -28,7 +27,7 @@ export default createRule<[], MessageID>({ [Symbol.for("rule_features")]: RULE_FEATURES, }, messages: { - noRenderReturnValue: "Do not depend on the return value from '{{objectName}}.render'.", + noRenderReturnValue: "Do not depend on the return value from 'ReactDOM.render'.", }, schema: [], }, @@ -38,34 +37,50 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + const reactDomNames = new Set(["ReactDOM", "ReactDom"]); + const renderNames = new Set(); + return { CallExpression(node) { - const { callee, parent } = node; - if (callee.type !== T.MemberExpression) { - return; - } - if (callee.object.type !== T.Identifier) { - return; + switch (true) { + case node.callee.type === T.Identifier + && renderNames.has(node.callee.name) + && banParentTypes.includes(node.parent.type): + context.report({ + messageId: "noRenderReturnValue", + node, + }); + return; + case node.callee.type === T.MemberExpression + && node.callee.object.type === T.Identifier + && node.callee.property.type === T.Identifier + && node.callee.property.name === "render" + && reactDomNames.has(node.callee.object.name) + && banParentTypes.includes(node.parent.type): + context.report({ + messageId: "noRenderReturnValue", + node, + }); + return; } - if (!("name" in callee.object)) { - return; - } - const objectName = callee.object.name; - if ( - objectName.toLowerCase() !== "reactdom" - || callee.property.type !== T.Identifier - || callee.property.name !== "render" - || !AST.isOneOf(banParentTypes)(parent) - ) { - return; + }, + ImportDeclaration(node) { + const [baseSource] = node.source.value.split("/"); + if (baseSource !== "react-dom") return; + for (const specifier of node.specifiers) { + switch (specifier.type) { + case T.ImportSpecifier: + if (specifier.imported.type !== T.Identifier) continue; + if (specifier.imported.name === "render") { + renderNames.add(specifier.local.name); + } + continue; + case T.ImportDefaultSpecifier: + case T.ImportNamespaceSpecifier: + reactDomNames.add(specifier.local.name); + continue; + } } - context.report({ - messageId: "noRenderReturnValue", - node, - data: { - objectName, - }, - }); }, }; } diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts index 7659c9be90..cf02400aaf 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts @@ -39,7 +39,7 @@ export function create(context: RuleContext): RuleListener { const settings = getSettingsFromContext(context); if (compare(settings.version, "18.0.0", "<")) return {}; - const reactDomNames = new Set(); + const reactDomNames = new Set(["ReactDOM", "ReactDom"]); const renderNames = new Set(); return { diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts index dca8057787..589884eef6 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts @@ -40,8 +40,7 @@ export function create(context: RuleContext): RuleListener { if (node.name.type !== T.JSXIdentifier || node.value == null) { return; } - const attributeName = ER.getAttributeName(context, node); - const attributeValue = ER.getAttributeValue(context, node, attributeName); + const attributeValue = ER.getAttributeValue(context, node, ER.getAttributeName(context, node)); if (attributeValue.kind === "none" || typeof attributeValue.value !== "string") return; if (RE.JAVASCRIPT_PROTOCOL.test(attributeValue.value)) { context.report({ diff --git a/packages/utilities/kit/src/RE.ts b/packages/utilities/kit/src/RE.ts index 902eb0784d..ffe4e3d955 100644 --- a/packages/utilities/kit/src/RE.ts +++ b/packages/utilities/kit/src/RE.ts @@ -1,3 +1,8 @@ +/** + * Regular expressions for matching a HTML tag name + */ +export const HTML_TAG = /^[a-z][^-]*$/u; + /** * Regular expression for matching a TypeScript file extension. */