Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/rules/prop-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,15 @@ This rule can take one argument to ignore some specific props during validation.

```js
...
"react/prop-types": [<enabled>, { ignore: <ignore>, customValidators: <customValidator>, skipUndeclared: <skipUndeclared> }]
"react/prop-types": [<enabled>, { ignore: <ignore>, customValidators: <customValidator>, skipUndeclared: <skipUndeclared>, skipUnexported: <skipUnexported> }]
...
```

- `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
- `ignore`: optional array of props name to ignore during validation.
- `customValidators`: optional array of validators used for propTypes validation.
- `skipUndeclared`: optional boolean to only error on components that have a propTypes block declared.
- `skipUnexported`: optional boolean to skip validation for components that are not exported from the module. This is useful when you have helper components or render functions that are only used internally within a file and do not need their own prop types.

### As for "exceptions"

Expand Down
108 changes: 108 additions & 0 deletions lib/rules/prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ module.exports = {
skipUndeclared: {
type: 'boolean',
},
skipUnexported: {
type: 'boolean',
},
},
additionalProperties: false,
}],
Expand All @@ -61,6 +64,7 @@ module.exports = {
const configuration = context.options[0] || {};
const ignored = configuration.ignore || [];
const skipUndeclared = configuration.skipUndeclared || false;
const skipUnexported = configuration.skipUnexported || false;

/**
* Checks if the prop is ignored
Expand All @@ -71,18 +75,85 @@ module.exports = {
return ignored.indexOf(name) !== -1;
}

const exportedIdentifiers = new Set();

/**
* Get the name of a component from its node
* @param {Object} component The component to get the name for
* @returns {string | null} The component name, or null if unnamed
*/
function getComponentName(component) {
const node = component.node;
if (node.id) {
return node.id.name;
}
if (node.parent && node.parent.type === 'VariableDeclarator' && node.parent.id) {
return node.parent.id.name;
}
return null;
}

/**
* Checks if a component node is directly in an export declaration
* @param {ASTNode} node The component AST node
* @returns {boolean} True if the component is directly exported
*/
function isDirectlyExported(node) {
let current = node;
while (current && current.parent) {
const parentType = current.parent.type;
if (parentType === 'ExportDefaultDeclaration' || parentType === 'ExportNamedDeclaration') {
return true;
}
if (parentType === 'AssignmentExpression' && current.parent.right === current) {
const left = current.parent.left;
if (
left.type === 'MemberExpression'
&& left.object.type === 'Identifier'
&& (
(left.object.name === 'module' && left.property.name === 'exports')
|| left.object.name === 'exports'
)
) {
return true;
}
}
if (parentType === 'VariableDeclarator' || parentType === 'VariableDeclaration') {
current = current.parent;
continue; // eslint-disable-line no-continue
}
break;
}
return false;
}

/**
* Checks if a component is exported from the module
* @param {Object} component The component to check
* @returns {boolean} True if the component is exported
*/
function isComponentExported(component) {
if (isDirectlyExported(component.node)) {
return true;
}
const name = getComponentName(component);
return name != null && exportedIdentifiers.has(name);
}

/**
* Checks if the component must be validated
* @param {Object} component The component to process
* @returns {boolean} True if the component must be validated, false if not.
*/
function mustBeValidated(component) {
const isSkippedByConfig = skipUndeclared && typeof component.declaredPropTypes === 'undefined';
const isSkippedAsUnexported = skipUnexported && !isComponentExported(component);
return !!(
component
&& component.usedPropTypes
&& !component.ignorePropsValidation
&& !isSkippedByConfig
&& !isSkippedAsUnexported
);
}

Expand Down Expand Up @@ -210,6 +281,43 @@ module.exports = {
}

return {
ExportDefaultDeclaration(node) {
if (node.declaration && node.declaration.type === 'Identifier') {
exportedIdentifiers.add(node.declaration.name);
}
},

ExportNamedDeclaration(node) {
if (node.specifiers) {
node.specifiers.forEach((specifier) => {
if (specifier.local) {
exportedIdentifiers.add(specifier.local.name);
}
});
}
},

AssignmentExpression(node) {
if (
node.left.type === 'MemberExpression'
&& node.left.object.type === 'Identifier'
) {
if (
node.left.object.name === 'module'
&& node.left.property.name === 'exports'
&& node.right.type === 'Identifier'
) {
exportedIdentifiers.add(node.right.name);
}
if (
node.left.object.name === 'exports'
&& node.right.type === 'Identifier'
) {
exportedIdentifiers.add(node.right.name);
}
}
},

'Program:exit'() {
const list = components.list();
// Report undeclared proptypes for all classes
Expand Down
212 changes: 212 additions & 0 deletions tests/lib/rules/prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,102 @@ ruleTester.run('prop-types', rule, {
`,
options: [{ skipUndeclared: false }],
},
{
code: `
function Hello(props) {
return <div>{props.name}</div>;
}
`,
options: [{ skipUnexported: true }],
},
{
code: `
const Hello = (props) => {
return <div>{props.name}</div>;
};
`,
options: [{ skipUnexported: true }],
},
{
code: `
class Hello extends React.Component {
render() {
return <div>{this.props.name}</div>;
}
}
`,
options: [{ skipUnexported: true }],
},
{
code: `
export function Hello(props) {
return <div>{props.name}</div>;
}
Hello.propTypes = {
name: PropTypes.string.isRequired
};
`,
options: [{ skipUnexported: true }],
},
{
code: `
export default function Hello(props) {
return <div>{props.name}</div>;
}
Hello.propTypes = {
name: PropTypes.string.isRequired
};
`,
options: [{ skipUnexported: true }],
},
{
code: `
function Hello(props) {
return <div>{props.name}</div>;
}
Hello.propTypes = {
name: PropTypes.string.isRequired
};
export default Hello;
`,
options: [{ skipUnexported: true }],
},
{
code: `
const Hello = (props) => {
return <div>{props.name}</div>;
};
Hello.propTypes = {
name: PropTypes.string.isRequired
};
export { Hello };
`,
options: [{ skipUnexported: true }],
},
{
code: `
function Hello(props) {
return <div>{props.name}</div>;
}
Hello.propTypes = {
name: PropTypes.string.isRequired
};
module.exports = Hello;
`,
options: [{ skipUnexported: true }],
},
{
code: `
function Hello(props) {
return <div>{props.name}</div>;
}
Hello.propTypes = {
name: PropTypes.string.isRequired
};
exports.Hello = Hello;
`,
options: [{ skipUnexported: true }],
},
{
// Async generator functions can't be components.
code: `
Expand Down Expand Up @@ -6727,6 +6823,122 @@ ruleTester.run('prop-types', rule, {
},
],
},
{
code: `
export function Hello(props) {
return <div>{props.name}</div>;
}
`,
options: [{ skipUnexported: true }],
errors: [
{
messageId: 'missingPropType',
data: { name: 'name' },
},
],
},
{
code: `
export default function Hello(props) {
return <div>{props.name}</div>;
}
`,
options: [{ skipUnexported: true }],
errors: [
{
messageId: 'missingPropType',
data: { name: 'name' },
},
],
},
{
code: `
export const Hello = (props) => {
return <div>{props.name}</div>;
};
`,
options: [{ skipUnexported: true }],
errors: [
{
messageId: 'missingPropType',
data: { name: 'name' },
},
],
},
{
code: `
function Hello(props) {
return <div>{props.name}</div>;
}
export default Hello;
`,
options: [{ skipUnexported: true }],
errors: [
{
messageId: 'missingPropType',
data: { name: 'name' },
},
],
},
{
code: `
const Hello = (props) => {
return <div>{props.name}</div>;
};
export { Hello };
`,
options: [{ skipUnexported: true }],
errors: [
{
messageId: 'missingPropType',
data: { name: 'name' },
},
],
},
{
code: `
function Hello(props) {
return <div>{props.name}</div>;
}
module.exports = Hello;
`,
options: [{ skipUnexported: true }],
errors: [
{
messageId: 'missingPropType',
data: { name: 'name' },
},
],
},
{
code: `
function Hello(props) {
return <div>{props.name}</div>;
}
exports.Hello = Hello;
`,
options: [{ skipUnexported: true }],
errors: [
{
messageId: 'missingPropType',
data: { name: 'name' },
},
],
},
{
code: `
function Hello(props) {
return <div>{props.name}</div>;
}
`,
options: [{ skipUnexported: false }],
errors: [
{
messageId: 'missingPropType',
data: { name: 'name' },
},
],
},
{
code: `
type MyComponentProps = {
Expand Down
Loading