Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/website/content/docs/rules/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ import { InlineTOC } from "fumadocs-ui/components/inline-toc";
| :--------------------------------------------------------------------------- | :-----: | :--: | :---------------------------------------------------------------------------------------------------------------------------- |
| [`no-leaked-event-listener`](/docs/rules/web-api-no-leaked-event-listener) | 1️⃣ 1️⃣ | | Enforces that every `addEventListener` in a component or custom hook has a corresponding `removeEventListener` |
| [`no-leaked-fetch`](/docs/rules/web-api-no-leaked-fetch) | 1️⃣ 1️⃣ | `🧪` | Enforces that every `fetch` in a component or custom hook has a corresponding `AbortController` abort in the cleanup function |
| [`no-leaked-intersection-observer`](/docs/rules/web-api-no-leaked-intersection-observer) | 1️⃣ 1️⃣ | | Enforces that every `IntersectionObserver` created in a component or custom hook has a corresponding `IntersectionObserver.disconnect()` |
| [`no-leaked-interval`](/docs/rules/web-api-no-leaked-interval) | 1️⃣ 1️⃣ | | Enforces that every `setInterval` in a component or custom hook has a corresponding `clearInterval` |
| [`no-leaked-resize-observer`](/docs/rules/web-api-no-leaked-resize-observer) | 1️⃣ 1️⃣ | | Enforces that every `ResizeObserver` created in a component or custom hook has a corresponding `ResizeObserver.disconnect()` |
| [`no-leaked-timeout`](/docs/rules/web-api-no-leaked-timeout) | 1️⃣ 1️⃣ | | Enforces that every `setTimeout` in a component or custom hook has a corresponding `clearTimeout` |
Expand Down
1 change: 1 addition & 0 deletions apps/website/content/docs/rules/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"---Web API Rules---",
"web-api-no-leaked-event-listener",
"web-api-no-leaked-fetch",
"web-api-no-leaked-intersection-observer",
"web-api-no-leaked-interval",
"web-api-no-leaked-resize-observer",
"web-api-no-leaked-timeout",
Expand Down
10 changes: 10 additions & 0 deletions docs/rule-relations-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,32 @@ Pairs of rules that reference each other:
| `react-web-api/no-leaked-event-listener` | `react-web-api/no-leaked-interval` | Enforces that every `setInterval` in a component or custom hook has a corresponding `clearInterval` |
| `react-web-api/no-leaked-event-listener` | `react-web-api/no-leaked-resize-observer` | Enforces that every `ResizeObserver` created in a component or custom hook has a corresponding `ResizeObserver.disconnect()` |
| `react-web-api/no-leaked-event-listener` | `react-web-api/no-leaked-timeout` | Enforces that every `setTimeout` in a component or custom hook has a corresponding `clearTimeout` |
| `react-web-api/no-leaked-event-listener` | `react-web-api/no-leaked-intersection-observer` | Enforces that every `IntersectionObserver` created in a component or custom hook has a corresponding `IntersectionObserver.disconnect()` |
| `react-web-api/no-leaked-fetch` | `react-web-api/no-leaked-event-listener` | Enforces that every `addEventListener` in a component or custom hook has a corresponding `removeEventListener` |
| `react-web-api/no-leaked-fetch` | `react-web-api/no-leaked-interval` | Enforces that every `setInterval` in a component or custom hook has a corresponding `clearInterval` |
| `react-web-api/no-leaked-fetch` | `react-web-api/no-leaked-resize-observer` | Enforces that every `ResizeObserver` created in a component or custom hook has a corresponding `ResizeObserver.disconnect()` |
| `react-web-api/no-leaked-fetch` | `react-web-api/no-leaked-timeout` | Enforces that every `setTimeout` in a component or custom hook has a corresponding `clearTimeout` |
| `react-web-api/no-leaked-fetch` | `react-web-api/no-leaked-intersection-observer` | Enforces that every `IntersectionObserver` created in a component or custom hook has a corresponding `IntersectionObserver.disconnect()` |
| `react-web-api/no-leaked-interval` | `react-web-api/no-leaked-event-listener` | Enforces that every `addEventListener` in a component or custom hook has a corresponding `removeEventListener` |
| `react-web-api/no-leaked-interval` | `react-web-api/no-leaked-fetch` | Enforces that every `fetch` in a component or custom hook has a corresponding `AbortController` abort in the cleanup function |
| `react-web-api/no-leaked-interval` | `react-web-api/no-leaked-resize-observer` | Enforces that every `ResizeObserver` created in a component or custom hook has a corresponding `ResizeObserver.disconnect()` |
| `react-web-api/no-leaked-interval` | `react-web-api/no-leaked-timeout` | Enforces that every `setTimeout` in a component or custom hook has a corresponding `clearTimeout` |
| `react-web-api/no-leaked-interval` | `react-web-api/no-leaked-intersection-observer` | Enforces that every `IntersectionObserver` created in a component or custom hook has a corresponding `IntersectionObserver.disconnect()` |
| `react-web-api/no-leaked-resize-observer` | `react-web-api/no-leaked-event-listener` | Enforces that every `addEventListener` in a component or custom hook has a corresponding `removeEventListener` |
| `react-web-api/no-leaked-resize-observer` | `react-web-api/no-leaked-fetch` | Enforces that every `fetch` in a component or custom hook has a corresponding `AbortController` abort in the cleanup function |
| `react-web-api/no-leaked-resize-observer` | `react-web-api/no-leaked-interval` | Enforces that every `setInterval` in a component or custom hook has a corresponding `clearInterval` |
| `react-web-api/no-leaked-resize-observer` | `react-web-api/no-leaked-timeout` | Enforces that every `setTimeout` in a component or custom hook has a corresponding `clearTimeout` |
| `react-web-api/no-leaked-resize-observer` | `react-web-api/no-leaked-intersection-observer` | Enforces that every `IntersectionObserver` created in a component or custom hook has a corresponding `IntersectionObserver.disconnect()` |
| `react-web-api/no-leaked-intersection-observer` | `react-web-api/no-leaked-event-listener` | Enforces that every `addEventListener` in a component or custom hook has a corresponding `removeEventListener` |
| `react-web-api/no-leaked-intersection-observer` | `react-web-api/no-leaked-fetch` | Enforces that every `fetch` in a component or custom hook has a corresponding `AbortController` abort in the cleanup function |
| `react-web-api/no-leaked-intersection-observer` | `react-web-api/no-leaked-interval` | Enforces that every `setInterval` in a component or custom hook has a corresponding `clearInterval` |
| `react-web-api/no-leaked-intersection-observer` | `react-web-api/no-leaked-timeout` | Enforces that every `setTimeout` in a component or custom hook has a corresponding `clearTimeout` |
| `react-web-api/no-leaked-intersection-observer` | `react-web-api/no-leaked-resize-observer` | Enforces that every `ResizeObserver` created in a component or custom hook has a corresponding `ResizeObserver.disconnect()` |
| `react-web-api/no-leaked-timeout` | `react-web-api/no-leaked-event-listener` | Enforces that every `addEventListener` in a component or custom hook has a corresponding `removeEventListener` |
| `react-web-api/no-leaked-timeout` | `react-web-api/no-leaked-fetch` | Enforces that every `fetch` in a component or custom hook has a corresponding `AbortController` abort in the cleanup function |
| `react-web-api/no-leaked-timeout` | `react-web-api/no-leaked-interval` | Enforces that every `setInterval` in a component or custom hook has a corresponding `clearInterval` |
| `react-web-api/no-leaked-timeout` | `react-web-api/no-leaked-resize-observer` | Enforces that every `ResizeObserver` created in a component or custom hook has a corresponding `ResizeObserver.disconnect()` |
| `react-web-api/no-leaked-timeout` | `react-web-api/no-leaked-intersection-observer` | Enforces that every `IntersectionObserver` created in a component or custom hook has a corresponding `IntersectionObserver.disconnect()` |
| `react-x/error-boundaries` | `react-x/no-class-component` | Disallows class components except for error boundaries |
| `react-x/error-boundaries` | `react-x/rules-of-hooks` | Enforces the Rules of Hooks |
| `react-x/exhaustive-deps` | `react-x/rules-of-hooks` | Enforces the Rules of Hooks |
Expand Down
3 changes: 2 additions & 1 deletion docs/term-based-rule-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function create(context: RuleContext<MessageID, []>) {
- `no-leaked-timeout`
- `no-leaked-interval`
- `no-leaked-resize-observer`
- `no-leaked-intersection-observer`
- `no-leaked-fetch` (first check)
- `no-leaked-event-listener` (first check)

Expand Down Expand Up @@ -215,7 +216,7 @@ export function create(context: RuleContext<MessageID, []>) {
}
```

**Examples:** `no-leaked-timeout`, `no-leaked-interval`, `no-leaked-event-listener`, `no-leaked-fetch`, `no-leaked-resize-observer`
**Examples:** `no-leaked-timeout`, `no-leaked-interval`, `no-leaked-event-listener`, `no-leaked-fetch`, `no-leaked-resize-observer`, `no-leaked-intersection-observer`

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"react-web-api/no-leaked-event-listener": "warn",
"react-web-api/no-leaked-fetch": "warn",
"react-web-api/no-leaked-interval": "warn",
"react-web-api/no-leaked-intersection-observer": "warn",

Check warning on line 11 in plugins/eslint-plugin-react-web-api/src/configs/recommended.ts

View workflow job for this annotation

GitHub Actions / check

Expected "react-web-api/no-leaked-intersection-observer" to come before "react-web-api/no-leaked-interval"
"react-web-api/no-leaked-resize-observer": "warn",
"react-web-api/no-leaked-timeout": "warn",
} as const satisfies Linter.RulesRecord;
Expand Down
2 changes: 2 additions & 0 deletions plugins/eslint-plugin-react-web-api/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { name, version } from "../package.json";
import noLeakedEventListener from "./rules/no-leaked-event-listener/no-leaked-event-listener";
import noLeakedFetch from "./rules/no-leaked-fetch/no-leaked-fetch";
import noLeakedIntersectionObserver from "./rules/no-leaked-intersection-observer/no-leaked-intersection-observer";
import noLeakedInterval from "./rules/no-leaked-interval/no-leaked-interval";
import noLeakedResizeObserver from "./rules/no-leaked-resize-observer/no-leaked-resize-observer";
import noLeakedTimeout from "./rules/no-leaked-timeout/no-leaked-timeout";
Expand All @@ -15,6 +16,7 @@
"no-leaked-event-listener": noLeakedEventListener,
"no-leaked-fetch": noLeakedFetch,
"no-leaked-interval": noLeakedInterval,
"no-leaked-intersection-observer": noLeakedIntersectionObserver,

Check warning on line 19 in plugins/eslint-plugin-react-web-api/src/plugin.ts

View workflow job for this annotation

GitHub Actions / check

Expected "no-leaked-intersection-observer" to come before "no-leaked-interval"
"no-leaked-resize-observer": noLeakedResizeObserver,
"no-leaked-timeout": noLeakedTimeout,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Extract, isOneOf } from "@eslint-react/ast";
import { type RuleContext } from "@eslint-react/eslint";
import { resolve } from "@eslint-react/var";
import { AST_NODE_TYPES as AST, type TSESTree } from "@typescript-eslint/types";

/**
* Check if a node is a loop statement
* @param node The node to check
* @returns True if the node is a loop
*/
export const isLoop = isOneOf([
AST.DoWhileStatement,
AST.ForInStatement,
AST.ForOfStatement,
AST.ForStatement,
AST.WhileStatement,
]);

/**
* Check if a node is a conditional expression or control flow statement
* @param node The node to check
* @returns True if the node is conditional
*/
export const isConditional = isOneOf([
AST.DoWhileStatement,
AST.ForInStatement,
AST.ForOfStatement,
AST.ForStatement,
AST.WhileStatement,
AST.IfStatement,
AST.SwitchStatement,
AST.LogicalExpression,
AST.ConditionalExpression,
]);

export function isNewIntersectionObserver(node: TSESTree.Node | null) {
if (node?.type !== AST.NewExpression) return false;
const callee = Extract.unwrap(node.callee);
return callee.type === AST.Identifier
&& callee.name === "IntersectionObserver";
}

export function isFromObserver(context: RuleContext, node: TSESTree.Expression): boolean {
switch (true) {
case node.type === AST.Identifier: {
const initNode = resolve(context, node);
const unwrapped = initNode == null ? null : Extract.unwrap(initNode);
return isNewIntersectionObserver(unwrapped);
}
case node.type === AST.MemberExpression:
return isFromObserver(context, node.object);
default:
return false;
}
}
Loading
Loading