Skip to content

Commit 3ee7bc2

Browse files
committed
fix: 'prefer-read-only-props' should not report only the first component
1 parent cbfc31b commit 3ee7bc2

File tree

5 files changed

+469
-428
lines changed

5 files changed

+469
-428
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
},
113113
"pnpm": {
114114
"overrides": {
115+
"@types/node": "20.14.11",
116+
"@types/react": "18.3.3",
117+
"@types/react-dom": "18.3.0",
115118
"@typescript-eslint/eslint-plugin": "8.0.0-alpha.47",
116119
"@typescript-eslint/parser": "8.0.0-alpha.47",
117120
"@typescript-eslint/rule-tester": "8.0.0-alpha.47",

packages/plugins/eslint-plugin-react-x/src/rules/prefer-read-only-props.spec.ts

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
55
invalid: [
66
{
77
code: /* tsx */ `
8+
import * as React from "react";
9+
810
const App = (props: { id: string; className: string }) => {
911
return <div id={props.id} className={props.className} />
1012
}
@@ -17,6 +19,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
1719
},
1820
{
1921
code: /* tsx */ `
22+
import * as React from "react";
23+
2024
function App(props: { id: string; className: string }) {
2125
return <div id={props.id} className={props.className} />
2226
}
@@ -29,6 +33,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
2933
},
3034
{
3135
code: /* tsx */ `
36+
import * as React from "react";
37+
3238
const App = function (props: { id: string; className: string }) {
3339
return <div id={props.id} className={props.className} />
3440
}
@@ -41,6 +47,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
4147
},
4248
{
4349
code: /* tsx */ `
50+
import * as React from "react";
51+
4452
const App = function ({ id, className }: { id: string; className: string }) {
4553
return <div id={id} className={className} />
4654
}
@@ -53,6 +61,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
5361
},
5462
{
5563
code: /* tsx */ `
64+
import * as React from "react";
65+
5666
const App = function ({ id, className }: { readonly id: string; className: string }) {
5767
return <div id={id} className={className} />
5868
}
@@ -65,8 +75,9 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
6575
},
6676
{
6777
code: /* tsx */ `
68-
import { FC } from "react";
69-
const App: FC<{ id: string; className: string }> = (props) => {
78+
import * as React from "react";
79+
80+
const App: React.FC<{ id: string; className: string }> = (props) => {
7081
return <div id={props.id} className={props.className} />
7182
}
7283
`,
@@ -78,8 +89,9 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
7889
},
7990
{
8091
code: /* tsx */ `
81-
import { FC } from "react";
82-
const App: FC<{ id: string; className: string }> = ({ id, className }) => {
92+
import * as React from "react";
93+
94+
const App: React.FC<{ id: string; className: string }> = ({ id, className }) => {
8395
return <div id={id} className={className} />
8496
}
8597
`,
@@ -91,7 +103,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
91103
},
92104
{
93105
code: /* tsx */ `
94-
import React from "react";
106+
import * as React from "react";
95107
96108
export const App: React.FC<{ id: string; className: string }> = (props) => {
97109
return <div className={props.className} id={props.id} />
@@ -105,7 +117,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
105117
},
106118
{
107119
code: /* tsx */ `
108-
import React from "react";
120+
import * as React from "react";
109121
110122
export const App: React.FC<{ readonly id: string; readonly className: string } | { id: string; className: string }> = (props) => {
111123
return <div className={props.className} id={props.id} />
@@ -119,7 +131,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
119131
},
120132
{
121133
code: /* tsx */ `
122-
import { FC } from "react";
134+
import * as React from "react";
123135
124136
const defaultProps = { id: "default-id", className: "default-class" };
125137
type Props = typeof defaultProps;
@@ -136,6 +148,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
136148
},
137149
{
138150
code: /* tsx */ `
151+
import * as React from "react";
152+
139153
interface HSV {
140154
h: number;
141155
s: number;
@@ -158,6 +172,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
158172
},
159173
{
160174
code: /* tsx */ `
175+
import * as React from "react";
176+
161177
interface HSV {
162178
h: number;
163179
s: number;
@@ -180,9 +196,9 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
180196
},
181197
{ // memo with generic
182198
code: /* tsx */ `
183-
import React, { memo } from "react";
199+
import * as React from "react";
184200
185-
const App = memo(({ id, className }: { id: string; className: string }) => {
201+
const App = React.memo(({ id, className }: { id: string; className: string }) => {
186202
return <div id={id} className={className} />
187203
});
188204
`,
@@ -194,11 +210,11 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
194210
},
195211
{ // memo with generic and default props
196212
code: /* tsx */ `
197-
import React, { memo } from "react";
213+
import * as React from "react";
198214
199215
const defaultProps = { id: "default-id", className: "default-class" };
200216
type Props = typeof defaultProps;
201-
const App = memo(({ id, className }: Props) => {
217+
const App = React.memo(({ id, className }: Props) => {
202218
return <div id={id} className={className} />
203219
});
204220
`,
@@ -210,9 +226,9 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
210226
},
211227
{ // forwardRef with generic
212228
code: /* tsx */ `
213-
import React, { forwardRef } from "react";
229+
import * as React from "react";
214230
215-
const App = forwardRef<HTMLDivElement, { id: string; className: string }>(({ id, className }, ref) => {
231+
const App = React.forwardRef<HTMLDivElement, { id: string; className: string }>(({ id, className }, ref) => {
216232
return <div id={id} className={className} ref={ref} />
217233
});
218234
`,
@@ -224,11 +240,11 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
224240
},
225241
{ // forwardRef with generic and default props
226242
code: /* tsx */ `
227-
import React, { forwardRef } from "react";
243+
import * as React from "react";
228244
229245
const defaultProps = { id: "default-id", className: "default-class" };
230246
type Props = typeof defaultProps;
231-
const App = forwardRef<HTMLDivElement, Props>(({ id, className }, ref) => {
247+
const App = React.forwardRef<HTMLDivElement, Props>(({ id, className }, ref) => {
232248
return <div id={id} className={className} ref={ref} />
233249
});
234250
`,
@@ -240,9 +256,9 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
240256
},
241257
{ // memo and forwardRef with generic
242258
code: /* tsx */ `
243-
import React, { memo, forwardRef } from "react";
259+
import * as React from "react";
244260
245-
const App = memo(forwardRef<HTMLDivElement, { id: string; className: string }>(({ id, className }, ref) => {
261+
const App = React.memo(React.forwardRef<HTMLDivElement, { id: string; className: string }>(({ id, className }, ref) => {
246262
return <div id={id} className={className} ref={ref} />
247263
}));
248264
`,
@@ -256,7 +272,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
256272
valid: [
257273
...allValid,
258274
/* tsx */ `
259-
import React from "react";
275+
import * as React from "react";
260276
261277
type DeepReadonly<T> = Readonly<{[K in keyof T]: T[K] extends (number | string | symbol) ? Readonly<T[K]> : T[K] extends Array<infer A> ? Readonly<Array<DeepReadonly<A>>> : DeepReadonly<T[K]>;}>;
262278
@@ -265,32 +281,36 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
265281
}
266282
`,
267283
/* tsx */ `
268-
import React from "react";
284+
import * as React from "react";
269285
import { ReadonlyDeep } from "type-fest";
270286
271287
export const App: React.FC<ReadonlyDeep<{ id: string; className: string }>> = (props) => {
272288
return <div className={props.className} id={props.id} />
273289
}
274290
`,
275291
/* tsx */ `
292+
import * as React from "react";
293+
276294
const App = function ({ id, className }: { readonly id: string; readonly className: string }) {
277295
return <div id={id} className={className} />
278296
}
279297
`,
280298
/* tsx */ `
281-
import { FC } from "react";
282-
const App: FC<{ readonly id: string; readonly className: string }> = (props) => {
299+
import * as React from "react";
300+
301+
const App: React.FC<{ readonly id: string; readonly className: string }> = (props) => {
283302
return <div id={props.id} className={props.className} />
284303
}
285304
`,
286305
/* tsx */ `
287-
import { FC } from "react";
288-
const App: FC<{ readonly id: string; readonly className: string }> = ({ id, className }) => {
306+
import * as React from "react";
307+
308+
const App: React.FC<{ readonly id: string; readonly className: string }> = ({ id, className }) => {
289309
return <div id={id} className={className} />
290310
}
291311
`,
292312
/* tsx */ `
293-
import { FC } from "react";
313+
import * as React from "react";
294314
295315
const defaultProps = { id: "default-id", className: "default-class" } as const;
296316
type Props = typeof defaultProps;
@@ -300,29 +320,33 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
300320
}
301321
`,
302322
/* tsx */ `
303-
import { FC } from "react";
323+
import * as React from "react";
304324
305325
const defaultProps = { id: "default-id", className: "default-class" } as const;
306326
type Props = typeof defaultProps;
307-
const App: FC<Props> = ({ id, className }) => {
327+
const App: React.FC<Props> = ({ id, className }) => {
308328
return <div id={id} className={className} />
309329
}
310330
`,
311331
/* tsx */ `
312-
import { FC } from "react";
332+
import * as React from "react";
313333
314334
const defaultProps = { id: "default-id", className: "default-class" } as const;
315-
const App: FC<typeof defaultProps> = ({ id, className }) => {
335+
const App: React.FC<typeof defaultProps> = ({ id, className }) => {
316336
return <div id={id} className={className} />
317337
}
318338
`,
319339
/* tsx */ `
340+
import * as React from "react";
341+
320342
const defaultProps = { id: "default-id", className: "default-class" } as const;
321343
const App = ({ id, className }: typeof defaultProps) => {
322344
return <div id={id} className={className} />
323345
}
324346
`,
325347
/* tsx */ `
348+
import * as React from "react";
349+
326350
interface HSV {
327351
h: number;
328352
s: number;
@@ -338,6 +362,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
338362
}
339363
`,
340364
/* tsx */ `
365+
import * as React from "react";
366+
341367
interface HSV {
342368
h: number;
343369
s: number;
@@ -355,48 +381,48 @@ ruleTesterWithTypes.run(RULE_NAME, rule, {
355381
`,
356382
// memo with generic
357383
/* tsx */ `
358-
import React, { memo } from "react";
384+
import * as React from "react";
359385
360386
type Props = { readonly id: string; readonly className: string };
361-
const App = memo<Props>(({ id, className }) => {
387+
const App = React.memo<Props>(({ id, className }) => {
362388
return <div id={id} className={className} />
363389
});
364390
`,
365391
// memo with generic and default props
366392
/* tsx */ `
367-
import React, { memo } from "react";
393+
import * as React from "react";
368394
369395
const defaultProps = { id: "default-id", className: "default-class" } as const;
370396
type Props = typeof defaultProps;
371-
const App = memo<Props>(({ id, className }) => {
397+
const App = React.memo<Props>(({ id, className }) => {
372398
return <div id={id} className={className} />
373399
});
374400
`,
375401
// forwardRef with generic
376402
/* tsx */ `
377-
import React, { forwardRef } from "react";
403+
import * as React from "react";
378404
379405
type Props = { readonly id: string; readonly className: string };
380-
const App = forwardRef<HTMLDivElement, Props>(({ id, className }, ref) => {
406+
const App = React.forwardRef<HTMLDivElement, Props>(({ id, className }, ref) => {
381407
return <div id={id} className={className} ref={ref} />
382408
});
383409
`,
384410
// forwardRef with generic and default props
385411
/* tsx */ `
386-
import React, { forwardRef } from "react";
412+
import * as React from "react";
387413
388414
const defaultProps = { id: "default-id", className: "default-class" } as const;
389415
type Props = typeof defaultProps;
390-
const App = forwardRef<HTMLDivElement, Props>(({ id, className }, ref) => {
416+
const App = React.forwardRef<HTMLDivElement, Props>(({ id, className }, ref) => {
391417
return <div id={id} className={className} ref={ref} />
392418
});
393419
`,
394420
// memo and forwardRef with generic
395421
/* tsx */ `
396-
import React, { memo, forwardRef } from "react";
422+
import * as React from "react";
397423
398424
type Props = { readonly id: string; readonly className: string };
399-
const App = memo(forwardRef<HTMLDivElement, Props>(({ id, className }, ref) => {
425+
const App = React.memo(React.forwardRef<HTMLDivElement, Props>(({ id, className }, ref) => {
400426
return <div id={id} className={className} ref={ref} />
401427
}));
402428
`,

packages/plugins/eslint-plugin-react-x/src/rules/prefer-read-only-props.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function isReadonlyType(type: ts.Type, services: ParserServicesWithTypeInformati
1717
const im = getTypeImmutability(services.program, type);
1818
return isUnknown(im) || isImmutable(im) || isReadonlyShallow(im) || isReadonlyDeep(im);
1919
} catch {
20-
return isTypeReadonly(services.program, type);
20+
return true;
2121
}
2222
}
2323

@@ -44,7 +44,7 @@ export default createRule<[], MessageID>({
4444
const [props] = component.node.params;
4545
if (!props) continue;
4646
const propsType = getConstrainedTypeAtLocation(services, props);
47-
if (isReadonlyType(propsType, services)) return;
47+
if (isTypeReadonly(services.program, propsType) || isReadonlyType(propsType, services)) continue;
4848
context.report({ messageId: "PREFER_READ_ONLY_PROPS", node: props });
4949
}
5050
},

0 commit comments

Comments
 (0)