Skip to content

Commit 3ba947a

Browse files
authored
fix: reimplement 'naming-convention/use-state', closes #953 (#954)
1 parent 2ab9b77 commit 3ba947a

File tree

4 files changed

+74
-127
lines changed

4 files changed

+74
-127
lines changed

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

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

packages/plugins/eslint-plugin-react-naming-convention/src/rules/use-state.spec.ts

Lines changed: 28 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,20 @@ ruleTester.run(RULE_NAME, rule, {
1212
}
1313
`,
1414
errors: [{
15-
messageId: "unexpected",
16-
data: {
17-
setterName: "setState",
18-
stateName: "state",
19-
},
15+
messageId: "badValueOrSetterName",
2016
}],
2117
},
2218
{
2319
code: /* tsx */ `
2420
function Component() {
25-
const [state, setValue] = useState(0);
21+
const [state, set] = useState(0);
2622
2723
return <div />;
2824
}
2925
`,
3026
errors: [{
31-
messageId: "unexpected",
32-
data: {
33-
setterName: "setState",
34-
stateName: "state",
35-
},
27+
messageId: "badValueOrSetterName",
3628
}],
37-
settings: {
38-
"react-x": {
39-
skipImportCheck: true,
40-
},
41-
},
4229
},
4330
{
4431
code: /* tsx */ `
@@ -51,11 +38,7 @@ ruleTester.run(RULE_NAME, rule, {
5138
}
5239
`,
5340
errors: [{
54-
messageId: "unexpected",
55-
data: {
56-
setterName: "setState",
57-
stateName: "state",
58-
},
41+
messageId: "badValueOrSetterName",
5942
}],
6043
},
6144
{
@@ -69,11 +52,7 @@ ruleTester.run(RULE_NAME, rule, {
6952
}
7053
`,
7154
errors: [{
72-
messageId: "unexpected",
73-
data: {
74-
setterName: "setState",
75-
stateName: "state",
76-
},
55+
messageId: "badValueOrSetterName",
7756
}],
7857
},
7958
{
@@ -87,37 +66,35 @@ ruleTester.run(RULE_NAME, rule, {
8766
}
8867
`,
8968
errors: [{
90-
messageId: "unexpected",
91-
data: {
92-
setterName: "setState",
93-
stateName: "state",
94-
},
69+
messageId: "badValueOrSetterName",
9570
}],
9671
},
9772
{
9873
code: /* tsx */ `
9974
import { useState } from "react";
10075
10176
function Component() {
102-
const [state, setstate] = useLocalStorageState(0);
77+
const [{foo, bar, baz}, setFooBar] = useState({foo: "bbb", bar: "aaa", baz: "qqq"})
10378
10479
return <div />;
10580
}
10681
`,
10782
errors: [{
108-
messageId: "unexpected",
109-
data: {
110-
setterName: "setState",
111-
stateName: "state",
112-
},
83+
messageId: "badValueOrSetterName",
84+
}],
85+
},
86+
{
87+
code: /* tsx */ `
88+
import { useState } from 'react';
89+
90+
export function useTest(): [number, (n: number) => void] {
91+
const [count1, setCount] = useState(0);
92+
return [count1, setCount];
93+
}
94+
`,
95+
errors: [{
96+
messageId: "badValueOrSetterName",
11397
}],
114-
settings: {
115-
"react-x": {
116-
additionalHooks: {
117-
useState: ["useLocalStorageState"],
118-
},
119-
},
120-
},
12198
},
12299
],
123100
valid: [
@@ -149,19 +126,13 @@ ruleTester.run(RULE_NAME, rule, {
149126
return <div />;
150127
}
151128
`,
152-
{
153-
code: /* tsx */ `
154-
function Component() {
155-
const [state, setValue] = useState(0);
129+
/* tsx */ `
130+
import { useState } from 'react';
156131
157-
return <div />;
158-
}
159-
`,
160-
settings: {
161-
"react-x": {
162-
skipImportCheck: false,
163-
},
164-
},
165-
},
132+
export function useTest(): [number, (n: number) => void] {
133+
const [count, setCount] = useState(0);
134+
return [count, setCount];
135+
}
136+
`,
166137
],
167138
});

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

Lines changed: 42 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import {
2-
DEFAULT_COMPONENT_HINT,
3-
isReactHookCallWithNameLoose,
4-
isUseStateCall,
5-
useComponentCollector,
6-
} from "@eslint-react/core";
1+
import { _ } from "@eslint-react/eff";
72
import type { RuleFeature } from "@eslint-react/shared";
8-
import { getSettingsFromContext } from "@eslint-react/shared";
3+
import * as VAR from "@eslint-react/var";
4+
import type { TSESTree } from "@typescript-eslint/types";
95
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
10-
import { capitalize } from "string-ts";
6+
import { snakeCase } from "string-ts";
7+
import { match } from "ts-pattern";
118

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

