Skip to content
Merged
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/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"hooks-extra-prefer-use-state-lazy-initialization",
"---Naming Convention Rules---",
"naming-convention-component-name",
"naming-convention-context-name",
"naming-convention-filename",
"naming-convention-filename-extension",
"naming-convention-use-state",
Expand Down
1 change: 1 addition & 0 deletions apps/website/content/docs/rules/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ full: true
| Rule | ✅ | Features | Description |
| :------------------------------------------------------------- | :- | :------- | :------------------------------------------------------------------------------- |
| [`component-name`](./naming-convention-component-name) | 0️⃣ | `🔍` `⚙️` | Enforces naming conventions for components. |
| [`context-name`](./naming-convention-context-name) | 0️⃣ | `🔍` | Enforces naming conventions for context providers. |
| [`filename`](./naming-convention-filename) | 0️⃣ | `🔍` `⚙️` | Enforces naming convention for JSX files. |
| [`filename-extension`](./naming-convention-filename-extension) | 0️⃣ | `🔍` `⚙️` | Enforces consistent use of the JSX file extension. |
| [`use-state`](./naming-convention-use-state) | 0️⃣ | `🔍` | Enforces destructuring and symmetric naming of `useState` hook value and setter. |
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils/is-instance-id-equal.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable jsdoc/require-param */
import * as AST from "@eslint-react/ast";
import type { RuleContext } from "@eslint-react/shared";
import * as VAR from "@eslint-react/var";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@eslint-react/eff": "workspace:*",
"@eslint-react/jsx": "workspace:*",
"@eslint-react/shared": "workspace:*",
"@eslint-react/var": "workspace:*",
"@typescript-eslint/scope-manager": "^8.25.0",
"@typescript-eslint/type-utils": "^8.25.0",
"@typescript-eslint/types": "^8.25.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { name, version } from "../package.json";
import componentName from "./rules/component-name";
import contextName from "./rules/context-name";
import filename from "./rules/filename";
import filenameExtension from "./rules/filename-extension";
import useState from "./rules/use-state";
Expand All @@ -11,6 +12,7 @@ export const plugin = {
},
rules: {
"component-name": componentName,
"context-name": contextName,
filename,
"filename-extension": filenameExtension,
"use-state": useState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: context-name
---

**Full Name in `eslint-plugin-react-naming-convention`**

```plain copy
react-naming-convention/context-name
```

**Full Name in `@eslint-react/eslint-plugin`**

```plain copy
@eslint-react/naming-convention/context-name
```

**Features**

`🔍`

## What it does

Enforces naming conventions for context providers.

## Examples

### Failing

```tsx
const Theme = createContext({});
```

### Passing

```tsx
const ThemeContext = createContext({});
```

## Implementation

- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts)
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.spec.ts)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { allFunctions, ruleTester } from "../../../../../test";
import rule, { RULE_NAME } from "./context-name";

