Skip to content

Commit b73d26b

Browse files
authored
feat(plugins/naming-convention): add 'context-name' rule (#952)
1 parent 081e1e6 commit b73d26b

File tree

13 files changed

+288
-8
lines changed

13 files changed

+288
-8
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"hooks-extra-prefer-use-state-lazy-initialization",
8383
"---Naming Convention Rules---",
8484
"naming-convention-component-name",
85+
"naming-convention-context-name",
8586
"naming-convention-filename",
8687
"naming-convention-filename-extension",
8788
"naming-convention-use-state",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ full: true
114114
| Rule || Features | Description |
115115
| :------------------------------------------------------------- | :- | :------- | :------------------------------------------------------------------------------- |
116116
| [`component-name`](./naming-convention-component-name) | 0️⃣ | `🔍` `⚙️` | Enforces naming conventions for components. |
117+
| [`context-name`](./naming-convention-context-name) | 0️⃣ | `🔍` | Enforces naming conventions for context providers. |
117118
| [`filename`](./naming-convention-filename) | 0️⃣ | `🔍` `⚙️` | Enforces naming convention for JSX files. |
118119
| [`filename-extension`](./naming-convention-filename-extension) | 0️⃣ | `🔍` `⚙️` | Enforces consistent use of the JSX file extension. |
119120
| [`use-state`](./naming-convention-use-state) | 0️⃣ | `🔍` | Enforces destructuring and symmetric naming of `useState` hook value and setter. |

packages/core/src/utils/is-instance-id-equal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable jsdoc/require-param */
12
import * as AST from "@eslint-react/ast";
23
import type { RuleContext } from "@eslint-react/shared";
34
import * as VAR from "@eslint-react/var";

packages/plugins/eslint-plugin-react-naming-convention/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@eslint-react/eff": "workspace:*",
5555
"@eslint-react/jsx": "workspace:*",
5656
"@eslint-react/shared": "workspace:*",
57+
"@eslint-react/var": "workspace:*",
5758
"@typescript-eslint/scope-manager": "^8.25.0",
5859
"@typescript-eslint/type-utils": "^8.25.0",
5960
"@typescript-eslint/types": "^8.25.0",

packages/plugins/eslint-plugin-react-naming-convention/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { name, version } from "../package.json";
22
import componentName from "./rules/component-name";
3+
import contextName from "./rules/context-name";
34
import filename from "./rules/filename";
45
import filenameExtension from "./rules/filename-extension";
56
import useState from "./rules/use-state";
@@ -11,6 +12,7 @@ export const plugin = {
1112
},
1213
rules: {
1314
"component-name": componentName,
15+
"context-name": contextName,
1416
filename,
1517
"filename-extension": filenameExtension,
1618
"use-state": useState,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
title: context-name
3+
---
4+
5+
**Full Name in `eslint-plugin-react-naming-convention`**
6+
7+
```plain copy
8+
react-naming-convention/context-name
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```plain copy
14+
@eslint-react/naming-convention/context-name
15+
```
16+
17+
**Features**
18+
19+
`🔍`
20+
21+
## What it does
22+
23+
Enforces naming conventions for context providers.
24+
25+
## Examples
26+
27+
### Failing
28+
29+
```tsx
30+
const Theme = createContext({});
31+
```
32+
33+
### Passing
34+
35+
```tsx
36+
const ThemeContext = createContext({});
37+
```
38+
39+
## Implementation
40+
41+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts)
42+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.spec.ts)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { allFunctions, ruleTester } from "../../../../../test";
2+
import rule, { RULE_NAME } from "./context-name";
3+
4+
ruleTester.run(RULE_NAME, rule, {
5+
invalid: [
6+
{
7+
code: `
8+
import { createContext } from "react";
9+
const Foo = createContext({});
10+
`,
11+
errors: [{ messageId: "contextName" }],
12+
},
13+
{
14+
code: `
15+
import { createContext } from "react";
16+
const Ctx = createContext({});
17+
`,
18+
errors: [{ messageId: "contextName" }],
19+
},
20+
],
21+
valid: [
22+
...allFunctions,
23+
/* tsx */ `
24+
import { createContext } from "react";
25+
const MyContext = createContext({});
26+
`,
27+
],
28+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { getInstanceId, isCreateContextCall } from "@eslint-react/core";
2+
import { _, identity } from "@eslint-react/eff";
3+
import type { RuleFeature } from "@eslint-react/shared";
4+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
5+
import type { CamelCase } from "string-ts";
6+
import { match, P } from "ts-pattern";
7+
8+
import { createRule } from "../utils";
9+
10+
export const RULE_NAME = "context-name";
11+
12+
export const RULE_FEATURES = [
13+
"CHK",
14+
] as const satisfies RuleFeature[];
15+
16+
export type MessageID = CamelCase<typeof RULE_NAME>;
17+
18+
export default createRule<[], MessageID>({
19+
meta: {
20+
type: "problem",
21+
docs: {
22+
description: "enforce context name to end with `Context`.",
23+
},
24+
messages: {
25+
contextName: "Context name must end with `Context`.",
26+
},
27+
schema: [],
28+
},
29+
name: RULE_NAME,
30+
create(context) {
31+
if (!context.sourceCode.text.includes("createContext")) return {};
32+
return {
33+
CallExpression(node) {
34+
if (!isCreateContextCall(context, node)) return;
35+
const id = getInstanceId(node);
36+
if (id == null) return;
37+
const name = match(id)
38+
.with({ type: T.Identifier, name: P.select() }, identity)
39+
.with({ type: T.MemberExpression, property: { name: P.select(P.string) } }, identity)
40+
.otherwise(() => _);
41+
if (name == null) return;
42+
if (name.endsWith("Context")) return;
43+
context.report({
44+
messageId: "contextName",
45+
node: id,
46+
});
47+
},
48+
};
49+
},
50+
defaultOptions: [],
51+
});

packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-context-value.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Prevents non-stable values (i.e. object literals) from being used as a value for
3131

3232
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.
3333

34+
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.
35+
3436
## Examples
3537

3638
### Failing

packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-context-value.spec.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,92 @@ ruleTester.run(RULE_NAME, rule, {
8888
},
8989
],
9090
},
91+
{
92+
code: /* tsx */ `
93+
function App() {
94+
const foo = {}
95+
return <Context value={foo}></Context>;
96+
}
97+
`,
98+
errors: [{
99+
messageId: "unstableContextValue",
100+
data: {
101+
type: "object expression",
102+
suggestion: "Consider wrapping it in a useMemo hook.",
103+
},
104+
}],
105+
settings: {
106+
"react-x": {
107+
version: "19.0.0",
108+
},
109+
},
110+
},
111+
{
112+
code: /* tsx */ `
113+
function App() {
114+
const foo = []
115+
return <CONTEXT value={foo}></CONTEXT>
116+
}
117+
`,
118+
errors: [
119+
{
120+
messageId: "unstableContextValue",
121+
data: {
122+
type: "array expression",
123+
suggestion: "Consider wrapping it in a useMemo hook.",
124+
},
125+
},
126+
],
127+
settings: {
128+
"react-x": {
129+
version: "19.0.0",
130+
},
131+
},
132+
},
133+
{
134+
code: /* tsx */ `
135+
function App() {
136+
const foo = []
137+
return <ThemeContext value={foo}></ThemeContext>
138+
}
139+
`,
140+
errors: [
141+
{
142+
messageId: "unstableContextValue",
143+
data: {
144+
type: "array expression",
145+
suggestion: "Consider wrapping it in a useMemo hook.",
146+
},
147+
},
148+
],
149+
settings: {
150+
"react-x": {
151+
version: "19.0.0",
152+
},
153+
},
154+
},
155+
{
156+
code: /* tsx */ `
157+
function App() {
158+
const foo = []
159+
return <THEME_CONTEXT value={foo}></THEME_CONTEXT>
160+
}
161+
`,
162+
errors: [
163+
{
164+
messageId: "unstableContextValue",
165+
data: {
166+
type: "array expression",
167+
suggestion: "Consider wrapping it in a useMemo hook.",
168+
},
169+
},
170+
],
171+
settings: {
172+
"react-x": {
173+
version: "19.0.0",
174+
},
175+
},
176+
},
91177
],
92178
valid: [
93179
...allValid,
@@ -127,5 +213,57 @@ ruleTester.run(RULE_NAME, rule, {
127213
return <Context.Provider value={foo}></Context.Provider>;
128214
}
129215
`,
216+
{
217+
code: /* tsx */ `
218+
function App() {
219+
const foo = {}
220+
return <Context value={foo}></Context>;
221+
}
222+
`,
223+
settings: {
224+
"react-x": {
225+
version: "18.0.0",
226+
},
227+
},
228+
},
229+
{
230+
code: /* tsx */ `
231+
function App() {
232+
const foo = []
233+
return <CONTEXT value={foo}></CONTEXT>
234+
}
235+
`,
236+
settings: {
237+
"react-x": {
238+
version: "18.0.0",
239+
},
240+
},
241+
},
242+
{
243+
code: /* tsx */ `
244+
function App() {
245+
const foo = []
246+
return <ThemeContext value={foo}></ThemeContext>
247+
}
248+
`,
249+
settings: {
250+
"react-x": {
251+
version: "18.0.0",
252+
},
253+
},
254+
},
255+
{
256+
code: /* tsx */ `
257+
function App() {
258+
const foo = []
259+
return <THEME_CONTEXT value={foo}></THEME_CONTEXT>
260+
}
261+
`,
262+
settings: {
263+
"react-x": {
264+
version: "18.0.0",
265+
},
266+
},
267+
},
130268
],
131269
});

0 commit comments

Comments
 (0)