Skip to content

Commit 68b38a5

Browse files
authored
feat: add react-x/no-misused-capture-owner-stack rule, closes #1049 (#1047)
1 parent e43fef6 commit 68b38a5

File tree

16 files changed

+352
-2
lines changed

16 files changed

+352
-2
lines changed

apps/website/content/docs/rules/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"no-missing-component-display-name",
3333
"no-missing-context-display-name",
3434
"no-missing-key",
35+
"no-misused-capture-owner-stack",
3536
"no-nested-component-definitions",
3637
"no-prop-types",
3738
"no-redundant-should-component-update",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Linter rules can have false positives, false negatives, and some rules are depen
5555
| [`no-missing-component-display-name`](./no-missing-component-display-name) | 0️⃣ | | Enforces that all components have a `displayName` which can be used in devtools | |
5656
| [`no-missing-context-display-name`](./no-missing-context-display-name) | 0️⃣ | | Enforces that all contexts have a `displayName` which can be used in devtools | |
5757
| [`no-missing-key`](./no-missing-key) | 2️⃣ | | Disallow missing `key` on items in list rendering | |
58+
| [`no-misused-capture-owner-stack`](./no-misused-capture-owner-stack) | 2️⃣ | | Prevents incorrect usage of `captureOwnerStack` | |
5859
| [`no-nested-component-definitions`](./no-nested-component-definitions) | 2️⃣ | | Disallow nesting component definitions inside other components | |
5960
| [`no-prop-types`](./no-prop-types) | 2️⃣ | | Disallow `propTypes` in favor of TypeScript or another type-checking solution | |
6061
| [`no-redundant-should-component-update`](./no-redundant-should-component-update) | 2️⃣ | | Disallow `shouldComponentUpdate` when extending `React.PureComponent` | |

apps/website/source.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ export default defineConfig({
2626
],
2727
},
2828
remarkPlugins: [
29-
remarkMermaid,
30-
remarkInstall,
3129
[remarkDocGen, { generators: [] }],
30+
remarkInstall,
31+
remarkMermaid,
3232
],
3333
},
3434
});

packages/core/docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
- [ComponentFlag](variables/ComponentFlag.md)
3838
- [ComponentPhaseRelevance](variables/ComponentPhaseRelevance.md)
3939
- [DEFAULT\_COMPONENT\_DETECTION\_HINT](variables/DEFAULT_COMPONENT_DETECTION_HINT.md)
40+
- [isCaptureOwnerStack](variables/isCaptureOwnerStack.md)
41+
- [isCaptureOwnerStackCall](variables/isCaptureOwnerStackCall.md)
4042
- [isChildrenCount](variables/isChildrenCount.md)
4143
- [isChildrenCountCall](variables/isChildrenCountCall.md)
4244
- [isChildrenForEach](variables/isChildrenForEach.md)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[**@eslint-react/core**](../README.md)
2+
3+
***
4+
5+
[@eslint-react/core](../README.md) / isCaptureOwnerStack
6+
7+
# Variable: isCaptureOwnerStack
8+
9+
> `const` **isCaptureOwnerStack**: `ReturnType`
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[**@eslint-react/core**](../README.md)
2+
3+
***
4+
5+
[@eslint-react/core](../README.md) / isCaptureOwnerStackCall
6+
7+
# Variable: isCaptureOwnerStackCall
8+
9+
> `const` **isCaptureOwnerStackCall**: `ReturnType`

packages/core/src/utils/is-react-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function isReactAPICall(arg0: string, arg1?: string) {
2020
: isCallFromReactObject(arg0, arg1);
2121
}
2222

23+
export const isCaptureOwnerStack = isReactAPI("captureOwnerStack");
2324
export const isChildrenCount = isReactAPI("Children", "count");
2425
export const isChildrenForEach = isReactAPI("Children", "forEach");
2526
export const isChildrenMap = isReactAPI("Children", "map");
@@ -32,6 +33,7 @@ export const isCreateRef = isReactAPI("createRef");
3233
export const isForwardRef = isReactAPI("forwardRef");
3334
export const isMemo = isReactAPI("memo");
3435

36+
export const isCaptureOwnerStackCall = isReactAPICall("captureOwnerStack");
3537
export const isChildrenCountCall = isReactAPICall("Children", "count");
3638
export const isChildrenForEachCall = isReactAPICall("Children", "forEach");
3739
export const isChildrenMapCall = isReactAPICall("Children", "map");

packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const rules = {
2727
"react-x/no-forward-ref": "warn",
2828
"react-x/no-implicit-key": "warn",
2929
"react-x/no-missing-key": "error",
30+
"react-x/no-misused-capture-owner-stack": "error",
3031
"react-x/no-nested-component-definitions": "error",
3132
"react-x/no-prop-types": "error",
3233
"react-x/no-redundant-should-component-update": "error",

packages/plugins/eslint-plugin-react-x/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import noLeakedConditionalRendering from "./rules/no-leaked-conditional-renderin
3131
import noMissingComponentDisplayName from "./rules/no-missing-component-display-name";
3232
import noMissingContextDisplayName from "./rules/no-missing-context-display-name";
3333
import noMissingKey from "./rules/no-missing-key";
34+
import noMisusedCaptureOwnerStack from "./rules/no-misused-capture-owner-stack";
3435
import noNestedComponentDefinitions from "./rules/no-nested-component-definitions";
3536
import noPropTypes from "./rules/no-prop-types";
3637
import noRedundantShouldComponentUpdate from "./rules/no-redundant-should-component-update";
@@ -88,6 +89,7 @@ export const plugin = {
8889
"no-missing-component-display-name": noMissingComponentDisplayName,
8990
"no-missing-context-display-name": noMissingContextDisplayName,
9091
"no-missing-key": noMissingKey,
92+
"no-misused-capture-owner-stack": noMisusedCaptureOwnerStack,
9193
"no-nested-component-definitions": noNestedComponentDefinitions,
9294
"no-prop-types": noPropTypes,
9395
"no-redundant-should-component-update": noRedundantShouldComponentUpdate,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
title: no-misused-capture-owner-stack
3+
---
4+
5+
**Full Name in `eslint-plugin-react-x@beta`**
6+
7+
```plain copy
8+
react-x/no-misused-capture-owner-stack
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin@beta`**
12+
13+
```plain copy
14+
@eslint-react/no-misused-capture-owner-stack
15+
```
16+
17+
## Description
18+
19+
Prevents incorrect usage of `captureOwnerStack`.
20+
21+
The `captureOwnerStack` is only available in development builds of React and must be:
22+
23+
1. Imported via namespace to avoid direct named imports.
24+
2. Conditionally accessed within an `if (process.env.NODE_ENV !== 'production') {...}` block to prevent execution in production environments.
25+
3. The call of `captureOwnerStack` happened inside of a React controlled function (**not implemented yet**).
26+
27+
## Examples
28+
29+
### Failing
30+
31+
```tsx
32+
// Failing: Using named import directly
33+
import { captureOwnerStack } from "react";
34+
// ^^^^^^^^^^^^^^^^^
35+
// - Don't use named imports of `captureOwnerStack` in files that are bundled for development and production. Use a namespace import instead.
36+
37+
if (process.env.NODE_ENV !== "production") {
38+
const ownerStack = React.captureOwnerStack();
39+
console.log("Owner Stack", ownerStack);
40+
}
41+
```
42+
43+
```tsx
44+
// Failing: Missing environment check
45+
import * as React from "react";
46+
47+
const ownerStack = React.captureOwnerStack();
48+
// ^^^^^^^^^^^^^^^^^^^^^^^^^
49+
// - `captureOwnerStack` should only be used in development builds. Use an environment check to ensure it is not executed in production.
50+
console.log(ownerStack);
51+
```
52+
53+
### Passing
54+
55+
```tsx
56+
// Passing: Correct namespace import with environment check
57+
import * as React from "react";
58+
59+
if (process.env.NODE_ENV !== "production") {
60+
const ownerStack = React.captureOwnerStack();
61+
console.log("Owner Stack", ownerStack);
62+
}
63+
```
64+
65+
## Implementation
66+
67+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts)
68+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.spec.ts)
69+
70+
## Further Reading
71+
72+
- [React: APIs `captureOwnerStack`](https://react.dev/reference/react/captureOwnerStack)
73+
- [The Owner Stack is `null`](https://react.dev/reference/react/captureOwnerStack#the-owner-stack-is-null)
74+
- [`captureOwnerStack` is not available](https://react.dev/reference/react/captureOwnerStack#captureownerstack-is-not-available)

0 commit comments

Comments
 (0)