ruleTester.run(RULE_NAME, rule, {
invalid: [
{
code: `
import { createContext } from "react";
const Foo = createContext({});
`,
errors: [{ messageId: "contextName" }],
},
{
code: `
import { createContext } from "react";
const Ctx = createContext({});
`,
errors: [{ messageId: "contextName" }],
},
],
valid: [
...allFunctions,
/* tsx */ `
import { createContext } from "react";
const MyContext = createContext({});
`,
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getInstanceId, isCreateContextCall } from "@eslint-react/core";
import { _, identity } from "@eslint-react/eff";
import type { RuleFeature } from "@eslint-react/shared";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
import type { CamelCase } from "string-ts";
import { match, P } from "ts-pattern";

import { createRule } from "../utils";

export const RULE_NAME = "context-name";

export const RULE_FEATURES = [
"CHK",
] as const satisfies RuleFeature[];

export type MessageID = CamelCase<typeof RULE_NAME>;

export default createRule<[], MessageID>({
meta: {
type: "problem",
docs: {
description: "enforce context name to end with `Context`.",
},
messages: {
contextName: "Context name must end with `Context`.",
},
schema: [],
},
name: RULE_NAME,
create(context) {
if (!context.sourceCode.text.includes("createContext")) return {};
return {
CallExpression(node) {
if (!isCreateContextCall(context, node)) return;
const id = getInstanceId(node);
if (id == null) return;
const name = match(id)
.with({ type: T.Identifier, name: P.select() }, identity)
.with({ type: T.MemberExpression, property: { name: P.select(P.string) } }, identity)
.otherwise(() => _);
if (name == null) return;
if (name.endsWith("Context")) return;
context.report({
messageId: "contextName",
node: id,
});
},
};
},
defaultOptions: [],
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Prevents non-stable values (i.e. object literals) from being used as a value for

React will re-render all consumers of a context whenever the context value changes, and if the value is not stable, this can lead to unnecessary re-renders.

In React 19 and later, the [`Context` component can be used via `<Context>` instead of `<Context.Provider>`](https://react.dev/blog/2024/12/05/react-19#context-as-a-provider), so it is recommended to use the [`context-name`](./naming-convention-context-name) rule to avoid false negatives.

## Examples

### Failing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,92 @@ ruleTester.run(RULE_NAME, rule, {
},
],
},
{
code: /* tsx */ `
function App() {
const foo = {}
return <Context value={foo}></Context>;
}
`,
errors: [{
messageId: "unstableContextValue",
data: {
type: "object expression",
suggestion: "Consider wrapping it in a useMemo hook.",
},
}],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
function App() {
const foo = []
return <CONTEXT value={foo}></CONTEXT>
}
`,
errors: [
{
messageId: "unstableContextValue",
data: {
type: "array expression",
suggestion: "Consider wrapping it in a useMemo hook.",
},
},
],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
function App() {
const foo = []
return <ThemeContext value={foo}></ThemeContext>
}
`,
errors: [
{
messageId: "unstableContextValue",
data: {
type: "array expression",
suggestion: "Consider wrapping it in a useMemo hook.",
},
},
],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
function App() {
const foo = []
return <THEME_CONTEXT value={foo}></THEME_CONTEXT>
}
`,
errors: [
{
messageId: "unstableContextValue",
data: {
type: "array expression",
suggestion: "Consider wrapping it in a useMemo hook.",
},
},
],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
],
valid: [
...allValid,
Expand Down Expand Up @@ -127,5 +213,57 @@ ruleTester.run(RULE_NAME, rule, {
return <Context.Provider value={foo}></Context.Provider>;
}
`,
{
code: /* tsx */ `
function App() {
const foo = {}
return <Context value={foo}></Context>;
}
`,
settings: {
"react-x": {
version: "18.0.0",
},
},
},
{
code: /* tsx */ `
function App() {
const foo = []
return <CONTEXT value={foo}></CONTEXT>
}
`,
settings: {
"react-x": {
version: "18.0.0",
},
},
},
{
code: /* tsx */ `
function App() {
const foo = []
return <ThemeContext value={foo}></ThemeContext>
}
`,
settings: {
"react-x": {
version: "18.0.0",
},
},
},
{
code: /* tsx */ `
function App() {
const foo = []
return <THEME_CONTEXT value={foo}></THEME_CONTEXT>
}
`,
settings: {
"react-x": {
version: "18.0.0",
},
},
},
],
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as AST from "@eslint-react/ast";
import { isReactHookCall, useComponentCollector } from "@eslint-react/core";
import { getOrUpdate } from "@eslint-react/eff";
import type { RuleFeature } from "@eslint-react/shared";
import * as JSX from "@eslint-react/jsx";
import { getSettingsFromContext, type RuleFeature } from "@eslint-react/shared";
import * as VAR from "@eslint-react/var";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
import { compare } from "compare-versions";

import { createRule } from "../utils";

Expand All @@ -30,19 +32,18 @@ export default createRule<[], MessageID>({
},
name: RULE_NAME,
create(context) {
const { version } = getSettingsFromContext(context);
const isReact18OrBelow = compare(version, "19.0.0", "<");
const { ctx, listeners } = useComponentCollector(context);
const constructions = new Map<AST.TSESTreeFunction, VAR.ValueConstruction[]>();

return {
...listeners,
JSXOpeningElement(node) {
const openingElementName = node.name;
if (openingElementName.type !== T.JSXMemberExpression) {
return;
}
if (openingElementName.property.name !== "Provider") {
return;
}
const fullName = JSX.getElementName(node.parent);
const selfName = fullName.split(".").at(-1);
if (selfName == null) return;
if (!isContextName(selfName, isReact18OrBelow)) return;
const functionEntry = ctx.getCurrentEntry();
if (functionEntry == null) return;
const attribute = node
Expand Down Expand Up @@ -86,3 +87,11 @@ export default createRule<[], MessageID>({
},
defaultOptions: [],
});

function isContextName(name: string, isReact18OrBelow: boolean): boolean {
if (name === "Provider") return true;
if (!isReact18OrBelow) {
return name.endsWith("Context") || name.endsWith("CONTEXT");
}
return false;
}
1 change: 1 addition & 0 deletions packages/plugins/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const rules = {

// Part: Naming Convention
"@eslint-react/naming-convention/component-name": "warn",
"@eslint-react/naming-convention/context-name": "warn",
"@eslint-react/naming-convention/filename": "warn",
"@eslint-react/naming-convention/filename-extension": "warn",
"@eslint-react/naming-convention/use-state": "warn",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.