diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index 351ec3a1f7..8c3959c2e7 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -30,6 +30,7 @@ "no-implicit-key", "no-leaked-conditional-rendering", "no-missing-component-display-name", + "no-missing-context-display-name", "no-missing-key", "no-nested-components", "no-prop-types", diff --git a/apps/website/content/docs/rules/overview.md b/apps/website/content/docs/rules/overview.md index dbc757e3ff..4826c40385 100644 --- a/apps/website/content/docs/rules/overview.md +++ b/apps/website/content/docs/rules/overview.md @@ -16,59 +16,60 @@ full: true ## Core Rules -| Rule | ✅ | Features | Description | React | -| :----------------------------------------------------------------------------------- | :- | :------------ | :--------------------------------------------------------------------------------------------------- | :------: | -| [`avoid-shorthand-boolean`](./avoid-shorthand-boolean) | 0️⃣ | `🔍` `🔧` | Enforces the use of explicit boolean values for boolean attributes. | | -| [`avoid-shorthand-fragment`](./avoid-shorthand-fragment) | 0️⃣ | `🔍` | Enforces the use of explicit `` components instead of the shorthand `<>` or `` syntax. | | -| [`ensure-forward-ref-using-ref`](./ensure-forward-ref-using-ref) | 1️⃣ | `🔍` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | | -| [`no-access-state-in-setstate`](./no-access-state-in-setstate) | 2️⃣ | `🔍` | Prevents accessing `this.state` inside `setState` calls. | | -| [`no-array-index-key`](./no-array-index-key) | 1️⃣ | `🔍` | Prevents using array `index` as `key`. | | -| [`no-children-count`](./no-children-count) | 1️⃣ | `🔍` | Prevents using `Children.count`. | | -| [`no-children-for-each`](./no-children-for-each) | 1️⃣ | `🔍` | Prevents using `Children.forEach`. | | -| [`no-children-map`](./no-children-map) | 1️⃣ | `🔍` | Prevents using `Children.map`. | | -| [`no-children-only`](./no-children-only) | 1️⃣ | `🔍` | Prevents using `Children.only`. | | -| [`no-children-prop`](./no-children-prop) | 0️⃣ | `🔍` | Prevents using `children` as a prop. | | -| [`no-children-to-array`](./no-children-to-array) | 1️⃣ | `🔍` | Prevents using `Children.toArray`. | | -| [`no-class-component`](./no-class-component) | 0️⃣ | `🔍` | Prevents using class component. | | -| [`no-clone-element`](./no-clone-element) | 1️⃣ | `🔍` | Prevents using `cloneElement`. | | -| [`no-comment-textnodes`](./no-comment-textnodes) | 1️⃣ | `🔍` | Prevents comments from being inserted as text nodes. | | -| [`no-complex-conditional-rendering`](./no-complex-conditional-rendering) | 0️⃣ | `🔍` | Prevents complex conditional rendering in JSX. | | -| [`no-component-will-mount`](./no-component-will-mount) | 2️⃣ | `🔍` `🔄` | Prevents using `componentWillMount`. | >=16.3.0 | -| [`no-component-will-receive-props`](./no-component-will-receive-props) | 2️⃣ | `🔍` `🔄` | Prevents using `componentWillReceiveProps`. | >=16.3.0 | -| [`no-component-will-update`](./no-component-will-update) | 2️⃣ | `🔍` `🔄` | Prevents using `componentWillUpdate`. | >=16.3.0 | -| [`no-context-provider`](./no-context-provider) | 1️⃣ | `🔍` `🔄` | Prevents using ``. | >=19.0.0 | -| [`no-create-ref`](./no-create-ref) | 2️⃣ | `🔍` | Prevents using `createRef`. | | -| [`no-default-props`](./no-default-props) | 2️⃣ | `🔍` | Prevents using `defaultProps` property in favor of ES6 default parameters. | | -| [`no-direct-mutation-state`](./no-direct-mutation-state) | 2️⃣ | `🔍` | Prevents direct mutation of `this.state`. | | -| [`no-duplicate-jsx-props`](./no-duplicate-jsx-props) | 1️⃣ | `🔍` | Prevents duplicate props in JSX. | | -| [`no-duplicate-key`](./no-duplicate-key) | 2️⃣ | `🔍` | Prevents duplicate `key` on elements in the same array or a list of `children`. | | -| [`no-forward-ref`](./no-forward-ref) | 1️⃣ | `🔍` `🔄` | Prevents using `React.forwardRef`. | >=19.0.0 | -| [`no-implicit-key`](./no-implicit-key) | 1️⃣ | `🔍` | Prevents `key` from not being explicitly specified (e.g. spreading `key` from objects). | | -| [`no-leaked-conditional-rendering`](./no-leaked-conditional-rendering) | 1️⃣ | `🔍` `💭` | Prevents problematic leaked values from being rendered. | | -| [`no-missing-component-display-name`](./no-missing-component-display-name) | 0️⃣ | `🔍` | Enforces that all components have a `displayName` which can be used in devtools. | | -| [`no-missing-key`](./no-missing-key) | 2️⃣ | `🔍` | Prevents missing `key` on items in list rendering. | | -| [`no-nested-components`](./no-nested-components) | 2️⃣ | `🔍` | Prevents nesting component definitions inside other components. | | -| [`no-prop-types`](./no-prop-types) | 2️⃣ | `🔍` | Prevents using `propTypes` in favor of TypeScript or another type-checking solution. | | -| [`no-redundant-should-component-update`](./no-redundant-should-component-update) | 2️⃣ | `🔍` | Prevents using `shouldComponentUpdate` when extending `React.PureComponent`. | | -| [`no-set-state-in-component-did-mount`](./no-set-state-in-component-did-mount) | 1️⃣ | `🔍` | Prevents calling `this.setState` in `componentDidMount` outside of functions, such as callbacks. | | -| [`no-set-state-in-component-did-update`](./no-set-state-in-component-did-update) | 1️⃣ | `🔍` | Prevents calling `this.setState` in `componentDidUpdate` outside of functions, such as callbacks. | | -| [`no-set-state-in-component-will-update`](./no-set-state-in-component-will-update) | 1️⃣ | `🔍` | Prevents calling `this.setState` in `componentWillUpdate` outside of functions, such as callbacks. | | -| [`no-string-refs`](./no-string-refs) | 2️⃣ | `🔍` | Prevents using deprecated string `refs`. | | -| [`no-unsafe-component-will-mount`](./no-unsafe-component-will-mount) | 1️⃣ | `🔍` | Warns the usage of `UNSAFE_componentWillMount` in class components. | | -| [`no-unsafe-component-will-receive-props`](./no-unsafe-component-will-receive-props) | 1️⃣ | `🔍` | Warns the usage of `UNSAFE_componentWillReceiveProps` in class components. | | -| [`no-unsafe-component-will-update`](./no-unsafe-component-will-update) | 1️⃣ | `🔍` | Warns the usage of `UNSAFE_componentWillUpdate` in class components. | | -| [`no-unstable-context-value`](./no-unstable-context-value) | 1️⃣ | `🔍` | Prevents non-stable values (i.e. object literals) from being used as a value for `Context.Provider`. | | -| [`no-unstable-default-props`](./no-unstable-default-props) | 1️⃣ | `🔍` | Prevents using referential-type values as default props in object destructuring. | | -| [`no-unused-class-component-members`](./no-unused-class-component-members) | 0️⃣ | `🔍` | Warns unused class component methods and properties. | | -| [`no-unused-state`](./no-unused-state) | 1️⃣ | `🔍` | Warns unused class component state. | | -| [`no-use-context`](./no-use-context) | 1️⃣ | `🔍` `🔄` | Prevents using `useContext` in favor of `use`. | >=19.0.0 | -| [`no-useless-fragment`](./no-useless-fragment) | 1️⃣ | `🔍` `🔧` `⚙️` | Prevents using useless `fragment` components or `<>` syntax. | | -| [`prefer-destructuring-assignment`](./prefer-destructuring-assignment) | 0️⃣ | `🔍` | Enforces using destructuring assignment over property assignment. | | -| [`prefer-react-namespace-import`](./prefer-react-namespace-import) | 0️⃣ | `🔍` `🔧` | Enforces React is imported via a namespace import | | -| [`prefer-read-only-props`](./prefer-read-only-props) | 0️⃣ | `🔍` `💭` | Enforces read-only props in components. | | -| [`prefer-shorthand-boolean`](./prefer-shorthand-boolean) | 0️⃣ | `🔍` `🔧` | Enforces using shorthand syntax for boolean attributes. | | -| [`prefer-shorthand-fragment`](./prefer-shorthand-fragment) | 0️⃣ | `🔍` `🔧` | Enforces using shorthand syntax for fragments. | | -| [`use-jsx-vars`](./use-jsx-vars) | 1️⃣ | | Marks variables used in JSX as used. | | +| Rule | ✅ | Features | Description | React | +| :----------------------------------------------------------------------------------- | :- | :------------ | :---------------------------------------------------------------------------------------------------- | :------: | +| [`avoid-shorthand-boolean`](./avoid-shorthand-boolean) | 0️⃣ | `🔍` `🔧` | Enforces the use of explicit boolean values for boolean attributes. | | +| [`avoid-shorthand-fragment`](./avoid-shorthand-fragment) | 0️⃣ | `🔍` | Enforces the use of explicit `` components instead of the shorthand `<>` or `` syntax. | | +| [`ensure-forward-ref-using-ref`](./ensure-forward-ref-using-ref) | 1️⃣ | `🔍` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | | +| [`no-access-state-in-setstate`](./no-access-state-in-setstate) | 2️⃣ | `🔍` | Prevents accessing `this.state` inside `setState` calls. | | +| [`no-array-index-key`](./no-array-index-key) | 1️⃣ | `🔍` | Prevents using array `index` as `key`. | | +| [`no-children-count`](./no-children-count) | 1️⃣ | `🔍` | Prevents using `Children.count`. | | +| [`no-children-for-each`](./no-children-for-each) | 1️⃣ | `🔍` | Prevents using `Children.forEach`. | | +| [`no-children-map`](./no-children-map) | 1️⃣ | `🔍` | Prevents using `Children.map`. | | +| [`no-children-only`](./no-children-only) | 1️⃣ | `🔍` | Prevents using `Children.only`. | | +| [`no-children-prop`](./no-children-prop) | 0️⃣ | `🔍` | Prevents using `children` as a prop. | | +| [`no-children-to-array`](./no-children-to-array) | 1️⃣ | `🔍` | Prevents using `Children.toArray`. | | +| [`no-class-component`](./no-class-component) | 0️⃣ | `🔍` | Prevents using class component. | | +| [`no-clone-element`](./no-clone-element) | 1️⃣ | `🔍` | Prevents using `cloneElement`. | | +| [`no-comment-textnodes`](./no-comment-textnodes) | 1️⃣ | `🔍` | Prevents comments from being inserted as text nodes. | | +| [`no-complex-conditional-rendering`](./no-complex-conditional-rendering) | 0️⃣ | `🔍` | Prevents complex conditional rendering in JSX. | | +| [`no-component-will-mount`](./no-component-will-mount) | 2️⃣ | `🔍` `🔄` | Prevents using `componentWillMount`. | >=16.3.0 | +| [`no-component-will-receive-props`](./no-component-will-receive-props) | 2️⃣ | `🔍` `🔄` | Prevents using `componentWillReceiveProps`. | >=16.3.0 | +| [`no-component-will-update`](./no-component-will-update) | 2️⃣ | `🔍` `🔄` | Prevents using `componentWillUpdate`. | >=16.3.0 | +| [`no-context-provider`](./no-context-provider) | 1️⃣ | `🔍` `🔄` | Prevents using ``. | >=19.0.0 | +| [`no-create-ref`](./no-create-ref) | 2️⃣ | `🔍` | Prevents using `createRef`. | | +| [`no-default-props`](./no-default-props) | 2️⃣ | `🔍` | Prevents using `defaultProps` property in favor of ES6 default parameters. | | +| [`no-direct-mutation-state`](./no-direct-mutation-state) | 2️⃣ | `🔍` | Prevents direct mutation of `this.state`. | | +| [`no-duplicate-jsx-props`](./no-duplicate-jsx-props) | 1️⃣ | `🔍` | Prevents duplicate props in JSX. | | +| [`no-duplicate-key`](./no-duplicate-key) | 2️⃣ | `🔍` | Prevents duplicate `key` on elements in the same array or a list of `children`. | | +| [`no-forward-ref`](./no-forward-ref) | 1️⃣ | `🔍` `🔄` | Prevents using `React.forwardRef`. | >=19.0.0 | +| [`no-implicit-key`](./no-implicit-key) | 1️⃣ | `🔍` | Prevents `key` from not being explicitly specified (e.g. spreading `key` from objects). | | +| [`no-leaked-conditional-rendering`](./no-leaked-conditional-rendering) | 1️⃣ | `🔍` `💭` | Prevents problematic leaked values from being rendered. | | +| [`no-missing-component-display-name`](./no-missing-component-display-name) | 0️⃣ | `🔍` | Enforces that all components have a `displayName` which can be used in devtools. | | +| [`no-missing-context-display-name`](./no-missing-context-display-name) | 0️⃣ | `🔍` | Enforces that all contexts have a `displayName` which React can use as its `displayName` in devtools. | | +| [`no-missing-key`](./no-missing-key) | 2️⃣ | `🔍` | Prevents missing `key` on items in list rendering. | | +| [`no-nested-components`](./no-nested-components) | 2️⃣ | `🔍` | Prevents nesting component definitions inside other components. | | +| [`no-prop-types`](./no-prop-types) | 2️⃣ | `🔍` | Prevents using `propTypes` in favor of TypeScript or another type-checking solution. | | +| [`no-redundant-should-component-update`](./no-redundant-should-component-update) | 2️⃣ | `🔍` | Prevents using `shouldComponentUpdate` when extending `React.PureComponent`. | | +| [`no-set-state-in-component-did-mount`](./no-set-state-in-component-did-mount) | 1️⃣ | `🔍` | Prevents calling `this.setState` in `componentDidMount` outside of functions, such as callbacks. | | +| [`no-set-state-in-component-did-update`](./no-set-state-in-component-did-update) | 1️⃣ | `🔍` | Prevents calling `this.setState` in `componentDidUpdate` outside of functions, such as callbacks. | | +| [`no-set-state-in-component-will-update`](./no-set-state-in-component-will-update) | 1️⃣ | `🔍` | Prevents calling `this.setState` in `componentWillUpdate` outside of functions, such as callbacks. | | +| [`no-string-refs`](./no-string-refs) | 2️⃣ | `🔍` | Prevents using deprecated string `refs`. | | +| [`no-unsafe-component-will-mount`](./no-unsafe-component-will-mount) | 1️⃣ | `🔍` | Warns the usage of `UNSAFE_componentWillMount` in class components. | | +| [`no-unsafe-component-will-receive-props`](./no-unsafe-component-will-receive-props) | 1️⃣ | `🔍` | Warns the usage of `UNSAFE_componentWillReceiveProps` in class components. | | +| [`no-unsafe-component-will-update`](./no-unsafe-component-will-update) | 1️⃣ | `🔍` | Warns the usage of `UNSAFE_componentWillUpdate` in class components. | | +| [`no-unstable-context-value`](./no-unstable-context-value) | 1️⃣ | `🔍` | Prevents non-stable values (i.e. object literals) from being used as a value for `Context.Provider`. | | +| [`no-unstable-default-props`](./no-unstable-default-props) | 1️⃣ | `🔍` | Prevents using referential-type values as default props in object destructuring. | | +| [`no-unused-class-component-members`](./no-unused-class-component-members) | 0️⃣ | `🔍` | Warns unused class component methods and properties. | | +| [`no-unused-state`](./no-unused-state) | 1️⃣ | `🔍` | Warns unused class component state. | | +| [`no-use-context`](./no-use-context) | 1️⃣ | `🔍` `🔄` | Prevents using `useContext` in favor of `use`. | >=19.0.0 | +| [`no-useless-fragment`](./no-useless-fragment) | 1️⃣ | `🔍` `🔧` `⚙️` | Prevents using useless `fragment` components or `<>` syntax. | | +| [`prefer-destructuring-assignment`](./prefer-destructuring-assignment) | 0️⃣ | `🔍` | Enforces using destructuring assignment over property assignment. | | +| [`prefer-react-namespace-import`](./prefer-react-namespace-import) | 0️⃣ | `🔍` `🔧` | Enforces React is imported via a namespace import | | +| [`prefer-read-only-props`](./prefer-read-only-props) | 0️⃣ | `🔍` `💭` | Enforces read-only props in components. | | +| [`prefer-shorthand-boolean`](./prefer-shorthand-boolean) | 0️⃣ | `🔍` `🔧` | Enforces using shorthand syntax for boolean attributes. | | +| [`prefer-shorthand-fragment`](./prefer-shorthand-fragment) | 0️⃣ | `🔍` `🔧` | Enforces using shorthand syntax for fragments. | | +| [`use-jsx-vars`](./use-jsx-vars) | 1️⃣ | | Marks variables used in JSX as used. | | ### Deprecated diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md index ac285ec0b6..d07961fdb9 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -39,7 +39,7 @@ ## Variables -- [COMPONENT\_DISPLAY\_NAME\_ASSIGNMENT\_SELECTOR](variables/COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR.md) +- [COMPONENT\_DISPLAY\_NAME\_ASSIGNMENT\_SELECTOR](variables/DISPLAY_NAME_ASSIGNMENT_SELECTOR.md) - [DEFAULT\_COMPONENT\_HINT](variables/DEFAULT_COMPONENT_HINT.md) - [ERComponentFlag](variables/ERComponentFlag.md) - [ERComponentHint](variables/ERComponentHint.md) @@ -70,7 +70,7 @@ - [isCloneElementCall](functions/isCloneElementCall.md) - [isComponentDidCatch](functions/isComponentDidCatch.md) - [isComponentDidMount](functions/isComponentDidMount.md) -- [isComponentDisplayNameAssignment](functions/isComponentDisplayNameAssignment.md) +- [isDisplayNameAssignment](functions/isDisplayNameAssignment.md) - [isComponentName](functions/isComponentName.md) - [isComponentWillUnmount](functions/isComponentWillUnmount.md) - [isCreateContext](functions/isCreateContext.md) diff --git a/packages/core/docs/functions/isComponentDisplayNameAssignment.md b/packages/core/docs/functions/isComponentDisplayNameAssignment.md index fb12eb01b9..6f366deac1 100644 --- a/packages/core/docs/functions/isComponentDisplayNameAssignment.md +++ b/packages/core/docs/functions/isComponentDisplayNameAssignment.md @@ -2,11 +2,11 @@ *** -[@eslint-react/core](../README.md) / isComponentDisplayNameAssignment +[@eslint-react/core](../README.md) / isDisplayNameAssignment -# Function: isComponentDisplayNameAssignment() +# Function: isDisplayNameAssignment() -> **isComponentDisplayNameAssignment**(`node`): `node is AssignmentExpression` +> **isDisplayNameAssignment**(`node`): `node is AssignmentExpression` Check if the node is a component display name assignment expression diff --git a/packages/core/src/component/component-collector.ts b/packages/core/src/component/component-collector.ts index 57b5755201..6688002fce 100644 --- a/packages/core/src/component/component-collector.ts +++ b/packages/core/src/component/component-collector.ts @@ -9,8 +9,8 @@ import type { ESLintUtils } from "@typescript-eslint/utils"; import { isChildrenOfCreateElement } from "../element"; import { isReactHookCall } from "../hook"; +import { DISPLAY_NAME_ASSIGNMENT_SELECTOR } from "../utils"; import { DEFAULT_COMPONENT_HINT, ERComponentHint } from "./component-collector-hint"; -import { COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR } from "./component-display-name"; import { ERComponentFlag } from "./component-flag"; import { getFunctionComponentIdentifier } from "./component-id"; import { isFunctionOfRenderMethod } from "./component-lifecycle"; @@ -106,7 +106,7 @@ export function useComponentCollector( }, ...collectDisplayName ? { - [COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR](node: TSESTree.AssignmentExpression) { + [DISPLAY_NAME_ASSIGNMENT_SELECTOR](node: TSESTree.AssignmentExpression) { const { left, right } = node; if (left.type !== T.MemberExpression) return; const componentName = left.object.type === T.Identifier diff --git a/packages/core/src/component/index.ts b/packages/core/src/component/index.ts index 7333c7f3ff..12d5cc1a1c 100644 --- a/packages/core/src/component/index.ts +++ b/packages/core/src/component/index.ts @@ -1,7 +1,6 @@ export * from "./component-collector"; export * from "./component-collector-hint"; export * from "./component-collector-legacy"; -export * from "./component-display-name"; export * from "./component-flag"; export * from "./component-id"; export type * from "./component-kind"; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 89f23d3b20..ea222efa31 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from "./is-display-name-assignment"; export * from "./is-from-react"; export * from "./is-initialized-from-react"; export * from "./is-react-api"; diff --git a/packages/core/src/component/component-display-name.ts b/packages/core/src/utils/is-display-name-assignment.ts similarity index 81% rename from packages/core/src/component/component-display-name.ts rename to packages/core/src/utils/is-display-name-assignment.ts index 20151b1299..4910f05248 100644 --- a/packages/core/src/component/component-display-name.ts +++ b/packages/core/src/utils/is-display-name-assignment.ts @@ -4,7 +4,7 @@ import { AST_NODE_TYPES as T, type TSESTree } from "@typescript-eslint/types"; /** * The ESQuery selector for a component display name assignment expression */ -export const COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR = [ +export const DISPLAY_NAME_ASSIGNMENT_SELECTOR = [ "AssignmentExpression", "[type]", "[operator='=']", @@ -17,7 +17,7 @@ export const COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR = [ * @param node The AST node * @returns `true` if the node is a component display name assignment */ -export function isComponentDisplayNameAssignment(node: TSESTree.Node | _): node is TSESTree.AssignmentExpression { +export function isDisplayNameAssignment(node: TSESTree.Node | _): node is TSESTree.AssignmentExpression { if (node == null) return false; return node.type === T.AssignmentExpression && node.operator === "=" diff --git a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts index 6619e10619..628eee0f18 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts @@ -8,7 +8,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; import { isMatching, P } from "ts-pattern"; import type { TimerEntry } from "../models"; -import { createRule, getPhaseKindOfFunction, isInstanceIDEqual } from "../utils"; +import { createRule, getPhaseKindOfFunction, isInstanceIdEqual } from "../utils"; // #region Rule Metadata @@ -81,7 +81,7 @@ export default createRule<[], MessageID>({ const sEntries: TimerEntry[] = []; const cEntries: TimerEntry[] = []; function isInverseEntry(a: TimerEntry, b: TimerEntry) { - return isInstanceIDEqual(a.timerId, b.timerId, context); + return isInstanceIdEqual(a.timerId, b.timerId, context); } return { [":function"](node: AST.TSESTreeFunction) { diff --git a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts index 712d7e3b5e..408419fa03 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts @@ -8,7 +8,7 @@ import type { TSESTree } from "@typescript-eslint/utils"; import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; import { isMatching, match, P } from "ts-pattern"; -import { createRule, getInstanceID, getPhaseKindOfFunction, isInstanceIDEqual } from "../utils"; +import { createRule, getPhaseKindOfFunction, isInstanceIdEqual } from "../utils"; import type { ObserverEntry, ObserverMethod } from "./../models"; // #region Rule Metadata @@ -185,7 +185,7 @@ export default createRule<[], MessageID>({ if (!isNewResizeObserver(node)) { return; } - const id = getInstanceID(node); + const id = VAR.getVariableId(node); if (id == null) { context.report({ messageId: "unexpectedFloatingInstance", @@ -202,11 +202,11 @@ export default createRule<[], MessageID>({ }, ["Program:exit"]() { for (const { id, node, phaseNode } of observers) { - if (dEntries.some((e) => isInstanceIDEqual(e.observer, id, context))) { + if (dEntries.some((e) => isInstanceIdEqual(e.observer, id, context))) { continue; } - const oentries = oEntries.filter((e) => isInstanceIDEqual(e.observer, id, context)); - const uentries = uEntries.filter((e) => isInstanceIDEqual(e.observer, id, context)); + const oentries = oEntries.filter((e) => isInstanceIdEqual(e.observer, id, context)); + const uentries = uEntries.filter((e) => isInstanceIdEqual(e.observer, id, context)); const isDynamic = (node: TSESTree.Node | _) => node?.type === T.CallExpression || AST.isConditional(node); const isPhaseNode = (node: TSESTree.Node | _) => node === phaseNode; const hasDynamicallyAdded = oentries @@ -216,7 +216,7 @@ export default createRule<[], MessageID>({ continue; } for (const oEntry of oentries) { - if (uentries.some((uEntry) => isInstanceIDEqual(uEntry.element, oEntry.element, context))) { + if (uentries.some((uEntry) => isInstanceIdEqual(uEntry.element, oEntry.element, context))) { continue; } context.report({ messageId: "expectedDisconnectOrUnobserveInCleanup", node: oEntry.node }); diff --git a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts index 11967f58f7..f2b9943fb8 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts @@ -8,7 +8,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; import { isMatching, P } from "ts-pattern"; import type { TimerEntry } from "../models"; -import { createRule, getPhaseKindOfFunction, isInstanceIDEqual } from "../utils"; +import { createRule, getPhaseKindOfFunction, isInstanceIdEqual } from "../utils"; // #region Rule Metadata @@ -80,7 +80,7 @@ export default createRule<[], MessageID>({ const sEntries: TimerEntry[] = []; const rEntries: TimerEntry[] = []; function isInverseEntry(a: TimerEntry, b: TimerEntry) { - return isInstanceIDEqual(a.timerId, b.timerId, context); + return isInstanceIdEqual(a.timerId, b.timerId, context); } return { [":function"](node: AST.TSESTreeFunction) { diff --git a/packages/plugins/eslint-plugin-react-web-api/src/utils/index.ts b/packages/plugins/eslint-plugin-react-web-api/src/utils/index.ts index e620f57f5d..66a079787c 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/utils/index.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/utils/index.ts @@ -1,4 +1,3 @@ export * from "./create-rule"; -export * from "./get-instance-id"; export * from "./get-phase-kind-of-function"; -export * from "./is-instance-id-equal"; +export * from "./is-instance-Id-equal"; diff --git a/packages/plugins/eslint-plugin-react-web-api/src/utils/is-instance-id-equal.ts b/packages/plugins/eslint-plugin-react-web-api/src/utils/is-instance-id-equal.ts index fc912f9efe..e981ab12a0 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/utils/is-instance-id-equal.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/utils/is-instance-id-equal.ts @@ -1,10 +1,9 @@ -import * as AST from "@eslint-react/ast"; import type { RuleContext } from "@eslint-react/shared"; import * as VAR from "@eslint-react/var"; import type { TSESTree } from "@typescript-eslint/types"; -export function isInstanceIDEqual(a: TSESTree.Node, b: TSESTree.Node, context: RuleContext) { - return AST.isNodeEqual(a, b) || VAR.isNodeValueEqual(a, b, [ +export function isInstanceIdEqual(a: TSESTree.Node, b: TSESTree.Node, context: RuleContext) { + return VAR.isVariableIdEqual(a, b, [ context.sourceCode.getScope(a), context.sourceCode.getScope(b), ]); diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index b6cbfa9718..b009efccba 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -27,6 +27,7 @@ import noForwardRef from "./rules/no-forward-ref"; import noImplicitKey from "./rules/no-implicit-key"; import noLeakedConditionalRendering from "./rules/no-leaked-conditional-rendering"; import noMissingComponentDisplayName from "./rules/no-missing-component-display-name"; +import noMissingContextDisplayName from "./rules/no-missing-context-display-name"; import noMissingKey from "./rules/no-missing-key"; import noNestedComponents from "./rules/no-nested-components"; import noPropTypes from "./rules/no-prop-types"; @@ -85,6 +86,7 @@ export const plugin = { "no-implicit-key": noImplicitKey, "no-leaked-conditional-rendering": noLeakedConditionalRendering, "no-missing-component-display-name": noMissingComponentDisplayName, + "no-missing-context-display-name": noMissingContextDisplayName, "no-missing-key": noMissingKey, "no-nested-components": noNestedComponents, "no-prop-types": noPropTypes, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.md b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.md index 9792d448b3..d3783d8ee6 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.md +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.md @@ -98,3 +98,10 @@ export default function Button() { - [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.ts) - [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.spec.ts) + +--- + +## See Also + +- [`no-missing-context-display-name`](./no-missing-context-display-name)\ + Enforces that all contexts have a `displayName` which React can use as its `displayName` in devtools. diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.md b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.md new file mode 100644 index 0000000000..0bb09726ef --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.md @@ -0,0 +1,54 @@ +--- +title: no-missing-context-display-name +--- + +**Full Name in `eslint-plugin-react-x`** + +```plain copy +react-x/no-missing-context-display-name +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```plain copy +@eslint-react/no-missing-context-display-name +``` + +**Features** + +`🔍` + +## What it does + +Enforces that all contexts have a `displayName` which React can use as its `displayName` in devtools. + +## Examples + +### Failing + +```tsx +import React from "react"; + +const MyContext = React.createContext(); +``` + +### Passing + +```tsx +import React from "react"; + +const MyContext = React.createContext(); +MyContext.displayName = "MyContext"; +``` + +## Implementation + +- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts) +- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.spec.ts) + +--- + +## See Also + +- [`no-missing-component-display-name`](./no-missing-component-display-name)\ + Enforces that all components have a `displayName` which React can use as its `displayName` in devtools. diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.spec.ts new file mode 100644 index 0000000000..7c01158134 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.spec.ts @@ -0,0 +1,68 @@ +import { allFunctions, ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./no-missing-context-display-name"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [ + { + code: /* tsx */ `createContext();`, + errors: [{ messageId: "noMissingContextDisplayName" }], + }, + { + code: /* tsx */ `const ctx = createContext();`, + errors: [{ messageId: "noMissingContextDisplayName" }], + }, + { + code: /* tsx */ ` + const ctx1 = createContext(); + const ctx2 = createContext(); + ctx1.displayName = "ctx"; + `, + errors: [{ messageId: "noMissingContextDisplayName" }], + }, + { + code: /* tsx */ ` + const ctx = createContext(); + ctx.displayname = "ctx"; + `, + errors: [{ messageId: "noMissingContextDisplayName" }], + }, + { + code: /* tsx */ ` + createContext(); + ctx.displayName = "ctx"; + `, + errors: [{ messageId: "noMissingContextDisplayName" }], + }, + ], + valid: [ + ...allFunctions, + /* tsx */ `const ctx = createContext(); ctx.displayName = "ctx";`, + /* tsx */ ` + const ctx = createContext(); + const displayName = "ctx"; + ctx.displayName = displayName; + `, + /* tsx */ ` + const ctx1 = createContext(); + const ctx2 = createContext(); + ctx1.displayName = "ctx1"; + ctx2.displayName = "ctx2"; + `, + /* tsx */ ` + const ctx1 = createContext(); + const ctx2 = createContext(); + const displayName = "ctx"; + ctx1.displayName = displayName; + ctx2.displayName = displayName; + `, + /* tsx */ ` + const ctx1 = createContext(); + const ctx2 = createContext(); + { + const displayName = "ctx"; + ctx1.displayName = displayName; + ctx2.displayName = displayName; + } + `, + ], +}); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts new file mode 100644 index 0000000000..b879fdc86d --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts @@ -0,0 +1,73 @@ +import { DISPLAY_NAME_ASSIGNMENT_SELECTOR, isCreateContextCall } from "@eslint-react/core"; +import type { RuleFeature } from "@eslint-react/shared"; +import * as VAR from "@eslint-react/var"; +import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import type { CamelCase } from "string-ts"; + +import { createRule } from "../utils"; + +export const RULE_NAME = "no-missing-context-display-name"; + +export const RULE_FEATURES = [ + "CHK", +] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + docs: { + description: "require 'displayName' for contexts.", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + messages: { + noMissingContextDisplayName: "Add missing 'displayName' for context.", + }, + schema: [], + }, + name: RULE_NAME, + create(context) { + if (!context.sourceCode.text.includes("createContext")) return {}; + const contexts = new Set(); + const displayNameAssignments = new Set(); + return { + CallExpression(node) { + if (!isCreateContextCall(node, context)) return; + contexts.add(node); + }, + [DISPLAY_NAME_ASSIGNMENT_SELECTOR](node) { + displayNameAssignments.add(node); + }, + "Program:exit"() { + for (const ctx of contexts) { + const id = VAR.getVariableId(ctx); + if (id == null) { + context.report({ + messageId: "noMissingContextDisplayName", + node: ctx, + }); + continue; + } + const hasDisplayNameAssignment = [...displayNameAssignments].some((node) => { + const left = node.left; + if (left.type !== T.MemberExpression) return false; + const object = left.object; + return VAR.isVariableIdEqual(id, object, [ + context.sourceCode.getScope(id), + context.sourceCode.getScope(object), + ]); + }); + if (!hasDisplayNameAssignment) { + context.report({ + messageId: "noMissingContextDisplayName", + node: ctx, + }); + } + } + }, + }; + }, + defaultOptions: [], +}); diff --git a/packages/plugins/eslint-plugin/src/configs/all.ts b/packages/plugins/eslint-plugin/src/configs/all.ts index a79a3e8834..40c83997f1 100644 --- a/packages/plugins/eslint-plugin/src/configs/all.ts +++ b/packages/plugins/eslint-plugin/src/configs/all.ts @@ -35,6 +35,7 @@ export const rules = { "@eslint-react/no-forward-ref": "warn", "@eslint-react/no-implicit-key": "warn", "@eslint-react/no-missing-component-display-name": "warn", + "@eslint-react/no-missing-context-display-name": "warn", "@eslint-react/no-missing-key": "error", "@eslint-react/no-nested-components": "error", "@eslint-react/no-prop-types": "error", diff --git a/packages/plugins/eslint-plugin-react-web-api/src/utils/get-instance-id.ts b/packages/utilities/var/src/get-variable-id.ts similarity index 84% rename from packages/plugins/eslint-plugin-react-web-api/src/utils/get-instance-id.ts rename to packages/utilities/var/src/get-variable-id.ts index 2b6c412a83..20bdc363c5 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/utils/get-instance-id.ts +++ b/packages/utilities/var/src/get-variable-id.ts @@ -2,7 +2,7 @@ import { _ } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -export function getInstanceID(node: TSESTree.Node, prev?: TSESTree.Node) { +export function getVariableId(node: TSESTree.Node, prev?: TSESTree.Node) { switch (true) { case node.type === T.VariableDeclarator && node.init === prev: @@ -18,6 +18,6 @@ export function getInstanceID(node: TSESTree.Node, prev?: TSESTree.Node) { || node.parent === node: return _; default: - return getInstanceID(node.parent, node); + return getVariableId(node.parent, node); } } diff --git a/packages/utilities/var/src/index.ts b/packages/utilities/var/src/index.ts index b0acf71cdd..9b2ca8055c 100644 --- a/packages/utilities/var/src/index.ts +++ b/packages/utilities/var/src/index.ts @@ -2,9 +2,11 @@ export * from "./find-property-in-properties"; export * from "./find-variable"; export * from "./get-child-scopes"; export * from "./get-variable-declarator-id"; +export * from "./get-variable-id"; export * from "./get-variable-node"; export * from "./get-variables"; export * from "./is-initialized-from-source"; export * from "./is-node-value-equal"; +export * from "./is-variable-id-equal"; export * from "./lazy-value"; export * from "./value-construction"; diff --git a/packages/utilities/var/src/is-variable-id-equal.ts b/packages/utilities/var/src/is-variable-id-equal.ts new file mode 100644 index 0000000000..4ba657d133 --- /dev/null +++ b/packages/utilities/var/src/is-variable-id-equal.ts @@ -0,0 +1,16 @@ +import * as AST from "@eslint-react/ast"; +import type { Scope } from "@typescript-eslint/scope-manager"; +import type { TSESTree } from "@typescript-eslint/types"; + +import { isNodeValueEqual } from "./is-node-value-equal"; + +export function isVariableIdEqual( + a: TSESTree.Node, + b: TSESTree.Node, + initialScopes: [ + aScope: Scope, + bScope: Scope, + ], +) { + return AST.isNodeEqual(a, b) || isNodeValueEqual(a, b, initialScopes); +}