eslint-plugin-react-web-api
Version:
ESLint React's ESLint plugin for interacting with Web APIs
1,409 lines (1,390 loc) • 42 kB
JavaScript
import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
import * as core from "@eslint-react/core";
import birecord from "birecord";
import { P, isMatching, match } from "ts-pattern";
import { ESLintUtils } from "@typescript-eslint/utils";
import { Check, Compare, Extract, Traverse } from "@eslint-react/ast";
import "@eslint-react/eslint";
import { isAssignmentTargetEqual, isInitializedFromReactNative, isValueEqual, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
import { AST_NODE_TYPES } from "@typescript-eslint/types";
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) {
__defProp(target, name, {
get: all[name],
enumerable: true
});
}
if (!no_symbols) {
__defProp(target, Symbol.toStringTag, { value: "Module" });
}
return target;
};
//#endregion
//#region package.json
var name$1 = "eslint-plugin-react-web-api";
var version = "5.18.1";
//#endregion
//#region src/types/component-phase.ts
const ComponentPhaseRelevance = birecord({ setup: "cleanup" });
function getPhaseKindOfFunction(node) {
return match(node).when(core.isUseEffectSetupCallback, () => "setup").when(core.isUseEffectCleanupCallback, () => "cleanup").otherwise(() => null);
}
//#endregion
//#region src/utils/create-rule.ts
function getDocsUrl(ruleName) {
return `https://eslint-react.xyz/docs/rules/web-api-${ruleName}`;
}
const createRule = ESLintUtils.RuleCreator(getDocsUrl);
//#endregion
//#region src/rules/no-leaked-event-listener/lib.ts
function getSignalValueExpression(context, node) {
if (node == null) return null;
switch (node.type) {
case AST_NODE_TYPES.Identifier: {
const resolved = resolve(context, node);
const unwrapped = resolved == null ? null : Extract.unwrap(resolved);
if (unwrapped != null && Check.isFunction(unwrapped)) return node;
return getSignalValueExpression(context, unwrapped);
}
case AST_NODE_TYPES.MemberExpression: return node;
default: return null;
}
}
const defaultOptions = {
capture: false,
signal: null
};
function getOptions(context, node) {
function visit(node) {
switch (node.type) {
case AST_NODE_TYPES.Identifier: {
const initNode = resolve(context, node);
if (initNode?.type === AST_NODE_TYPES.ObjectExpression) return visit(initNode);
return defaultOptions;
}
case AST_NODE_TYPES.Literal: return {
...defaultOptions,
capture: Boolean(node.value)
};
case AST_NODE_TYPES.ObjectExpression: {
const vCapture = match(Extract.findProperty(node.properties, "capture")).with(P.nullish, () => false).with({ type: AST_NODE_TYPES.Property }, (prop) => {
const value = prop.value;
switch (value.type) {
case AST_NODE_TYPES.Literal: return Boolean(value.value);
default: return Boolean(getStaticValue(value, context.sourceCode.getScope(node))?.value);
}
}).otherwise(() => false);
const pSignal = Extract.findProperty(node.properties, "signal");
return {
capture: vCapture,
signal: pSignal?.type === AST_NODE_TYPES.Property ? getSignalValueExpression(context, pSignal.value) : null
};
}
default: return defaultOptions;
}
}
return visit(node);
}
//#endregion
//#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
const RULE_NAME$5 = "no-leaked-event-listener";
function getCallKind$5(node) {
const name = Extract.getCalleeName(node);
if (name != null && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(name)) return name;
return "other";
}
function getFunctionKind$2(node) {
return getPhaseKindOfFunction(node) ?? "other";
}
var no_leaked_event_listener_default = createRule({
meta: {
type: "problem",
docs: { description: "Enforces that every 'addEventListener' in a component or custom hook has a corresponding 'removeEventListener'." },
messages: {
expectedRemoveEventListenerInCleanup: "An 'addEventListener' in '{{effectMethodKind}}' should have a corresponding 'removeEventListener' in its cleanup function.",
unexpectedInlineFunction: "A/an '{{eventMethodKind}}' should not have an inline listener function."
},
schema: []
},
name: RULE_NAME$5,
create: create$5,
defaultOptions: []
});
function create$5(context) {
if (!context.sourceCode.text.includes("addEventListener")) return {};
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
const fEntries = [];
const aEntries = [];
const rEntries = [];
const abortedSignals = [];
function isSameObject(a, b) {
switch (true) {
case a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression: return Compare.isEqual(a.object, b.object);
default: return false;
}
}
function isInverseEntry(aEntry, rEntry) {
const { type: aType, callee: aCallee, capture: aCapture, listener: aListener, phase: aPhase } = aEntry;
const { type: rType, callee: rCallee, capture: rCapture, listener: rListener, phase: rPhase } = rEntry;
if (ComponentPhaseRelevance.get(aPhase) !== rPhase) return false;
return isSameObject(aCallee, rCallee) && Compare.isEqual(aListener, rListener) && isValueEqual(context, aType, rType) && aCapture === rCapture;
}
function checkInlineFunction(node, callKind, options) {
const listener = node.arguments.at(1);
if (!Check.isFunction(listener)) return;
if (options.signal != null) return;
context.report({
data: { eventMethodKind: callKind },
messageId: "unexpectedInlineFunction",
node: listener
});
}
return {
[":function"](node) {
const kind = getFunctionKind$2(node);
fEntries.push({
kind,
node
});
},
[":function:exit"]() {
fEntries.pop();
},
["CallExpression"](node) {
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
if (fKind == null) return;
if (!ComponentPhaseRelevance.has(fKind)) return;
const unwrappedCallee = Extract.unwrap(node.callee);
match(getCallKind$5(node)).with("addEventListener", (callKind) => {
if (unwrappedCallee.type === AST_NODE_TYPES.MemberExpression && unwrappedCallee.object.type === AST_NODE_TYPES.Identifier && isInitializedFromReactNative(unwrappedCallee.object.name, context.sourceCode.getScope(node))) return;
const [type, listener, options] = node.arguments;
if (type == null || listener == null) return;
const opts = options == null ? defaultOptions : getOptions(context, options);
const { callee } = node;
checkInlineFunction(node, callKind, opts);
aEntries.push({
...opts,
type,
callee,
listener,
method: "addEventListener",
node,
phase: fKind
});
}).with("removeEventListener", (callKind) => {
const [type, listener, options] = node.arguments;
if (type == null || listener == null) return;
const opts = options == null ? defaultOptions : getOptions(context, options);
const { callee } = node;
checkInlineFunction(node, callKind, opts);
rEntries.push({
...opts,
type,
callee,
listener,
method: "removeEventListener",
node,
phase: fKind
});
}).with("abort", () => {
abortedSignals.push(node.callee);
}).otherwise(() => null);
},
["Program:exit"]() {
for (const aEntry of aEntries) {
if (aEntry.signal != null) continue;
if (rEntries.some((rEntry) => isInverseEntry(aEntry, rEntry))) continue;
switch (aEntry.phase) {
case "setup":
case "cleanup":
context.report({
data: { effectMethodKind: "useEffect" },
messageId: "expectedRemoveEventListenerInCleanup",
node: aEntry.node
});
continue;
}
}
}
};
}
//#endregion
//#region src/rules/no-leaked-fetch/lib.ts
function findProperty(node, name) {
for (const property of node) {
if (property.type === AST_NODE_TYPES.Property && !property.computed && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === name) return property;
if (property.type === AST_NODE_TYPES.SpreadElement && property.argument.type === AST_NODE_TYPES.ObjectExpression) {
const found = findProperty(property.argument.properties, name);
if (found != null) return found;
}
}
return null;
}
function resolveToObjectExpression(context, node) {
node = Extract.unwrap(node);
switch (node.type) {
case AST_NODE_TYPES.ObjectExpression: return node;
case AST_NODE_TYPES.Identifier: {
let resolved = resolve(context, node);
resolved = resolved == null ? null : Extract.unwrap(resolved);
if (resolved?.type === AST_NODE_TYPES.ObjectExpression) return resolved;
return null;
}
default: return null;
}
}
//#endregion
//#region src/rules/no-leaked-fetch/no-leaked-fetch.ts
const RULE_NAME$4 = "no-leaked-fetch";
function getCallKind$4(node) {
switch (Extract.getCalleeName(node)) {
case "fetch": return "fetch";
case "abort": return "abort";
default: return "other";
}
}
function getControllerFromSignal(context, node) {
node = Extract.unwrap(node);
switch (node.type) {
case AST_NODE_TYPES.MemberExpression: return {
controller: node.object,
isParamSignal: false
};
case AST_NODE_TYPES.Identifier: {
const resolved = resolve(context, node);
const resolvedUnwrapped = resolved == null ? null : Extract.unwrap(resolved);
if (resolvedUnwrapped?.type === AST_NODE_TYPES.MemberExpression) return {
controller: resolvedUnwrapped.object,
isParamSignal: false
};
if (resolved != null && Check.isFunction(resolved)) return {
controller: node,
isParamSignal: true
};
return {
controller: null,
isParamSignal: false
};
}
default: return {
controller: null,
isParamSignal: false
};
}
}
function getFetchController(context, node) {
const [, optionsArg] = node.arguments;
if (optionsArg == null) return {
controller: null,
isParamSignal: false
};
const options = resolveToObjectExpression(context, optionsArg);
if (options == null) return {
controller: null,
isParamSignal: false
};
const signalProp = findProperty(options.properties, "signal");
if (signalProp?.type !== AST_NODE_TYPES.Property) return {
controller: null,
isParamSignal: false
};
return getControllerFromSignal(context, signalProp.value);
}
function getAbortController(node) {
const callee = Extract.unwrap(node.callee);
if (callee.type === AST_NODE_TYPES.MemberExpression) return callee.object;
return null;
}
var no_leaked_fetch_default = createRule({
meta: {
type: "problem",
docs: { description: "Enforces that every 'fetch' in a component or custom hook has a corresponding 'AbortController' abort in the cleanup function." },
messages: {
expectedAbortController: "A 'fetch' must be provided with an 'AbortController' for proper cleanup.",
expectedAbortInCleanup: "A 'fetch' started in effect must be aborted with 'AbortController.abort' in the cleanup function."
},
schema: []
},
name: RULE_NAME$4,
create: create$4,
defaultOptions: []
});
function create$4(context) {
if (!context.sourceCode.text.includes("fetch")) return {};
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
const fEntries = [];
const fetchEntries = [];
const abortEntries = [];
return {
[":function"](node) {
const kind = getPhaseKindOfFunction(node) ?? "other";
fEntries.push({
kind,
node
});
},
[":function:exit"]() {
fEntries.pop();
},
["CallExpression"](node) {
const fEntry = fEntries.at(-1);
if (fEntry == null || !ComponentPhaseRelevance.has(fEntry.kind)) return;
switch (getCallKind$4(node)) {
case "fetch": {
if (fEntry.kind !== "setup") return;
const { controller, isParamSignal } = getFetchController(context, node);
fetchEntries.push({
controller,
isParamSignal,
node,
phase: fEntry.kind
});
break;
}
case "abort": {
if (fEntry.kind !== "cleanup") return;
const controller = getAbortController(node);
if (controller == null) break;
abortEntries.push({
controller,
node,
phase: fEntry.kind
});
break;
}
}
},
["Program:exit"]() {
for (const fEntry of fetchEntries) {
const controller = fEntry.controller;
if (controller == null) {
context.report({
messageId: "expectedAbortController",
node: fEntry.node
});
continue;
}
if (fEntry.isParamSignal) continue;
if (!abortEntries.some((aEntry) => isAssignmentTargetEqual(context, aEntry.controller, controller))) context.report({
messageId: "expectedAbortInCleanup",
node: fEntry.node
});
}
}
};
}
//#endregion
//#region ../../.pkgs/eff/dist/index.js
function or(a, b) {
return (data) => a(data) || b(data);
}
/**
* Applies a `pipe` method's variadic arguments to an initial value from left
* to right.
*
* **When to use**
*
* Use to implement a custom `.pipe(...)` method from JavaScript's `arguments`
* object.
*
* **Details**
*
* This helper is intended for implementing `Pipeable.pipe` methods that
* receive JavaScript's `arguments` object. With no functions it returns the
* original value; otherwise it feeds each result into the next function.
*
* **Example** (Implementing a pipe method)
*
* ```ts
* import { Pipeable } from "effect"
*
* class NumberBox {
* constructor(readonly value: number) {}
*
* pipe(..._fns: ReadonlyArray<(value: number) => number>): number {
* return Pipeable.pipeArguments(this.value, arguments) as number
* }
* }
*
* const result = new NumberBox(5).pipe(
* (n) => n + 2,
* (n) => n * 3
* )
* console.log(result) // 21
* ```
*
* @category combinators
* @since 2.0.0
*/
const pipeArguments = (self, args) => {
switch (args.length) {
case 0: return self;
case 1: return args[0](self);
case 2: return args[1](args[0](self));
case 3: return args[2](args[1](args[0](self)));
case 4: return args[3](args[2](args[1](args[0](self))));
case 5: return args[4](args[3](args[2](args[1](args[0](self)))));
case 6: return args[5](args[4](args[3](args[2](args[1](args[0](self))))));
case 7: return args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))));
case 8: return args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))));
case 9: return args[8](args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))))));
default: {
let ret = self;
for (let i = 0, len = args.length; i < len; i++) ret = args[i](ret);
return ret;
}
}
};
/**
* Reusable prototype that implements `Pipeable.pipe`.
*
* **When to use**
*
* Use when classes or object prototypes can reuse this value when they need the
* standard pipe implementation backed by `pipeArguments`.
*
* @category prototypes
* @since 3.15.0
*/
const Prototype = { pipe() {
return pipeArguments(this, arguments);
} };
/**
* Provides a base constructor whose instances implement the standard `Pipeable.pipe`
* method.
*
* **When to use**
*
* Use when you need to define a class that supports Effect-style method
* chaining through `.pipe(...)`.
*
* @category constructors
* @since 3.15.0
*/
const Class = (function() {
function PipeableBase() {}
PipeableBase.prototype = Prototype;
return PipeableBase;
})();
/**
* Provides small helpers for defining and reusing TypeScript functions.
*
* The main helpers are `pipe` and `flow` for left-to-right composition and
* `dual` for APIs that support both direct and pipe-friendly call styles. The
* module also contains small identity, constant, tuple, type-level, and
* memoization helpers used across the library.
*
* @since 2.0.0
*/
/**
* Creates a function that can be called in data-first style or data-last
* (`pipe`-friendly) style.
*
* **When to use**
*
* Use to expose one implementation through both direct and `pipe`-friendly
* call styles.
*
* **Details**
*
* Pass either the arity of the uncurried function or a predicate that decides
* whether the current call is data-first. Arity is the common case. Use a
* predicate when optional arguments make arity ambiguous.
*
* **Example** (Selecting data-first or data-last style by arity)
*
* ```ts
* import { Function, pipe } from "effect"
*
* const sum = Function.dual<
* (that: number) => (self: number) => number,
* (self: number, that: number) => number
* >(2, (self, that) => self + that)
*
* console.log(sum(2, 3)) // 5
* console.log(pipe(2, sum(3))) // 5
* ```
*
* **Example** (Defining overloads with call signatures)
*
* ```ts
* import { Function, pipe } from "effect"
*
* const sum: {
* (that: number): (self: number) => number
* (self: number, that: number): number
* } = Function.dual(2, (self: number, that: number): number => self + that)
*
* console.log(sum(2, 3)) // 5
* console.log(pipe(2, sum(3))) // 5
* ```
*
* **Example** (Selecting data-first or data-last style with a predicate)
*
* ```ts
* import { Function, pipe } from "effect"
*
* const sum = Function.dual<
* (that: number) => (self: number) => number,
* (self: number, that: number) => number
* >(
* (args) => args.length === 2,
* (self, that) => self + that
* )
*
* console.log(sum(2, 3)) // 5
* console.log(pipe(2, sum(3))) // 5
* ```
*
* @category combinators
* @since 2.0.0
*/
const dual = function(arity, body) {
if (typeof arity === "function") return function() {
return arity(arguments) ? body.apply(this, arguments) : ((self) => body(self, ...arguments));
};
switch (arity) {
case 0:
case 1: throw new RangeError(`Invalid arity ${arity}`);
case 2: return function(a, b) {
if (arguments.length >= 2) return body(a, b);
return function(self) {
return body(self, a);
};
};
case 3: return function(a, b, c) {
if (arguments.length >= 3) return body(a, b, c);
return function(self) {
return body(self, a, b);
};
};
default: return function() {
if (arguments.length >= arity) return body.apply(this, arguments);
const args = arguments;
return function(self) {
return body(self, ...args);
};
};
}
};
/**
* Returns its input argument unchanged.
*
* **When to use**
*
* Use to return a value unchanged where a function is required.
*
* **Example** (Returning the same value)
*
* ```ts
* import { identity } from "effect"
* import * as assert from "node:assert"
*
* assert.deepStrictEqual(identity(5), 5)
* ```
*
* @category combinators
* @since 2.0.0
*/
const identity = (a) => a;
/**
* Returns the input value with a different static type.
*
* **When to use**
*
* Use when you need an explicit type-level cast and accept that the value is
* returned unchanged at runtime.
*
* **Gotchas**
*
* This is a type-level cast only; it performs no runtime validation or
* conversion.
*
* @see {@link satisfies} for checking assignability without changing the resulting type
*
* @category utility types
* @since 4.0.0
*/
const cast = identity;
/**
* Creates a zero-argument function that always returns the provided value.
*
* **When to use**
*
* Use when you need a thunk or callback that returns the same value on every
* invocation.
*
* **Example** (Creating a constant thunk)
*
* ```ts
* import { Function } from "effect"
* import * as assert from "node:assert"
*
* const constNull = Function.constant(null)
*
* assert.deepStrictEqual(constNull(), null)
* assert.deepStrictEqual(constNull(), null)
* ```
*
* @category constructors
* @since 2.0.0
*/
const constant = (value) => () => value;
/**
* Returns `true` when called.
*
* **When to use**
*
* Use when you need a thunk that returns `true` on every invocation.
*
* **Example** (Returning true from a thunk)
*
* ```ts
* import { Function } from "effect"
* import * as assert from "node:assert"
*
* assert.deepStrictEqual(Function.constTrue(), true)
* ```
*
* @category constants
* @since 2.0.0
*/
const constTrue = constant(true);
/**
* Returns `false` when called.
*
* **When to use**
*
* Use when you need a thunk that returns `false` on every invocation.
*
* **Example** (Returning false from a thunk)
*
* ```ts
* import { Function } from "effect"
* import * as assert from "node:assert"
*
* assert.deepStrictEqual(Function.constFalse(), false)
* ```
*
* @category constants
* @since 2.0.0
*/
const constFalse = constant(false);
/**
* Returns `null` when called.
*
* **When to use**
*
* Use when you need a thunk that returns `null` on every invocation.
*
* **Example** (Returning null from a thunk)
*
* ```ts
* import { Function } from "effect"
* import * as assert from "node:assert"
*
* assert.deepStrictEqual(Function.constNull(), null)
* ```
*
* @category constants
* @since 2.0.0
*/
const constNull = constant(null);
/**
* Returns `undefined` when called.
*
* **When to use**
*
* Use when you need a thunk that returns `undefined` on every invocation.
*
* **Example** (Returning undefined from a thunk)
*
* ```ts
* import { Function } from "effect"
* import * as assert from "node:assert"
*
* assert.deepStrictEqual(Function.constUndefined(), undefined)
* ```
*
* @category constants
* @since 2.0.0
*/
const constUndefined = constant(void 0);
/**
* Composes two functions, `ab` and `bc` into a single function that takes in an argument `a` of type `A` and returns a result of type `C`.
* The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`.
*
* **When to use**
*
* Use to compose exactly two unary functions into a reusable unary function.
*
* **Example** (Composing two functions)
*
* ```ts
* import { Function } from "effect"
* import * as assert from "node:assert"
*
* const increment = (n: number) => n + 1
* const square = (n: number) => n * n
*
* assert.strictEqual(Function.compose(increment, square)(2), 9)
* ```
*
* @see {@link flow} for composing a left-to-right sequence of functions
* @see {@link pipe} for applying a value through a left-to-right sequence immediately
*
* @category combinators
* @since 2.0.0
*/
const compose = dual(2, (ab, bc) => (a) => bc(ab(a)));
/**
* Marks an impossible branch by accepting a `never` value and returning any
* type.
*
* **When to use**
*
* Use when you need a return value in a branch that exhaustive checks prove
* cannot be reached.
*
* **Gotchas**
*
* Calling `absurd` throws, because a value of type `never` should be
* impossible at runtime.
*
* **Example** (Handling impossible values)
*
* ```ts
* import { absurd } from "effect"
*
* const handleNever = (value: never) => {
* return absurd(value) // This will throw an error if called
* }
* ```
*
* @category utility types
* @since 2.0.0
*/
const absurd = (_) => {
throw new Error("Called `absurd` function which should be uncallable");
};
/**
* Creates a compile-time placeholder for a value of any type.
*
* **When to use**
*
* Use as a temporary typed placeholder while developing incomplete code.
*
* **Gotchas**
*
* `hole` is intended for temporary development use. If the placeholder is
* evaluated at runtime, it throws.
*
* **Example** (Creating a development placeholder)
*
* ```ts
* import { hole } from "effect"
*
* // Intentionally not called: `hole` throws if the placeholder is evaluated.
* const buildUser = (id: number): { readonly id: number; readonly name: string } => ({
* id,
* name: hole<string>()
* })
*
* console.log(typeof buildUser) // "function"
* ```
*
* @category utility types
* @since 2.0.0
*/
const hole = cast(absurd);
/**
* Drops the longest prefix of elements from an array that satisfy the given predicate.
*
* Supports both data-first and data-last (`pipe`-friendly) call styles.
*
* @param pred - The predicate to test each element with.
* @returns A new array without the matching prefix.
* @example
* ```ts
* import * as assert from "node:assert"
* import { dropWhile, pipe } from "@local/eff"
*
* // data-first
* assert.deepStrictEqual(dropWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [3, 2, 1])
*
* // data-last
* assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], dropWhile((n: number) => n < 3)), [3, 2, 1])
* ```
* @category array
*/
const dropWhile = dual(2, (xs, pred) => {
const len = xs.length;
let idx = 0;
while (idx < len && pred(xs[idx])) idx++;
return xs.slice(idx);
});
/**
* Takes the longest prefix of elements from an array that satisfy the given predicate.
*
* Supports both data-first and data-last (`pipe`-friendly) call styles.
*
* @param pred - The predicate to test each element with.
* @returns A new array containing only the matching prefix.
* @example
* ```ts
* import * as assert from "node:assert"
* import { pipe, takeWhile } from "@local/eff"
*
* // data-first
* assert.deepStrictEqual(takeWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [1, 2])
*
* // data-last
* assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], takeWhile((n: number) => n < 3)), [1, 2])
* ```
* @category array
*/
const takeWhile = dual(2, (xs, pred) => {
const len = xs.length;
let idx = 0;
while (idx < len && pred(xs[idx])) idx++;
return xs.slice(0, idx);
});
//#endregion
//#region src/rules/no-leaked-intersection-observer/lib.ts
function isNewIntersectionObserver(node) {
if (node?.type !== AST_NODE_TYPES.NewExpression) return false;
const callee = Extract.unwrap(node.callee);
return callee.type === AST_NODE_TYPES.Identifier && callee.name === "IntersectionObserver";
}
function isFromObserver$1(context, node) {
switch (true) {
case node.type === AST_NODE_TYPES.Identifier: {
const initNode = resolve(context, node);
return isNewIntersectionObserver(initNode == null ? null : Extract.unwrap(initNode));
}
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver$1(context, node.object);
default: return false;
}
}
//#endregion
//#region src/rules/no-leaked-intersection-observer/no-leaked-intersection-observer.ts
const RULE_NAME$3 = "no-leaked-intersection-observer";
function getCallKind$3(context, node) {
const callee = Extract.unwrap(node.callee);
if (callee.type !== AST_NODE_TYPES.Identifier && callee.type !== AST_NODE_TYPES.MemberExpression) return "other";
const name = Extract.getCalleeName(node);
if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver$1(context, callee)) return name;
return "other";
}
function getFunctionKind$1(node) {
return getPhaseKindOfFunction(node) ?? "other";
}
var no_leaked_intersection_observer_default = createRule({
meta: {
type: "problem",
docs: { description: "Enforces that every 'IntersectionObserver' created in a component or custom hook has a corresponding 'IntersectionObserver.disconnect()'." },
messages: {
expectedDisconnectInControlFlow: "Dynamically added 'IntersectionObserver.observe' should be cleared all at once using 'IntersectionObserver.disconnect' in the cleanup function.",
expectedDisconnectOrUnobserveInCleanup: "An 'IntersectionObserver' instance created in 'useEffect' must be disconnected in the cleanup function.",
unexpectedFloatingInstance: "An 'IntersectionObserver' instance created in component or custom hook must be assigned to a variable for proper cleanup."
},
schema: []
},
name: RULE_NAME$3,
create: create$3,
defaultOptions: []
});
function create$3(context) {
if (!context.sourceCode.text.includes("IntersectionObserver")) return {};
const fEntries = [];
const observers = [];
const oEntries = [];
const uEntries = [];
const dEntries = [];
return {
[":function"](node) {
const kind = getFunctionKind$1(node);
fEntries.push({
kind,
node
});
},
[":function:exit"]() {
fEntries.pop();
},
["CallExpression"](node) {
const unwrappedCallee = Extract.unwrap(node.callee);
if (unwrappedCallee.type !== AST_NODE_TYPES.MemberExpression) return;
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
if (fKind == null || !ComponentPhaseRelevance.has(fKind)) return;
const { object } = unwrappedCallee;
match(getCallKind$3(context, node)).with("disconnect", () => {
dEntries.push({
kind: "IntersectionObserver",
callee: node.callee,
method: "disconnect",
node,
observer: object,
phase: fKind
});
}).with("observe", () => {
const [element] = node.arguments;
if (element == null) return;
oEntries.push({
kind: "IntersectionObserver",
callee: node.callee,
element,
method: "observe",
node,
observer: object,
phase: fKind
});
}).with("unobserve", () => {
const [element] = node.arguments;
if (element == null) return;
uEntries.push({
kind: "IntersectionObserver",
callee: node.callee,
element,
method: "unobserve",
node,
observer: object,
phase: fKind
});
}).otherwise(() => null);
},
["NewExpression"](node) {
const fEntry = fEntries.findLast((x) => x.kind !== "other");
if (fEntry == null) return;
if (!ComponentPhaseRelevance.has(fEntry.kind)) return;
if (!isNewIntersectionObserver(node)) return;
const id = resolveEnclosingAssignmentTarget(node);
if (id == null) {
context.report({
messageId: "unexpectedFloatingInstance",
node
});
return;
}
observers.push({
id,
node,
phase: fEntry.kind,
phaseNode: fEntry.node
});
},
["Program:exit"]() {
for (const { id, node, phaseNode } of observers) {
const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
const oentries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
const uentries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || Check.isConditional(node);
const isPhaseNode = (node) => node === phaseNode;
if (oentries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
context.report({
messageId: "expectedDisconnectInControlFlow",
node
});
continue;
}
for (const oEntry of oentries) {
if (uentries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
context.report({
messageId: "expectedDisconnectOrUnobserveInCleanup",
node: oEntry.node
});
}
}
}
};
}
//#endregion
//#region src/rules/no-leaked-interval/no-leaked-interval.ts
const RULE_NAME$2 = "no-leaked-interval";
function getCallKind$2(node) {
const name = Extract.getCalleeName(node);
if (name != null && isMatching(P.union("setInterval", "clearInterval"))(name)) return name;
return "other";
}
var no_leaked_interval_default = createRule({
meta: {
type: "problem",
docs: { description: "Enforces that every 'setInterval' in a component or custom hook has a corresponding 'clearInterval'." },
messages: {
expectedClearIntervalInCleanup: "A 'setInterval' created in '{{ kind }}' must be cleared with 'clearInterval' in the cleanup function.",
expectedIntervalId: "A 'setInterval' must be assigned to a variable for proper cleanup."
},
schema: []
},
name: RULE_NAME$2,
create: create$2,
defaultOptions: []
});
function create$2(context) {
if (!context.sourceCode.text.includes("setInterval")) return {};
const fEntries = [];
const sEntries = [];
const cEntries = [];
function isInverseEntry(a, b) {
return isAssignmentTargetEqual(context, a.timerId, b.timerId);
}
return {
[":function"](node) {
const kind = getPhaseKindOfFunction(node) ?? "other";
fEntries.push({
kind,
node
});
},
[":function:exit"]() {
fEntries.pop();
},
["CallExpression"](node) {
switch (getCallKind$2(node)) {
case "setInterval": {
const fEntry = fEntries.findLast((x) => x.kind !== "other");
if (fEntry == null) break;
if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
const intervalIdNode = resolveEnclosingAssignmentTarget(node);
if (intervalIdNode == null) {
context.report({
messageId: "expectedIntervalId",
node
});
break;
}
sEntries.push({
kind: "interval",
callee: node.callee,
node,
phase: fEntry.kind,
timerId: intervalIdNode
});
break;
}
case "clearInterval": {
const fEntry = fEntries.findLast((x) => x.kind !== "other");
if (fEntry == null) break;
if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
const [intervalIdNode] = node.arguments;
if (intervalIdNode == null) break;
cEntries.push({
kind: "interval",
callee: node.callee,
node,
phase: fEntry.kind,
timerId: intervalIdNode
});
break;
}
}
},
["Program:exit"]() {
for (const sEntry of sEntries) {
if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
switch (sEntry.phase) {
case "setup":
case "cleanup":
context.report({
data: { kind: "useEffect" },
messageId: "expectedClearIntervalInCleanup",
node: sEntry.node
});
continue;
}
}
}
};
}
//#endregion
//#region src/rules/no-leaked-resize-observer/lib.ts
function isNewResizeObserver(node) {
if (node?.type !== AST_NODE_TYPES.NewExpression) return false;
const callee = Extract.unwrap(node.callee);
return callee.type === AST_NODE_TYPES.Identifier && callee.name === "ResizeObserver";
}
function isFromObserver(context, node) {
switch (true) {
case node.type === AST_NODE_TYPES.Identifier: {
const initNode = resolve(context, node);
return isNewResizeObserver(initNode == null ? null : Extract.unwrap(initNode));
}
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object);
default: return false;
}
}
//#endregion
//#region src/rules/no-leaked-resize-observer/no-leaked-resize-observer.ts
const RULE_NAME$1 = "no-leaked-resize-observer";
function getCallKind$1(context, node) {
const callee = Extract.unwrap(node.callee);
if (callee.type !== AST_NODE_TYPES.Identifier && callee.type !== AST_NODE_TYPES.MemberExpression) return "other";
const name = Extract.getCalleeName(node);
if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver(context, callee)) return name;
return "other";
}
function getFunctionKind(node) {
return getPhaseKindOfFunction(node) ?? "other";
}
var no_leaked_resize_observer_default = createRule({
meta: {
type: "problem",
docs: { description: "Enforces that every 'ResizeObserver' created in a component or custom hook has a corresponding 'ResizeObserver.disconnect()'." },
messages: {
expectedDisconnectInControlFlow: "Dynamically added 'ResizeObserver.observe' should be cleared all at once using 'ResizeObserver.disconnect' in the cleanup function.",
expectedDisconnectOrUnobserveInCleanup: "A 'ResizeObserver' instance created in 'useEffect' must be disconnected in the cleanup function.",
unexpectedFloatingInstance: "A 'ResizeObserver' instance created in component or custom hook must be assigned to a variable for proper cleanup."
},
schema: []
},
name: RULE_NAME$1,
create: create$1,
defaultOptions: []
});
function create$1(context) {
if (!context.sourceCode.text.includes("ResizeObserver")) return {};
const fEntries = [];
const observers = [];
const oEntries = [];
const uEntries = [];
const dEntries = [];
return {
[":function"](node) {
const kind = getFunctionKind(node);
fEntries.push({
kind,
node
});
},
[":function:exit"]() {
fEntries.pop();
},
["CallExpression"](node) {
const unwrappedCallee = Extract.unwrap(node.callee);
if (unwrappedCallee.type !== AST_NODE_TYPES.MemberExpression) return;
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
if (fKind == null || !ComponentPhaseRelevance.has(fKind)) return;
const { object } = unwrappedCallee;
match(getCallKind$1(context, node)).with("disconnect", () => {
dEntries.push({
kind: "ResizeObserver",
callee: node.callee,
method: "disconnect",
node,
observer: object,
phase: fKind
});
}).with("observe", () => {
const [element] = node.arguments;
if (element == null) return;
oEntries.push({
kind: "ResizeObserver",
callee: node.callee,
element,
method: "observe",
node,
observer: object,
phase: fKind
});
}).with("unobserve", () => {
const [element] = node.arguments;
if (element == null) return;
uEntries.push({
kind: "ResizeObserver",
callee: node.callee,
element,
method: "unobserve",
node,
observer: object,
phase: fKind
});
}).otherwise(() => null);
},
["NewExpression"](node) {
const fEntry = fEntries.findLast((x) => x.kind !== "other");
if (fEntry == null) return;
if (!ComponentPhaseRelevance.has(fEntry.kind)) return;
if (!isNewResizeObserver(node)) return;
const id = resolveEnclosingAssignmentTarget(node);
if (id == null) {
context.report({
messageId: "unexpectedFloatingInstance",
node
});
return;
}
observers.push({
id,
node,
phase: fEntry.kind,
phaseNode: fEntry.node
});
},
["Program:exit"]() {
for (const { id, node, phaseNode } of observers) {
const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
const oentries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
const uentries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || Check.isConditional(node);
const isPhaseNode = (node) => node === phaseNode;
if (oentries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
context.report({
messageId: "expectedDisconnectInControlFlow",
node
});
continue;
}
for (const oEntry of oentries) {
if (uentries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
context.report({
messageId: "expectedDisconnectOrUnobserveInCleanup",
node: oEntry.node
});
}
}
}
};
}
//#endregion
//#region src/rules/no-leaked-timeout/no-leaked-timeout.ts
const RULE_NAME = "no-leaked-timeout";
function getCallKind(node) {
const name = Extract.getCalleeName(node);
if (name != null && isMatching(P.union("setTimeout", "clearTimeout"))(name)) return name;
return "other";
}
var no_leaked_timeout_default = createRule({
meta: {
type: "problem",
docs: { description: "Enforces that every 'setTimeout' in a component or custom hook has a corresponding 'clearTimeout'." },
messages: {
expectedClearTimeoutInCleanup: "A 'setTimeout' created in '{{ kind }}' must be cleared with 'clearTimeout' in the cleanup function.",
expectedTimeoutId: "A 'setTimeout' must be assigned to a variable for proper cleanup."
},
schema: []
},
name: RULE_NAME,
create,
defaultOptions: []
});
function create(context) {
if (!context.sourceCode.text.includes("setTimeout")) return {};
const fEntries = [];
const sEntries = [];
const rEntries = [];
function isInverseEntry(a, b) {
return isAssignmentTargetEqual(context, a.timerId, b.timerId);
}
return {
[":function"](node) {
const kind = getPhaseKindOfFunction(node) ?? "other";
fEntries.push({
kind,
node
});
},
[":function:exit"]() {
fEntries.pop();
},
["CallExpression"](node) {
const fEntry = fEntries.findLast((f) => f.kind !== "other");
if (!ComponentPhaseRelevance.has(fEntry?.kind)) return;
switch (getCallKind(node)) {
case "setTimeout": {
const timeoutIdNode = resolveEnclosingAssignmentTarget(node);
if (timeoutIdNode == null) {
context.report({
messageId: "expectedTimeoutId",
node
});
break;
}
sEntries.push({
kind: "timeout",
callee: node.callee,
node,
phase: fEntry.kind,
timerId: timeoutIdNode
});
break;
}
case "clearTimeout": {
const [timeoutIdNode] = node.arguments;
if (timeoutIdNode == null) break;
rEntries.push({
kind: "timeout",
callee: node.callee,
node,
phase: fEntry.kind,
timerId: timeoutIdNode
});
break;
}
}
},
["Program:exit"]() {
for (const sEntry of sEntries) {
if (rEntries.some((rEntry) => isInverseEntry(sEntry, rEntry))) continue;
switch (sEntry.phase) {
case "setup":
case "cleanup":
context.report({
data: { kind: "useEffect" },
messageId: "expectedClearTimeoutInCleanup",
node: sEntry.node
});
continue;
}
}
}
};
}
//#endregion
//#region src/plugin.ts
const plugin = {
meta: {
name: name$1,
version
},
rules: {
"no-leaked-event-listener": no_leaked_event_listener_default,
"no-leaked-fetch": no_leaked_fetch_default,
"no-leaked-intersection-observer": no_leaked_intersection_observer_default,
"no-leaked-interval": no_leaked_interval_default,
"no-leaked-resize-observer": no_leaked_resize_observer_default,
"no-leaked-timeout": no_leaked_timeout_default
}
};
//#endregion
//#region src/configs/recommended.ts
var recommended_exports = /* @__PURE__ */ __exportAll({
name: () => name,
plugins: () => plugins,
rules: () => rules,
settings: () => settings
});
const name = "react-web-api/recommended";
const rules = {
"react-web-api/no-leaked-event-listener": "warn",
"react-web-api/no-leaked-fetch": "warn",
"react-web-api/no-leaked-intersection-observer": "warn",
"react-web-api/no-leaked-interval": "warn",
"react-web-api/no-leaked-resize-observer": "warn",
"react-web-api/no-leaked-timeout": "warn"
};
const plugins = { "react-web-api": plugin };
const settings = { "react-x": DEFAULT_ESLINT_REACT_SETTINGS };
//#endregion
//#region src/index.ts
const finalPlugin = {
...plugin,
configs: { ["recommended"]: recommended_exports }
};
//#endregion
export { finalPlugin as default };