Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions apps/website/content/docs/faq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ import { Accordion, Accordions } from "fumadocs-ui/components/accordion";

<Accordion title="Why?">

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

</Accordion>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ export default createRule<[], MessageID>({
defaultOptions: [],
});

const dangerouslySetInnerHTML = "dangerouslySetInnerHTML";

export function create(context: RuleContext<MessageID, []>): 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ export default createRule<[], MessageID>({
defaultOptions: [],
});

const dangerouslySetInnerHTML = "dangerouslySetInnerHTML";

export function create(context: RuleContext<MessageID, []>): 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@ export default createRule<[], MessageID>({
defaultOptions: [],
});

const findDOMNode = "findDOMNode";

export function create(context: RuleContext<MessageID, []>): 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@ export default createRule<[], MessageID>({
defaultOptions: [],
});

const flushSync = "flushSync";

export function create(context: RuleContext<MessageID, []>): 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ export default createRule<[], MessageID>({
defaultOptions: [],
});

const hydrate = "hydrate";

export function create(context: RuleContext<MessageID, []>): 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 {};

Expand All @@ -56,7 +58,7 @@ export function create(context: RuleContext<MessageID, []>): 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",
Expand All @@ -73,7 +75,7 @@ export function create(context: RuleContext<MessageID, []>): 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -18,7 +17,7 @@ const banParentTypes = [
T.ReturnStatement,
T.ArrowFunctionExpression,
T.AssignmentExpression,
] as const;
];

export default createRule<[], MessageID>({
meta: {
Expand All @@ -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: [],
},
Expand All @@ -38,34 +37,50 @@ export default createRule<[], MessageID>({
});

export function create(context: RuleContext<MessageID, []>): RuleListener {
const reactDomNames = new Set<string>(["ReactDOM", "ReactDom"]);
const renderNames = new Set<string>();

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,
},
});
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
const settings = getSettingsFromContext(context);
if (compare(settings.version, "18.0.0", "<")) return {};

const reactDomNames = new Set<string>();
const reactDomNames = new Set<string>(["ReactDOM", "ReactDom"]);
const renderNames = new Set<string>();

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ export function create(context: RuleContext<MessageID, []>): 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({
Expand Down
5 changes: 5 additions & 0 deletions packages/utilities/kit/src/RE.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down
Loading