Skip to content

Commit 141aa5d

Browse files
committed
Improve comments and code style in x and dom rules
The changes involve adding, improving, and clarifying comments throughout various React ESLint rules, making the code more maintainable and easier to understand. This includes: 1. Adding descriptive comments to explain complex logic 2. Clarifying the purpose of various conditions and checks 3. Making code intentions more explicit 4. Improving parameter and variable descriptions 5. Documenting component lifecycle hooks and state management The bulk of the changes are in adding comments to provide better context while leaving the actual logic unchanged. This will help developers better understand the rules' behaviors and make future maintenance easier.
1 parent 49dbee5 commit 141aa5d

File tree

53 files changed

+629
-149
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+629
-149
lines changed

apps/website/content/docs/rules/overview.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ full: true
2727

2828
| Rule || 🌟 | Description | `react` |
2929
| :----------------------------------------------------------------------------------- | :-- | :-------: | :-------------------------------------------------------------------------------------------------- | :------: |
30-
| [`jsx-key-before-spread`](./jsx-key-before-spread) | 1️⃣ | | Enforces that the `key` attribute is placed before the spread attribute in JSX elements | |
30+
| [`jsx-key-before-spread`](./jsx-key-before-spread) | 1️⃣ | | Enforces that the 'key' prop is placed before the spread prop in JSX elements | |
3131
| [`jsx-no-comment-textnodes`](./jsx-no-comment-textnodes) | 1️⃣ | | Prevents comments from being inserted as text nodes | |
3232
| [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ | | Disallow duplicate props in JSX elements | |
3333
| [`jsx-no-iife`](./jsx-no-iife) | 0️⃣ | `🧪` | Disallows `IIFE` in JSX elements | |

packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml-with-children.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,39 @@ export default createRule<[], MessageID>({
3232
const DSIH = "dangerouslySetInnerHTML";
3333

3434
/**
35-
* Checks if a JSX child node is considered significant (i.e., not just whitespace for formatting).
36-
* @param node The JSX child node to check.
37-
* @returns `true` if the node is significant, `false` otherwise.
35+
* Checks if a JSX child node is considered significant (i.e., not just whitespace for formatting)
36+
* @param node The JSX child node to check
37+
* @returns `true` if the node is significant, `false` otherwise
3838
*/
3939
function isSignificantChildren(node: TSESTree.JSXElement["children"][number]) {
40+
// Any node that is not plain text is considered significant
4041
if (!isJsxText(node)) {
4142
return true;
4243
}
4344
// A JSXText node is insignificant if it's purely whitespace and contains a newline,
4445
// which is a common pattern for formatting.
4546
const isFormattingWhitespace = node.raw.trim() === "" && node.raw.includes("\n");
4647

48+
// The node is significant if it's not just formatting whitespace
4749
return !isFormattingWhitespace;
4850
}
4951

5052
export function create(context: RuleContext<MessageID, []>): RuleListener {
51-
// Fast path: skip if `dangerouslySetInnerHTML` is not present in the file
53+
// Fast path: if the file doesn't contain `dangerouslySetInnerHTML`, we don't need to do anything
5254
if (!context.sourceCode.text.includes(DSIH)) {
5355
return {};
5456
}
5557

5658
return {
5759
JSXElement(node) {
5860
const findJsxAttribute = getJsxAttribute(context, node);
61+
// Check if the element has the 'dangerouslySetInnerHTML' prop. If not, we can stop
5962
if (findJsxAttribute(DSIH) == null) return;
63+
// Check for a 'children' prop or actual child nodes that are not just whitespace
6064
const childrenPropOrNode = findJsxAttribute("children") ?? node.children.find(isSignificantChildren);
65+
// If no children are found, the rule passes
6166
if (childrenPropOrNode == null) return;
67+
// If both 'dangerouslySetInnerHTML' and children are present, report an error
6268
context.report({
6369
messageId: "noDangerouslySetInnerhtmlWithChildren",
6470
node: childrenPropOrNode,

packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
3434
// Fast path: skip if `dangerouslySetInnerHTML` is not present in the file
3535
if (!context.sourceCode.text.includes(DSIH)) return {};
3636
return {
37+
// This function runs for each JSX element
3738
JSXElement(node) {
39+
// Check if the element has the 'dangerouslySetInnerHTML' prop
3840
const dsihProp = getJsxAttribute(context, node)(DSIH);
41+
// If the prop is not found, do nothing
3942
if (dsihProp == null) return;
43+
// If the prop is found, report an error
4044
context.report({
4145
messageId: "noDangerouslySetInnerhtml",
4246
node: dsihProp,

packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
3737
CallExpression(node) {
3838
const { callee } = node;
3939
switch (callee.type) {
40+
// Handles cases like `findDOMNode()`.
4041
case T.Identifier:
4142
if (callee.name === findDOMNode) {
4243
context.report({ messageId: "noFindDomNode", node });
4344
}
4445
return;
46+
// Handles cases like `ReactDOM.findDOMNode()`.
4547
case T.MemberExpression:
4648
if (callee.property.type === T.Identifier && callee.property.name === findDOMNode) {
4749
context.report({ messageId: "noFindDomNode", node });

packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
3737
CallExpression(node) {
3838
const { callee } = node;
3939
switch (callee.type) {
40+
// Handle direct calls like `flushSync()`
4041
case T.Identifier:
4142
if (callee.name === flushSync) {
4243
context.report({ messageId: "noFlushSync", node });
4344
}
4445
return;
46+
// Handle member access calls like `ReactDOM.flushSync()`
4547
case T.MemberExpression:
4648
if (callee.property.type === T.Identifier && callee.property.name === flushSync) {
4749
context.report({ messageId: "noFlushSync", node });

packages/plugins/eslint-plugin-react-dom/src/rules/no-hydrate.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
4040
// Fast path: skip if `hydrate` is not present in the file
4141
if (!context.sourceCode.text.includes(hydrate)) return {};
4242
const settings = getSettingsFromContext(context);
43+
// This rule only applies to React 18.0.0 and later.
4344
if (compare(settings.version, "18.0.0", "<")) return {};
4445

45-
const reactDomNames = new Set<string>();
46-
const hydrateNames = new Set<string>();
46+
// Keep track of imports from 'react-dom'.
47+
const reactDomNames = new Set<string>(); // For `import ReactDOM from 'react-dom'`
48+
const hydrateNames = new Set<string>(); // For `import { hydrate } from 'react-dom'`
4749

4850
return {
4951
CallExpression(node) {
5052
switch (true) {
53+
// Case 1: Direct call to `hydrate()`.
5154
case node.callee.type === T.Identifier
5255
&& hydrateNames.has(node.callee.name):
5356
context.report({
@@ -56,6 +59,7 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
5659
fix: getFix(context, node),
5760
});
5861
return;
62+
// Case 2: Call on a `react-dom` import, like `ReactDOM.hydrate()`
5963
case node.callee.type === T.MemberExpression
6064
&& node.callee.object.type === T.Identifier
6165
&& node.callee.property.type === T.Identifier
@@ -71,15 +75,18 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
7175
},
7276
ImportDeclaration(node) {
7377
const [baseSource] = node.source.value.split("/");
78+
// We only care about imports from 'react-dom'
7479
if (baseSource !== "react-dom") return;
7580
for (const specifier of node.specifiers) {
7681
switch (specifier.type) {
82+
// `import { hydrate } from 'react-dom'`
7783
case T.ImportSpecifier:
7884
if (specifier.imported.type !== T.Identifier) continue;
7985
if (specifier.imported.name === hydrate) {
8086
hydrateNames.add(specifier.local.name);
8187
}
8288
continue;
89+
// `import ReactDOM from 'react-dom'` or `import * as ReactDOM from 'react-dom'`
8390
case T.ImportDefaultSpecifier:
8491
case T.ImportNamespaceSpecifier:
8592
reactDomNames.add(specifier.local.name);
@@ -95,8 +102,12 @@ function getFix(context: RuleContext, node: TSESTree.CallExpression) {
95102
return (fixer: RuleFixer) => {
96103
const [arg0, arg1] = node.arguments;
97104
if (arg0 == null || arg1 == null) return null;
105+
// The fix consists of two parts:
98106
return [
107+
// 1. Add the new import for `hydrateRoot`
99108
fixer.insertTextBefore(context.sourceCode.ast, 'import { hydrateRoot } from "react-dom/client";\n'),
109+
// 2. Replace `hydrate(element, container)` with `hydrateRoot(container, element)`
110+
// Note that the arguments are swapped
100111
fixer.replaceText(node, `hydrateRoot(${getText(arg1)}, ${getText(arg0)})`),
101112
];
102113
};

packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,26 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
4141

4242
return {
4343
JSXElement(node) {
44+
// Resolve the JSX element to its corresponding DOM element type
45+
// If it's not a '<button>', skip it
4446
if (resolver.resolve(node).domElementType !== "button") {
4547
return;
4648
}
4749

50+
// Check if the 'type' attribute already exists on the button element
4851
if (getJsxAttribute(context, node)("type") != null) {
4952
return;
5053
}
5154

55+
// If the 'type' attribute is missing, report an error
5256
context.report({
5357
messageId: "noMissingButtonType",
5458
node: node.openingElement,
59+
// Provide suggestions to automatically fix the issue
5560
suggest: BUTTON_TYPES.map((type): RuleSuggest<MessageID> => ({
5661
messageId: "addButtonType",
5762
data: { type },
63+
// The fix function inserts the 'type' attribute with a suggested value
5864
fix: (fixer) => fixer.insertTextAfter(node.openingElement.name, ` type="${type}"`),
5965
})),
6066
});

packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
4141
return {
4242
JSXElement(node) {
4343
const { domElementType } = resolver.resolve(node);
44-
// If the element is not an iframe, we don't need to do anything.
44+
// If the element is not an iframe, we don't need to do anything
4545
if (domElementType !== "iframe") return;
4646

4747
// Find the 'sandbox' prop on the iframe element.
4848
const sandboxProp = getJsxAttribute(context, node)("sandbox");
4949

50-
// If the 'sandbox' prop is missing, report an error.
50+
// If the 'sandbox' prop is missing, report an error
5151
if (sandboxProp == null) {
5252
context.report({
5353
messageId: "noMissingIframeSandbox",
@@ -56,20 +56,20 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
5656
messageId: "addIframeSandbox",
5757
data: { value: "" },
5858
fix(fixer) {
59-
// Suggest adding a 'sandbox' attribute.
59+
// Suggest adding a 'sandbox' attribute
6060
return fixer.insertTextAfter(node.openingElement.name, ` sandbox=""`);
6161
},
6262
}],
6363
});
6464
return;
6565
}
6666

67-
// Resolve the value of the 'sandbox' attribute.
67+
// Resolve the value of the 'sandbox' attribute
6868
const sandboxValue = resolveJsxAttributeValue(context, sandboxProp);
69-
// If the value is a static string, the prop is correctly used.
69+
// If the value is a static string, the prop is correctly used
7070
if (typeof sandboxValue.toStatic("sandbox") === "string") return;
7171

72-
// If the value is not a static string (e.g., a variable), report an error.
72+
// If the value is not a static string (e.g., a variable), report an error
7373
context.report({
7474
messageId: "noMissingIframeSandbox",
7575
node: sandboxValue.node ?? sandboxProp,
@@ -78,9 +78,9 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
7878
messageId: "addIframeSandbox",
7979
data: { value: "" },
8080
fix(fixer) {
81-
// Do not try to fix spread attributes.
81+
// Do not try to fix spread attributes
8282
if (sandboxValue.kind.startsWith("spread")) return null;
83-
// Suggest replacing the prop with a valid one.
83+
// Suggest replacing the prop with a valid one
8484
return fixer.replaceText(sandboxProp, `sandbox=""`);
8585
},
8686
},

packages/plugins/eslint-plugin-react-dom/src/rules/no-render-return-value.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const RULE_FEATURES = [] as const satisfies RuleFeature[];
1111

1212
export type MessageID = CamelCase<typeof RULE_NAME>;
1313

14+
// Parent AST node types that indicate the return value of `ReactDOM.render` is being used
1415
const banParentTypes = [
1516
T.VariableDeclarator,
1617
T.Property,
@@ -37,25 +38,31 @@ export default createRule<[], MessageID>({
3738
});
3839

3940
export function create(context: RuleContext<MessageID, []>): RuleListener {
41+
// Sets to track imported names for 'ReactDOM' and 'render'
4042
const reactDomNames = new Set<string>(["ReactDOM", "ReactDom"]);
4143
const renderNames = new Set<string>();
4244

4345
return {
46+
// Checks for calls to 'render' or 'ReactDOM.render' and reports if their return value is used
4447
CallExpression(node) {
4548
switch (true) {
49+
// Handles direct calls to 'render' (e.g., from `import { render } from 'react-dom'`)
4650
case node.callee.type === T.Identifier
4751
&& renderNames.has(node.callee.name)
52+
// Check if the return value is being used
4853
&& banParentTypes.includes(node.parent.type):
4954
context.report({
5055
messageId: "noRenderReturnValue",
5156
node,
5257
});
5358
return;
59+
// Handles member expression calls like 'ReactDOM.render'
5460
case node.callee.type === T.MemberExpression
5561
&& node.callee.object.type === T.Identifier
5662
&& node.callee.property.type === T.Identifier
5763
&& node.callee.property.name === "render"
5864
&& reactDomNames.has(node.callee.object.name)
65+
// Check if the return value is being used
5966
&& banParentTypes.includes(node.parent.type):
6067
context.report({
6168
messageId: "noRenderReturnValue",
@@ -64,17 +71,21 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
6471
return;
6572
}
6673
},
74+
// Tracks imports from 'react-dom' to identify 'ReactDOM' and 'render' related identifiers
6775
ImportDeclaration(node) {
76+
// Check if the import source is 'react-dom' or 'react-dom/client'
6877
const [baseSource] = node.source.value.split("/");
6978
if (baseSource !== "react-dom") return;
7079
for (const specifier of node.specifiers) {
7180
switch (specifier.type) {
81+
// Handles named imports like `import { render } from 'react-dom'`
7282
case T.ImportSpecifier:
7383
if (specifier.imported.type !== T.Identifier) continue;
7484
if (specifier.imported.name === "render") {
7585
renderNames.add(specifier.local.name);
7686
}
7787
continue;
88+
// Handles default or namespace imports like `import ReactDOM from 'react-dom'`
7889
case T.ImportDefaultSpecifier:
7990
case T.ImportNamespaceSpecifier:
8091
reactDomNames.add(specifier.local.name);

packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,19 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
3838
// Fast path: skip if `render` is not present in the file
3939
if (!context.sourceCode.text.includes("render")) return {};
4040
const settings = getSettingsFromContext(context);
41+
// This rule only applies to React 18.0.0 and later
4142
if (compare(settings.version, "18.0.0", "<")) return {};
4243

44+
// Tracks imported names for ReactDOM, e.g., `ReactDOM` or `ReactDom`.
4345
const reactDomNames = new Set<string>(["ReactDOM", "ReactDom"]);
46+
// Tracks imported names for the `render` function
4447
const renderNames = new Set<string>();
4548

4649
return {
50+
// Visitor for call expressions, e.g., render() or ReactDOM.render()
4751
CallExpression(node) {
4852
switch (true) {
53+
// Case 1: Direct call to 'render', e.g., from `import { render } from 'react-dom'`
4954
case node.callee.type === T.Identifier
5055
&& renderNames.has(node.callee.name):
5156
context.report({
@@ -54,6 +59,7 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
5459
fix: getFix(context, node),
5560
});
5661
return;
62+
// Case 2: Member expression call, e.g., `ReactDOM.render()`
5763
case node.callee.type === T.MemberExpression
5864
&& node.callee.object.type === T.Identifier
5965
&& node.callee.property.type === T.Identifier
@@ -69,15 +75,18 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
6975
},
7076
ImportDeclaration(node) {
7177
const [baseSource] = node.source.value.split("/");
78+
// Only consider imports from 'react-dom'
7279
if (baseSource !== "react-dom") return;
7380
for (const specifier of node.specifiers) {
7481
switch (specifier.type) {
82+
// Handles: import { render } from 'react-dom'
7583
case T.ImportSpecifier:
7684
if (specifier.imported.type !== T.Identifier) continue;
7785
if (specifier.imported.name === "render") {
7886
renderNames.add(specifier.local.name);
7987
}
8088
continue;
89+
// Handles: import ReactDOM from 'react-dom' or import * as ReactDOM from 'react-dom'
8190
case T.ImportDefaultSpecifier:
8291
case T.ImportNamespaceSpecifier:
8392
reactDomNames.add(specifier.local.name);
@@ -88,13 +97,22 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
8897
};
8998
}
9099

100+
/**
101+
* Provides a fixer function to replace `render(app, container)` with `createRoot(container).render(app)`
102+
* @param context The rule context
103+
* @param node The `CallExpression` node to fix
104+
* @returns A fixer function or null if the fix cannot be applied
105+
*/
91106
function getFix(context: RuleContext, node: TSESTree.CallExpression) {
92107
const getText = (n: TSESTree.Node) => context.sourceCode.getText(n);
93108
return (fixer: RuleFixer) => {
109+
// `render` takes two arguments: component and container
94110
const [arg0, arg1] = node.arguments;
95111
if (arg0 == null || arg1 == null) return null;
96112
return [
113+
// Add `import { createRoot } from "react-dom/client";` at the top of the file
97114
fixer.insertTextBefore(context.sourceCode.ast, 'import { createRoot } from "react-dom/client";\n'),
115+
// Replace `render(arg0, arg1)` with `createRoot(arg1).render(arg0)`
98116
fixer.replaceText(node, `createRoot(${getText(arg1)}).render(${getText(arg0)})`),
99117
];
100118
};

0 commit comments

Comments
 (0)