Skip to content

Commit

Permalink
[Fix] boolean-prop-naming: handle React.FC, intersection, union types
Browse files Browse the repository at this point in the history
  • Loading branch information
mobily authored and ljharb committed Mar 11, 2022
1 parent cdfd558 commit 521f13b
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,7 +7,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange

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

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

## [7.29.3] - 2022.03.03
Expand Down
71 changes: 52 additions & 19 deletions lib/rules/boolean-prop-naming.js
Expand Up @@ -228,6 +228,47 @@ module.exports = {
args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
}

function getTypeAnnotation(component) {
// If this is a functional component that uses a global type, check it
if (
(component.node.type === 'FunctionDeclaration' || component.node.type === 'ArrowFunctionExpression')
&& component.node.params
&& component.node.params.length > 0
&& component.node.params[0].typeAnnotation
) {
return component.node.params[0].typeAnnotation.typeAnnotation;
}

if (
component.node.parent
&& component.node.parent.type === 'VariableDeclarator'
&& component.node.parent.id
&& component.node.parent.id.type === 'Identifier'
&& component.node.parent.id.typeAnnotation
&& component.node.parent.id.typeAnnotation.typeAnnotation
&& component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters
&& component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation'
) {
return component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters.params.find(
(param) => param.type === 'TSTypeReference'
);
}
}

function findTypeAnnotations(identifier, node) {
if (node.type === 'TSTypeLiteral') {
const currentNode = [].concat(
objectTypeAnnotations.get(identifier.name) || [],
node
);
objectTypeAnnotations.set(identifier.name, currentNode);
} else if (node.type === 'TSIntersectionType' || node.type === 'TSUnionType') {
node.types.forEach((type) => {
findTypeAnnotations(identifier, type);
});
}
}

// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
Expand Down Expand Up @@ -299,9 +340,7 @@ module.exports = {
},

TSTypeAliasDeclaration(node) {
if (node.typeAnnotation.type === 'TSTypeLiteral') {
objectTypeAnnotations.set(node.id.name, node.typeAnnotation);
}
findTypeAnnotations(node.id, node.typeAnnotation);
},

// eslint-disable-next-line object-shorthand
Expand All @@ -311,19 +350,11 @@ module.exports = {
}

const list = components.list();

Object.keys(list).forEach((component) => {
// If this is a functional component that uses a global type, check it
if (
(
list[component].node.type === 'FunctionDeclaration'
|| list[component].node.type === 'ArrowFunctionExpression'
)
&& list[component].node.params
&& list[component].node.params.length
&& list[component].node.params[0].typeAnnotation
) {
const typeNode = list[component].node.params[0].typeAnnotation;
const annotation = typeNode.typeAnnotation;
const annotation = getTypeAnnotation(list[component]);

if (annotation) {
let propType;
if (annotation.type === 'GenericTypeAnnotation') {
propType = objectTypeAnnotations.get(annotation.id.name);
Expand All @@ -334,10 +365,12 @@ module.exports = {
}

if (propType) {
validatePropNaming(
list[component].node,
propType.properties || propType.members
);
[].concat(propType).forEach((prop) => {
validatePropNaming(
list[component].node,
prop.properties || prop.members
);
});
}
}

Expand Down
189 changes: 189 additions & 0 deletions tests/lib/rules/boolean-prop-naming.js
Expand Up @@ -417,6 +417,86 @@ ruleTester.run('boolean-prop-naming', rule, {
features: ['ts'],
errors: [],
},
{
code: `
type Props = {
isEnabled: boolean
} & OtherProps
const HelloNew = (props: Props) => { return <div /> };
`,
options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }],
features: ['types'],
errors: [],
},
{
code: `
type Props = {
isEnabled: boolean
} & {
hasLOL: boolean
} & OtherProps
const HelloNew = (props: Props) => { return <div /> };
`,
options: [{ rule: '(is|has)[A-Z]([A-Za-z0-9]?)+' }],
features: ['types'],
errors: [],
},
{
code: `
type Props = {
isEnabled: boolean
}
const HelloNew: React.FC<Props> = (props) => { return <div /> };
`,
options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }],
features: ['types'],
errors: [],
},
{
code: `
type Props = {
isEnabled: boolean
} & {
hasLOL: boolean
}
const HelloNew: React.FC<Props> = (props) => { return <div /> };
`,
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
features: ['types'],
errors: [],
},
{
code: `
type Props = {
isEnabled: boolean
} | {
hasLOL: boolean
}
const HelloNew = (props: Props) => { return <div /> };
`,
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
features: ['types'],
errors: [],
},
{
code: `
type Props = {
isEnabled: boolean
} & ({
hasLOL: boolean
} | {
isLOL: boolean
})
const HelloNew = (props: Props) => { return <div /> };
`,
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
features: ['types'],
errors: [],
},
]),

invalid: parsers.all([
Expand Down Expand Up @@ -1050,5 +1130,114 @@ ruleTester.run('boolean-prop-naming', rule, {
},
],
},
{
code: `
type Props = {
enabled: boolean
} & OtherProps
const HelloNew = (props: Props) => { return <div /> };
`,
options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }],
features: ['types', 'no-ts-old'],
errors: [
{
message: 'Prop name (enabled) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)',
},
],
},
{
code: `
type Props = {
enabled: boolean
} & {
hasLOL: boolean
} & OtherProps
const HelloNew = (props: Props) => { return <div /> };
`,
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
features: ['types', 'no-ts-old'],
errors: [
{
message: 'Prop name (enabled) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
},
],
},
{
code: `
type Props = {
enabled: boolean
}
const HelloNew: React.FC<Props> = (props) => { return <div /> };
`,
options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }],
features: ['types', 'no-ts-old'],
errors: [
{
message: 'Prop name (enabled) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)',
},
],
},
{
code: `
type Props = {
enabled: boolean
} & {
hasLOL: boolean
}
const HelloNew: React.FC<Props> = (props) => { return <div /> };
`,
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
features: ['types', 'no-ts-old'],
errors: [
{
message: 'Prop name (enabled) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
},
],
},
{
code: `
type Props = {
enabled: boolean
} | {
hasLOL: boolean
}
const HelloNew = (props: Props) => { return <div /> };
`,
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
features: ['types', 'no-ts-old'],
errors: [
{
message: 'Prop name (enabled) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
},
],
},
{
code: `
type Props = {
enabled: boolean
} & ({
hasLOL: boolean
} | {
lol: boolean
})
const HelloNew = (props: Props) => { return <div /> };
`,
options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }],
features: ['types', 'no-ts-old'],
errors: [
{
message: 'Prop name (enabled) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
},
{
message: 'Prop name (lol) doesn\'t match rule (^(is|has)[A-Z]([A-Za-z0-9]?)+)',
},
],
},
]),
});

0 comments on commit 521f13b

Please sign in to comment.