@@ -17,15 +14,7 @@ export const RULE_FEATURES = [
1714
"CHK",
1815
] as const satisfies RuleFeature[];
1916

20-
export type MessageID = "unexpected";
21-
22-
function isSetterNameLoose(name: string) {
23-
// eslint-disable-next-line @typescript-eslint/no-misused-spread
24-
const fourthChar = [...name][3];
25-
26-
return name.startsWith("set")
27-
&& fourthChar === fourthChar?.toUpperCase();
28-
}
17+
export type MessageID = "badValueOrSetterName";
2918

3019
export default createRule<[], MessageID>({
3120
meta: {
@@ -35,66 +24,49 @@ export default createRule<[], MessageID>({
3524
[Symbol.for("rule_features")]: RULE_FEATURES,
3625
},
3726
messages: {
38-
unexpected: "An useState call is not destructured into value + setter pair.",
27+
badValueOrSetterName: "An useState call is not destructured into value + setter pair.",
3928
},
4029
schema: [],
4130
},
4231
name: RULE_NAME,
4332
create(context) {
44-
const alias = getSettingsFromContext(context).additionalHooks.useState ?? [];
45-
const {
46-
ctx,
47-
listeners,
48-
} = useComponentCollector(
49-
context,
50-
{
51-
collectDisplayName: false,
52-
collectHookCalls: true,
53-
hint: DEFAULT_COMPONENT_HINT,
54-
},
55-
);
56-
5733
return {
58-
...listeners,
59-
"Program:exit"(node) {
60-
const components = ctx.getAllComponents(node);
61-
for (const { hookCalls } of components.values()) {
62-
if (hookCalls.length === 0) {
63-
continue;
64-
}
65-
for (const hookCall of hookCalls) {
66-
if (!isUseStateCall(context, hookCall) && !alias.some(isReactHookCallWithNameLoose(hookCall))) {
67-
continue;
68-
}
69-
if (hookCall.parent.type !== T.VariableDeclarator) {
70-
continue;
71-
}
72-
const { id } = hookCall.parent;
73-
switch (id.type) {
74-
case T.Identifier: {
75-
context.report({ messageId: "unexpected", node: id });
76-
break;
77-
}
78-
case T.ArrayPattern: {
79-
const [state, setState] = id.elements;
80-
if (state?.type === T.ObjectPattern && setState?.type === T.Identifier) {
81-
if (!isSetterNameLoose(setState.name)) {
82-
context.report({ messageId: "unexpected", node: id });
83-
}
84-
break;
85-
}
86-
if (state?.type !== T.Identifier || setState?.type !== T.Identifier) {
87-
return;
88-
}
89-
const [stateName, setStateName] = [state.name, setState.name];
90-
const expectedSetterName = `set${capitalize(stateName)}`;
91-
if (setStateName === expectedSetterName) {
92-
return;
93-
}
94-
context.report({ messageId: "unexpected", node: id });
34+
"CallExpression[callee.name='useState']"(node: TSESTree.CallExpression) {
35+
if (node.parent.type !== T.VariableDeclarator) {
36+
context.report({ messageId: "badValueOrSetterName", node });
37+
}
38+
const id = VAR.getVariableId(node);
39+
if (id?.type !== T.ArrayPattern) {
40+
context.report({ messageId: "badValueOrSetterName", node });
41+
return;
42+
}
43+
const [value, setter] = id.elements;
44+
if (value == null || setter == null) {
45+
context.report({ messageId: "badValueOrSetterName", node });
46+
return;
47+
}
48+
const setterName = match(setter)
49+
.with({ type: T.Identifier }, (id) => id.name)
50+
.otherwise(() => _);
51+
if (setterName == null || !setterName.startsWith("set")) {
52+
context.report({ messageId: "badValueOrSetterName", node });
53+
return;
54+
}
55+
const valueName = match(value)
56+
.with({ type: T.Identifier }, (id) => id.name)
57+
.with({ type: T.ObjectPattern }, ({ properties }) => {
58+
const values = properties.reduce<string[]>((acc, prop) => {
59+
if (prop.type === T.Property && prop.key.type === T.Identifier) {
60+
return [...acc, prop.key.name];
9561
}
96-
}
97-
}
62+
return acc;
63+
}, []);
64+
return values.join("_");
65+
})
66+
.otherwise(() => _);
67+
if (valueName == null || `set_${valueName}` !== snakeCase(setterName)) {
68+
context.report({ messageId: "badValueOrSetterName", node });
69+
return;
9870
}
9971
},
10072
};

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)