Skip to content

Commit 86a3177

Browse files
mobilyljharb
authored andcommitted
[Fix] boolean-prop-naming: handle React.FC, intersection, union types
1 parent cdfd558 commit 86a3177

File tree

3 files changed

+262
-23
lines changed

3 files changed

+262
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
77

88
### Fixed
99
* [`no-unused-state`]: avoid a crash on a class field gDSFP ([#3236][] @ljharb)
10+
* [`boolean-prop-naming`]: handle React.FC, intersection, union types ([#3241][] @ljharb)
1011

12+
[#3241]: https://github.com/yannickcr/eslint-plugin-react/pull/3241
1113
[#3236]: https://github.com/yannickcr/eslint-plugin-react/issues/3236
1214

1315
## [7.29.3] - 2022.03.03

lib/rules/boolean-prop-naming.js

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,65 @@ module.exports = {
228228
args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
229229
}
230230

231+
function getComponentTypeAnnotation(component) {
232+
// If this is a functional component that uses a global type, check it
233+
if (
234+
(component.node.type === 'FunctionDeclaration' || component.node.type === 'ArrowFunctionExpression')
235+
&& component.node.params
236+
&& component.node.params.length > 0
237+
&& component.node.params[0].typeAnnotation
238+
) {
239+
return component.node.params[0].typeAnnotation.typeAnnotation;
240+
}
241+
242+
if (
243+
component.node.parent
244+
&& component.node.parent.type === 'VariableDeclarator'
245+
&& component.node.parent.id
246+
&& component.node.parent.id.type === 'Identifier'
247+
&& component.node.parent.id.typeAnnotation
248+
&& component.node.parent.id.typeAnnotation.typeAnnotation
249+
&& component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters
250+
&& (
251+
component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation'
252+
|| component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters.type === 'TypeParameterInstantiation'
253+
)
254+
) {
255+
return component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters.params.find(
256+
(param) => param.type === 'TSTypeReference' || param.type === 'GenericTypeAnnotation'
257+
);
258+
}
259+
}
260+
261+
function findAllTypeAnnotations(identifier, node) {
262+
if (node.type === 'TSTypeLiteral' || node.type === 'ObjectTypeAnnotation') {
263+
const currentNode = [].concat(
264+
objectTypeAnnotations.get(identifier.name) || [],
265+
node
266+
);
267+
objectTypeAnnotations.set(identifier.name, currentNode);
268+
} else if (
269+
node.type === 'TSParenthesizedType'
270+
&& (
271+
node.typeAnnotation.type === 'TSIntersectionType'
272+
|| node.typeAnnotation.type === 'TSUnionType'
273+
)
274+
) {
275+
node.typeAnnotation.types.forEach((type) => {
276+
findAllTypeAnnotations(identifier, type);
277+
});
278+
} else if (
279+
node.type === 'TSIntersectionType'
280+
|| node.type === 'TSUnionType'
281+
|| node.type === 'IntersectionTypeAnnotation'
282+
|| node.type === 'UnionTypeAnnotation'
283+
) {
284+
node.types.forEach((type) => {
285+
findAllTypeAnnotations(identifier, type);
286+
});
287+
}
288+
}
289+
231290
// --------------------------------------------------------------------------
232291
// Public
233292
// --------------------------------------------------------------------------
@@ -292,16 +351,11 @@ module.exports = {
292351
},
293352

294353
TypeAlias(node) {
295-
// Cache all ObjectType annotations, we will check them at the end
296-
if (node.right.type === 'ObjectTypeAnnotation') {
297-
objectTypeAnnotations.set(node.id.name, node.right);
298-
}
354+
findAllTypeAnnotations(node.id, node.right);
299355
},
300356

301357
TSTypeAliasDeclaration(node) {
302-
if (node.typeAnnotation.type === 'TSTypeLiteral') {
303-
objectTypeAnnotations.set(node.id.name, node.typeAnnotation);
304-
}
358+
findAllTypeAnnotations(node.id, node.typeAnnotation);
305359
},
306360

307361
// eslint-disable-next-line object-shorthand
@@ -311,19 +365,11 @@ module.exports = {
311365
}
312366

313367
const list = components.list();
368+
314369
Object.keys(list).forEach((component) => {
315-
// If this is a functional component that uses a global type, check it
316-
if (
317-
(
318-
list[component].node.type === 'FunctionDeclaration'
319-
|| list[component].node.type === 'ArrowFunctionExpression'
320-
)
321-
&& list[component].node.params
322-
&& list[component].node.params.length
323-
&& list[component].node.params[0].typeAnnotation
324-
) {
325-
const typeNode = list[component].node.params[0].typeAnnotation;
326-
const annotation = typeNode.typeAnnotation;
370+
const annotation = getComponentTypeAnnotation(list[component]);
371+
372+
if (annotation) {
327373
let propType;
328374
if (annotation.type === 'GenericTypeAnnotation') {
329375
propType = objectTypeAnnotations.get(annotation.id.name);
@@ -334,10 +380,12 @@ module.exports = {
334380
}
335381

336382
if (propType) {
337-
validatePropNaming(
338-
list[component].node,
339-
propType.properties || propType.members
340-
);
383+
[].concat(propType).forEach((prop) => {
384+
validatePropNaming(
385+
list[component].node,
386+
prop.properties || prop.members
387+
);
388+
});
341389
}
342390
}
343391

tests/lib/rules/boolean-prop-naming.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,86 @@ ruleTester.run('boolean-prop-naming', rule, {
417417
features: ['ts'],
418418
errors: [],
419419
},
420+
{
421+
code: `
422+
type Props = {
423+
isEnabled: boolean
424+
} & OtherProps
425+
const HelloNew = (props: Props) => { return <div /> };
426+
`,
427+
options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }],
428+
features: ['types'],
429+
errors: [],
430+
},
431+
{
432+
code: `
433+
type Props = {
434+
isEnabled: boolean
435+
} & {
436+
hasLOL: boolean
437+
} & OtherProps
438+
const HelloNew = (props: Props) => { return <div /> };
439+
`,
440+
options: [{ rule: '(is|has)[A-Z]([A-Za-z0-9]?)+' }],
441+
features: ['types'],
442+
errors: [],
443+
},
444+
{
445+
code: `
446+
type Props = {
447+
isEnabled: boolean
448+
}
449+
450+
const HelloNew: React.FC<Props> = (props) => { return <div /> };
451+
`,
452+
options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }],
453+
features: ['types'],
454+
errors: [],
455+
},
456+
{
457+
code: `
458+
type Props = {
459+
isEnabled: boolean
460+
} & {
461+
hasLOL: boolean
462+
}
463+
464+
const HelloNew: React.FC<Props> = (props) => { return <div /> };
465+
`,
466+
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
467+
features: ['types'],
468+
errors: [],
469+
},
470+
{
471+
code: `
472+
type Props = {
473+
isEnabled: boolean
474+
} | {
475+
hasLOL: boolean
476+
}
477+
478+
const HelloNew = (props: Props) => { return <div /> };
479+
`,
480+
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
481+
features: ['types'],
482+
errors: [],
483+
},
484+
{
485+
code: `
486+
type Props = {
487+
isEnabled: boolean
488+
} & ({
489+
hasLOL: boolean
490+
} | {
491+
isLOL: boolean
492+
})
493+
494+
const HelloNew = (props: Props) => { return <div /> };
495+
`,
496+
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
497+
features: ['types'],
498+
errors: [],
499+
},
420500
]),
421501

422502
invalid: parsers.all([
@@ -1050,5 +1130,114 @@ ruleTester.run('boolean-prop-naming', rule, {
10501130
},
10511131
],
10521132
},
1133+
{
1134+
code: `
1135+
type Props = {
1136+
enabled: boolean
1137+
} & OtherProps
1138+
1139+
const HelloNew = (props: Props) => { return <div /> };
1140+
`,
1141+
options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }],
1142+
features: ['types', 'no-ts-old'],
1143+
errors: [
1144+
{
1145+
message: 'Prop name (enabled) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)',
1146+
},
1147+
],
1148+
},
1149+
{
1150+
code: `
1151+
type Props = {
1152+
enabled: boolean
1153+
} & {
1154+
hasLOL: boolean
1155+
} & OtherProps
1156+
1157+
const HelloNew = (props: Props) => { return <div /> };
1158+
`,
1159+
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
1160+
features: ['types', 'no-ts-old'],
1161+
errors: [
1162+
{
1163+
message: 'Prop name (enabled) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
1164+
},
1165+
],
1166+
},
1167+
{
1168+
code: `
1169+
type Props = {
1170+
enabled: boolean
1171+
}
1172+
1173+
const HelloNew: React.FC<Props> = (props) => { return <div /> };
1174+
`,
1175+
options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }],
1176+
features: ['types', 'no-ts-old'],
1177+
errors: [
1178+
{
1179+
message: 'Prop name (enabled) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)',
1180+
},
1181+
],
1182+
},
1183+
{
1184+
code: `
1185+
type Props = {
1186+
enabled: boolean
1187+
} & {
1188+
hasLOL: boolean
1189+
}
1190+
1191+
const HelloNew: React.FC<Props> = (props) => { return <div /> };
1192+
`,
1193+
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
1194+
features: ['types', 'no-ts-old'],
1195+
errors: [
1196+
{
1197+
message: 'Prop name (enabled) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
1198+
},
1199+
],
1200+
},
1201+
{
1202+
code: `
1203+
type Props = {
1204+
enabled: boolean
1205+
} | {
1206+
hasLOL: boolean
1207+
}
1208+
1209+
const HelloNew = (props: Props) => { return <div /> };
1210+
`,
1211+
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
1212+
features: ['types', 'no-ts-old'],
1213+
errors: [
1214+
{
1215+
message: 'Prop name (enabled) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
1216+
},
1217+
],
1218+
},
1219+
{
1220+
code: `
1221+
type Props = {
1222+
enabled: boolean
1223+
} & ({
1224+
hasLOL: boolean
1225+
} | {
1226+
lol: boolean
1227+
})
1228+
1229+
const HelloNew = (props: Props) => { return <div /> };
1230+
`,
1231+
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
1232+
features: ['types', 'no-ts-old'],
1233+
errors: [
1234+
{
1235+
message: 'Prop name (enabled) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
1236+
},
1237+
{
1238+
message: 'Prop name (lol) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
1239+
},
1240+
],
1241+
},
10531242
]),
10541243
});

0 commit comments

Comments
 (0)