Skip to content

Commit 07bcf8d

Browse files
committed
WIP
1 parent 1470959 commit 07bcf8d

File tree

7 files changed

+206
-0
lines changed

7 files changed

+206
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"no-missing-context-display-name",
3838
"no-missing-key",
3939
"no-misused-capture-owner-stack",
40+
"no-misused-use-memo",
4041
"no-nested-component-definitions",
4142
"no-nested-lazy-component-declarations",
4243
"no-prop-types",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
title: no-misused-use-memo
3+
---
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
2+
import tsx from "dedent";
3+
4+
import { ruleTester } from "../../../../../test";
5+
import rule, { RULE_NAME } from "./no-misused-use-memo";
6+
7+
ruleTester.run(RULE_NAME, rule, {
8+
invalid: [
9+
{
10+
code: tsx`
11+
import React, { useMemo } from "react";
12+
13+
function MyComponent(props) {
14+
const data = useMemo(() => {
15+
fetch("/api/data"); // Side effect
16+
return props.initialData;
17+
}, [props.initialData]);
18+
19+
return <div>{data}</div>;
20+
}
21+
`,
22+
errors: [
23+
{
24+
type: T.CallExpression,
25+
messageId: "noMisusedUseMemo",
26+
},
27+
],
28+
},
29+
],
30+
valid: [
31+
{
32+
code: `import * as React from 'react';`,
33+
},
34+
{
35+
code: `import {useState} from 'react';`,
36+
},
37+
{
38+
code: `import {} from 'react';`,
39+
},
40+
{
41+
code: `import * as React from "react";`,
42+
},
43+
{
44+
code: `import {useState} from "react";`,
45+
},
46+
{
47+
code: `import {} from "react";`,
48+
},
49+
],
50+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as AST from "@eslint-react/ast";
2+
import { isUseMemoCall } from "@eslint-react/core";
3+
import type { RuleContext, RuleFeature } from "@eslint-react/shared";
4+
import { SIDE_EFFECT_FUNCTION_NAMES } from "@eslint-react/shared";
5+
import { AST_NODE_TYPES as T, type TSESTree } from "@typescript-eslint/types";
6+
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
7+
import type { CamelCase } from "string-ts";
8+
import { match } from "ts-pattern";
9+
10+
import { createRule } from "../utils";
11+
12+
export const RULE_NAME = "no-misused-use-memo";
13+
14+
export const RULE_FEATURES = [
15+
"EXP",
16+
] as const satisfies RuleFeature[];
17+
18+
export type MessageID = CamelCase<typeof RULE_NAME>;
19+
20+
export default createRule<[], MessageID>({
21+
meta: {
22+
type: "problem",
23+
docs: {
24+
description: "Prevents incorrect usage of `useMemo`.",
25+
[Symbol.for("rule_features")]: RULE_FEATURES,
26+
},
27+
messages: {
28+
noMisusedUseMemo: "`useMemo` must have no side effects and return a value.",
29+
},
30+
schema: [],
31+
},
32+
name: RULE_NAME,
33+
create,
34+
defaultOptions: [],
35+
});
36+
37+
export function create(context: RuleContext<MessageID, []>): RuleListener {
38+
// Fast path: skip if `useMemo` is not present in the file
39+
if (!context.sourceCode.text.includes("useMemo")) return {};
40+
41+
const state = {
42+
isWithinUseMemo: false,
43+
};
44+
45+
return {
46+
CallExpression(node) {
47+
if (isUseMemoCall(node)) {
48+
state.isWithinUseMemo = true;
49+
}
50+
if (!state.isWithinUseMemo) return;
51+
const name = match(node.callee)
52+
.with({ type: T.Identifier }, (n) => n.name)
53+
.with({ type: T.MemberExpression, property: { type: T.Identifier } }, (n) => n.property.name)
54+
.otherwise(() => null);
55+
if (name == null) return;
56+
if (!SIDE_EFFECT_FUNCTION_NAMES.some((effectName) => name.startsWith(effectName))) return;
57+
context.report({
58+
messageId: "noMisusedUseMemo",
59+
node,
60+
});
61+
},
62+
"CallExpression:exit"(node) {
63+
if (isUseMemoCall(node)) {
64+
state.isWithinUseMemo = false;
65+
}
66+
},
67+
};
68+
}

packages/shared/docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
- [RE\_REGEXP\_STR](variables/RE_REGEXP_STR.md)
5050
- [RE\_SNAKE\_CASE](variables/RE_SNAKE_CASE.md)
5151
- [RE\_TS\_EXT](variables/RE_TS_EXT.md)
52+
- [SIDE\_EFFECT\_FUNCTION\_NAMES](variables/SIDE_EFFECT_FUNCTION_NAMES.md)
5253
- [WEBSITE\_URL](variables/WEBSITE_URL.md)
5354

5455
## Functions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[**@eslint-react/shared**](../README.md)
2+
3+
***
4+
5+
[@eslint-react/shared](../README.md) / SIDE\_EFFECT\_FUNCTION\_NAMES
6+
7+
# Variable: SIDE\_EFFECT\_FUNCTION\_NAMES
8+
9+
> `const` **SIDE\_EFFECT\_FUNCTION\_NAMES**: `string`[]

packages/shared/src/constants.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,77 @@ export const RE_COMPONENT_NAME_LOOSE = /^_?[A-Z]/u;
110110
export const RE_HOOK_NAME = /^use/u;
111111

112112
// #endregion
113+
114+
// #region Static Data
115+
116+
export const SIDE_EFFECT_FUNCTION_NAMES: string[] = [
117+
// Data Operations (CRUD & similar)
118+
"set",
119+
"add",
120+
"create",
121+
"update",
122+
"save",
123+
"delete",
124+
"remove",
125+
"clear",
126+
"reset",
127+
"fetch",
128+
"patch",
129+
"put",
130+
"insert",
131+
"upsert",
132+
"write",
133+
"modify",
134+
"change",
135+
"erase",
136+
"destroy",
137+
"load",
138+
"get",
139+
"retrieve",
140+
141+
// State & Visibility
142+
"open",
143+
"close",
144+
"show",
145+
"hide",
146+
"start",
147+
"stop",
148+
"toggle",
149+
"enable",
150+
"disable",
151+
"activate",
152+
"deactivate",
153+
"begin",
154+
"end",
155+
"pause",
156+
"resume",
157+
158+
// Events & Communication
159+
"dispatch",
160+
"send",
161+
"trigger",
162+
"post",
163+
"log",
164+
"report",
165+
"track",
166+
"emit",
167+
"broadcast",
168+
"notify",
169+
"publish",
170+
"subscribe",
171+
"unsubscribe",
172+
173+
// Navigation & Routing
174+
"navigate",
175+
"redirect",
176+
"push",
177+
"replace",
178+
179+
// Browser/System Actions
180+
"alert",
181+
"confirm",
182+
"prompt",
183+
"print",
184+
];
185+
186+
// #endregion

0 commit comments

Comments
 (0)