Skip to content

Commit 82de74d

Browse files
committed
Refactor tsl-rules-of-react rules
1 parent 69debdc commit 82de74d

16 files changed

+1207
-444
lines changed

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,16 @@
3838
"@tsconfig/node24": "^24.0.4",
3939
"@tsconfig/strictest": "^2.0.8",
4040
"@types/node": "^25.2.3",
41+
"@types/react": "^19.2.14",
42+
"@types/react-dom": "^19.2.3",
4143
"ansis": "^4.2.0",
44+
"dedent": "^1.7.1",
4245
"dprint": "^0.51.1",
43-
"effect": "^3.19.17",
46+
"effect": "^3.19.18",
47+
"preact": "^10.28.3",
4448
"publint": "^0.3.17",
49+
"react": "^19.2.4",
50+
"react-dom": "^19.2.4",
4551
"sort-package-json": "^3.6.1",
4652
"tinyglobby": "^0.2.15",
4753
"ts-pattern": "^5.9.0",
@@ -55,7 +61,7 @@
5561
"typescript": "^5.9.3",
5662
"vitest": "^4.0.18"
5763
},
58-
"packageManager": "pnpm@10.29.3",
64+
"packageManager": "pnpm@10.30.0",
5965
"engines": {
6066
"node": ">=24.0.0"
6167
}

