Skip to content

Commit

Permalink
Update to support exact prop wrapper functions
Browse files Browse the repository at this point in the history
  • Loading branch information
jomasti committed Dec 4, 2018
1 parent 1819fd9 commit 4989a30
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 22 deletions.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -46,7 +46,9 @@ You should also specify settings that will be shared across all the plugin rules
// The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
"forbidExtraProps",
{"property": "freeze", "object": "Object"}
{"property": "myFavoriteWrapper"}
{"property": "myFavoriteWrapper"},
// for rules that check exact prop wrappers
{"property": "forbidExtraProps", "exact": true}
]
}
}
Expand Down
12 changes: 12 additions & 0 deletions docs/rules/prefer-exact-props.md
Expand Up @@ -50,6 +50,18 @@ See the [Flow docs](https://flow.org/en/docs/types/objects/#toc-exact-object-typ

## Rule Details

This rule will only produce errors for prop types when combined with the appropriate entries in `propWrapperFunctions`. For example:

```json
{
"settings": {
"propWrapperFunctions": [
{"property": "exact", "exact": true}
]
}
}
```

The following patterns are considered warnings:

```jsx
Expand Down
29 changes: 22 additions & 7 deletions lib/rules/prefer-exact-props.js
Expand Up @@ -4,10 +4,12 @@
'use strict';

const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const propsUtil = require('../util/props');
const propWrapperUtil = require('../util/propWrapper');
const variableUtil = require('../util/variable');

const PROP_TYPES_MESSAGE = 'Component propTypes should be exact by using prop-types-exact.';
const PROP_TYPES_MESSAGE = 'Component propTypes should be exact by using {{exactPropWrappers}}.';
const FLOW_MESSAGE = 'Component flow props should be set with exact objects.';

// -----------------------------------------------------------------------------
Expand All @@ -19,12 +21,21 @@ module.exports = {
docs: {
description: 'Prefer exact proptype definitions',
category: 'Possible Errors',
recommended: false
recommended: false,
url: docsUrl('prefer-exact-props')
},
schema: []
},

create: Components.detect((context, components, utils) => {
const exactWrappers = propWrapperUtil.getExactPropWrapperFunctions(context);

function getPropTypesErrorMessage() {
const formattedWrappers = propWrapperUtil.formatPropWrapperFunctions(exactWrappers);
const message = exactWrappers.size > 1 ? `one of ${formattedWrappers}` : formattedWrappers;
return {exactPropWrappers: message};
}

function isNonExactObjectTypeAnnotation(node) {
return (
node &&
Expand Down Expand Up @@ -71,10 +82,11 @@ module.exports = {
node: node,
message: FLOW_MESSAGE
});
} else if (isNonEmptyObjectExpression(node.value)) {
} else if (isNonEmptyObjectExpression(node.value) && exactWrappers.size > 0) {
context.report({
node: node,
message: PROP_TYPES_MESSAGE
message: PROP_TYPES_MESSAGE,
data: getPropTypesErrorMessage()
});
}
},
Expand All @@ -83,6 +95,7 @@ module.exports = {
if (!utils.getParentStatelessComponent(node)) {
return;
}

if (hasNonExactObjectTypeAnnotation(node)) {
context.report({
node: node,
Expand All @@ -101,23 +114,25 @@ module.exports = {
},

MemberExpression: function(node) {
if (!propsUtil.isPropTypesDeclaration(node)) {
if (!propsUtil.isPropTypesDeclaration(node) || exactWrappers.size === 0) {
return;
}

const right = node.parent.right;
if (isNonEmptyObjectExpression(right)) {
context.report({
node: node,
message: PROP_TYPES_MESSAGE
message: PROP_TYPES_MESSAGE,
data: getPropTypesErrorMessage()
});
} else if (right.type === 'Identifier') {
const identifier = right.name;
const propsDefinition = variableUtil.findVariableByName(context, identifier);
if (isNonEmptyObjectExpression(propsDefinition)) {
context.report({
node: node,
message: PROP_TYPES_MESSAGE
message: PROP_TYPES_MESSAGE,
data: getPropTypesErrorMessage()
});
}
}
Expand Down
41 changes: 35 additions & 6 deletions lib/util/propWrapper.js
Expand Up @@ -3,12 +3,7 @@
*/
'use strict';

function getPropWrapperFunctions(context) {
return new Set(context.settings.propWrapperFunctions || []);
}

function isPropWrapperFunction(context, name) {
const propWrapperFunctions = getPropWrapperFunctions(context);
function searchPropWrapperFunctions(name, propWrapperFunctions) {
const splitName = name.split('.');
return Array.from(propWrapperFunctions).some(func => {
if (splitName.length === 2 && func.object === splitName[0] && func.property === splitName[1]) {
Expand All @@ -18,7 +13,41 @@ function isPropWrapperFunction(context, name) {
});
}

function getPropWrapperFunctions(context) {
return new Set(context.settings.propWrapperFunctions || []);
}

function isPropWrapperFunction(context, name) {
const propWrapperFunctions = getPropWrapperFunctions(context);
return searchPropWrapperFunctions(name, propWrapperFunctions);
}

function getExactPropWrapperFunctions(context) {
const propWrapperFunctions = getPropWrapperFunctions(context);
const exactPropWrappers = Array.from(propWrapperFunctions).filter(func => func.exact === true);
return new Set(exactPropWrappers);
}

function isExactPropWrapperFunction(context, name) {
const exactPropWrappers = getExactPropWrapperFunctions(context);
return searchPropWrapperFunctions(name, exactPropWrappers);
}

function formatPropWrapperFunctions(propWrapperFunctions) {
return Array.from(propWrapperFunctions).map(func => {
if (func.object && func.property) {
return `'${func.object}.${func.property}'`;
} else if (func.property) {
return `'${func.property}'`;
}
return `'${func}'`;
}).join(', ');
}

module.exports = {
formatPropWrapperFunctions: formatPropWrapperFunctions,
getExactPropWrapperFunctions: getExactPropWrapperFunctions,
getPropWrapperFunctions: getPropWrapperFunctions,
isExactPropWrapperFunction: isExactPropWrapperFunction,
isPropWrapperFunction: isPropWrapperFunction
};
95 changes: 88 additions & 7 deletions tests/lib/rules/prefer-exact-props.js
Expand Up @@ -19,7 +19,13 @@ const parserOptions = {
}
};

const PROP_TYPES_MESSAGE = 'Component propTypes should be exact by using prop-types-exact.';
const settings = {
propWrapperFunctions: [
{property: 'exact', exact: true}
]
};

const PROP_TYPES_MESSAGE = 'Component propTypes should be exact by using \'exact\'.';
const FLOW_MESSAGE = 'Component flow props should be set with exact objects.';

const ruleTester = new RuleTester({parserOptions});
Expand All @@ -32,7 +38,8 @@ ruleTester.run('prefer-exact-props', rule, {
}
}
Component.propTypes = {};
`
`,
settings: settings
}, {
code: `
class Component extends React.Component {
Expand All @@ -42,7 +49,8 @@ ruleTester.run('prefer-exact-props', rule, {
}
}
`,
parser: 'babel-eslint'
parser: 'babel-eslint',
settings: settings
}, {
code: `
class Component extends React.Component {
Expand All @@ -52,14 +60,16 @@ ruleTester.run('prefer-exact-props', rule, {
}
}
`,
parser: 'babel-eslint'
parser: 'babel-eslint',
settings: settings
}, {
code: `
function Component(props) {
return <div />;
}
Component.propTypes = {};
`
`,
settings: settings
}, {
code: `
function Component(props: {}) {
Expand Down Expand Up @@ -107,7 +117,8 @@ ruleTester.run('prefer-exact-props', rule, {
return <div />;
}
Component.propTypes = props;
`
`,
settings: settings
}, {
code: `
const props = {};
Expand All @@ -117,7 +128,8 @@ ruleTester.run('prefer-exact-props', rule, {
}
}
Component.propTypes = props;
`
`,
settings: settings
}, {
code: `
import props from 'foo';
Expand All @@ -127,6 +139,52 @@ ruleTester.run('prefer-exact-props', rule, {
}
}
Component.propTypes = props;
`,
settings: settings
}, {
code: `
class Component extends React.Component {
state = {hi: 'hi'}
render() {
return <div>{this.state.hi}</div>;
}
}
`,
parser: 'babel-eslint'
}, {
code: `
import exact from "prop-types-exact";
function Component({ foo, bar }) {
return <div>{foo}{bar}</div>;
}
Component.propTypes = exact({
foo: PropTypes.string,
bar: PropTypes.string,
});
`,
settings: settings
}, {
code: `
function Component({ foo, bar }) {
return <div>{foo}{bar}</div>;
}
Component.propTypes = {
foo: PropTypes.string,
bar: PropTypes.string,
};
`
}, {
code: `
class Component extends React.Component {
render() {
const { foo, bar } = this.props;
return <div>{foo}{bar}</div>;
}
}
Component.propTypes = {
foo: PropTypes.string,
bar: PropTypes.string,
};
`
}],
invalid: [{
Expand All @@ -140,6 +198,7 @@ ruleTester.run('prefer-exact-props', rule, {
foo: PropTypes.string
};
`,
settings: settings,
errors: [{message: PROP_TYPES_MESSAGE}]
}, {
code: `
Expand All @@ -152,6 +211,7 @@ ruleTester.run('prefer-exact-props', rule, {
}
}
`,
settings: settings,
parser: 'babel-eslint',
errors: [{message: PROP_TYPES_MESSAGE}]
}, {
Expand Down Expand Up @@ -196,6 +256,7 @@ ruleTester.run('prefer-exact-props', rule, {
}
Component.propTypes = props;
`,
settings: settings,
errors: [{message: PROP_TYPES_MESSAGE}]
}, {
code: `
Expand All @@ -209,6 +270,26 @@ ruleTester.run('prefer-exact-props', rule, {
}
Component.propTypes = props;
`,
settings: settings,
errors: [{message: PROP_TYPES_MESSAGE}]
}, {
code: `
const props = {
foo: PropTypes.string
};
class Component extends React.Component {
render() {
return <div />;
}
}
Component.propTypes = props;
`,
settings: {
propWrapperFunctions: [
{property: 'exact', exact: true},
{property: 'forbidExtraProps', exact: true}
]
},
errors: [{message: 'Component propTypes should be exact by using one of \'exact\', \'forbidExtraProps\'.'}]
}]
});

0 comments on commit 4989a30

Please sign in to comment.