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.
*/