packages/eslint-plugin-function/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
"dependencies": {
3737
"@eslint-react/ast": "^2.13.0",
3838
"@eslint-react/shared": "^2.13.0",
39-
"@typescript-eslint/scope-manager": "^8.55.0",
40-
"@typescript-eslint/type-utils": "^8.55.0",
41-
"@typescript-eslint/types": "^8.55.0",
42-
"@typescript-eslint/utils": "^8.55.0",
39+
"@typescript-eslint/scope-manager": "^8.56.0",
40+
"@typescript-eslint/type-utils": "^8.56.0",
41+
"@typescript-eslint/types": "^8.56.0",
42+
"@typescript-eslint/utils": "^8.56.0",
4343
"string-ts": "^2.3.1",
4444
"ts-api-utils": "^2.4.0",
4545
"ts-pattern": "^5.9.0",
46-
"typescript-eslint": "^8.55.0"
46+
"typescript-eslint": "^8.56.0"
4747
},
4848
"devDependencies": {
4949
"@local/eff": "workspace:*",

packages/tsl-rules-of-react/docs/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
| Variable | Description |
66
| ------ | ------ |
7-
| [rulesOfComponentHookFactories](variables/rulesOfComponentHookFactories.md) | - |
87
| [rulesOfEffect](variables/rulesOfEffect.md) | - |
98
| [rulesOfJsx](variables/rulesOfJsx.md) | TS checks for most of the issues described at https://react.dev/learn/writing-markup-with-jsx#the-rules-of-jsx by default, so there isn't much for us to implement. |
109
| [rulesOfKeys](variables/rulesOfKeys.md) | - |

packages/tsl-rules-of-react/docs/variables/rulesOfComponentHookFactories.md

Lines changed: 0 additions & 21 deletions
This file was deleted.

packages/tsl-rules-of-react/docs/variables/rulesOfProps.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,3 @@ const rulesOfProps: (options?: "off") => Rule<unknown>;
1515
## Returns
1616

1717
`Rule`\<`unknown`\>
18-
19-
## Todo
20-
21-
implement this

packages/tsl-rules-of-react/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export { rulesOfComponentHookFactories } from "./rules/rules-of-component-hook-factories";
21
export { rulesOfEffect } from "./rules/rules-of-effect";
32
export { rulesOfJsx } from "./rules/rules-of-jsx";
43
export { rulesOfKeys } from "./rules/rules-of-keys";

packages/tsl-rules-of-react/src/rules/rules-of-component-hook-factories.test.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/tsl-rules-of-react/src/rules/rules-of-component-hook-factories.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 250 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,262 @@
11
import tsx from "dedent";
22
import { ruleTester } from "tsl/ruleTester";
3+
import ts from "typescript";
34
import { expect, test } from "vitest";
45

56
import { messages, rulesOfJsx } from "./rules-of-jsx";
67

8+
const compilerOptions = {
9+
strict: true,
10+
jsx: ts.JsxEmit.ReactJSX,
11+
} as const satisfies ts.CompilerOptions;
12+
713
test("rules-of-jsx", () => {
814
const ret = ruleTester({
9-
invalid: [],
10-
ruleFn: rulesOfJsx,
1115
tsx: true,
12-
valid: [],
16+
ruleFn: rulesOfJsx,
17+
invalid: [
18+
// Rule000: Don't pass children as a prop
19+
{
20+
code: tsx`
21+
function Component() {
22+
return <div children="child" />;
23+
}
24+
`,
25+
compilerOptions,
26+
errors: [
27+
{ message: "Don't pass children as a prop. Instead, put the children between the opening and closing tags." },
28+
],
29+
},
30+
// Rule001: key after spread
31+
{
32+
code: tsx`
33+
function Component({ props }) {
34+
return <div {...props} key="id" />;
35+
}
36+
`,
37+
compilerOptions,
38+
errors: [
39+
{ message: "The 'key' prop must be placed before any spread props when using the new JSX transform." },
40+
],
41+
},
42+
{
43+
code: tsx`
44+
function Component({ props1, props2 }) {
45+
return <div {...props1} {...props2} key="id" />;
46+
}
47+
`,
48+
compilerOptions,
49+
errors: [
50+
{ message: "The 'key' prop must be placed before any spread props when using the new JSX transform." },
51+
],
52+
},
53+
{
54+
code: tsx`
55+
function Component({ props }) {
56+
return <div className="test" {...props} key="id" />;
57+
}
58+
`,
59+
compilerOptions,
60+
errors: [
61+
{ message: "The 'key' prop must be placed before any spread props when using the new JSX transform." },
62+
],
63+
},
64+
{
65+
code: tsx`
66+
function Component({ props }) {
67+
return <div {...props} key="id" className="test" />;
68+
}
69+
`,
70+
compilerOptions,
71+
errors: [
72+
{ message: "The 'key' prop must be placed before any spread props when using the new JSX transform." },
73+
],
74+
},
75+
// key after spread (with ref too, but only key triggers Rule000)
76+
{
77+
code: tsx`
78+
function Component({ props }) {
79+
return <div {...props} key="id" ref={null} />;
80+
}
81+
`,
82+
compilerOptions,
83+
errors: [
84+
{ message: "The 'key' prop must be placed before any spread props when using the new JSX transform." },
85+
],
86+
},
87+
],
88+
valid: [
89+
// key before spread
90+
{
91+
code: tsx`
92+
function Component({ props }) {
93+
return <div key="id" {...props} />;
94+
}
95+
`,
96+
compilerOptions,
97+
},
98+
// ref before spread
99+
{
100+
code: tsx`
101+
function Component({ props }) {
102+
return <div ref={null} {...props} />;
103+
}
104+
`,
105+
compilerOptions,
106+
},
107+
// key and ref before spread
108+
{
109+
code: tsx`
110+
function Component({ props }) {
111+
return <div key="id" ref={null} {...props} />;
112+
}
113+
`,
114+
compilerOptions,
115+
},
116+
// no spread
117+
{
118+
code: tsx`
119+
function Component() {
120+
return <div key="id" />;
121+
}
122+
`,
123+
compilerOptions,
124+
},
125+
{
126+
code: tsx`
127+
function Component() {
128+
return <div ref={null} />;
129+
}
130+
`,
131+
compilerOptions,
132+
},
133+
// spread without key or ref
134+
{
135+
code: tsx`
136+
function Component({ props }) {
137+
return <div {...props} />;
138+
}
139+
`,
140+
compilerOptions,
141+
},
142+
{
143+
code: tsx`
144+
function Component({ props }) {
145+
return <div key="id" key="id2" {...props} />;
146+
}
147+
`,
148+
compilerOptions,
149+
},
150+
{
151+
code: tsx`
152+
function Component({ props }) {
153+
return <div className="test" {...props} />;
154+
}
155+
`,
156+
compilerOptions,
157+
},
158+
// ref after spread (no rule for this — only key-after-spread is checked)
159+
{
160+
code: tsx`
161+
function Component({ props }) {
162+
return <div {...props} ref={null} />;
163+
}
164+
`,
165+
compilerOptions,
166+
},
167+
{
168+
code: tsx`
169+
function Component({ props1, props2 }) {
170+
return <div {...props1} {...props2} ref={null} />;
171+
}
172+
`,
173+
compilerOptions,
174+
},
175+
{
176+
code: tsx`
177+
function Component({ props }) {
178+
return <div className="test" {...props} ref={null} />;
179+
}
180+
`,
181+
compilerOptions,
182+
},
183+
{
184+
code: tsx`
185+
function Component({ props }) {
186+
return <div {...props} ref={null} className="test" />;
187+
}
188+
`,
189+
compilerOptions,
190+
},
191+
// non-new JSX transforms: key after spread is OK
192+
{
193+
code: tsx`
194+
function Component({ props }) {
195+
return <div {...props} key="id" />;
196+
}
197+
`,
198+
compilerOptions: {
199+
strict: true,
200+
jsx: ts.JsxEmit.None,
201+
},
202+
},
203+
{
204+
code: tsx`
205+
function Component({ props }) {
206+
return <div {...props} key="id" />;
207+
}
208+
`,
209+
compilerOptions: {
210+
strict: true,
211+
jsx: ts.JsxEmit.Preserve,
212+
},
213+
},
214+
{
215+
code: tsx`
216+
function Component({ props }) {
217+
return <div {...props} key="id" />;
218+
}
219+
`,
220+
compilerOptions: {
221+
strict: true,
222+
jsx: ts.JsxEmit.ReactNative,
223+
},
224+
},
225+
// non-new JSX transforms: ref after spread is OK
226+
{
227+
code: tsx`
228+
function Component({ props }) {
229+
return <div {...props} ref={null} />;
230+
}
231+
`,
232+
compilerOptions: {
233+
strict: true,
234+
jsx: ts.JsxEmit.None,
235+
},
236+
},
237+
{
238+
code: tsx`
239+
function Component({ props }) {
240+
return <div {...props} ref={null} />;
241+
}
242+
`,
243+
compilerOptions: {
244+
strict: true,
245+
jsx: ts.JsxEmit.Preserve,
246+
},
247+
},
248+
{
249+
code: tsx`
250+
function Component({ props }) {
251+
return <div {...props} ref={null} />;
252+
}
253+
`,
254+
compilerOptions: {
255+
strict: true,
256+
jsx: ts.JsxEmit.ReactNative,
257+
},
258+
},
259+
],
13260
});
14261
expect(ret).toBe(false);
15262
});

0 commit comments

Comments
 (0)