diff --git a/README.md b/README.md index 4e000f5989..af4150f914 100644 --- a/README.md +++ b/README.md @@ -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} ] } } diff --git a/docs/rules/prefer-exact-props.md b/docs/rules/prefer-exact-props.md index e4c84e0d6a..297a5204ef 100644 --- a/docs/rules/prefer-exact-props.md +++ b/docs/rules/prefer-exact-props.md @@ -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 diff --git a/lib/rules/prefer-exact-props.js b/lib/rules/prefer-exact-props.js index 5bb0ffba03..044311643d 100644 --- a/lib/rules/prefer-exact-props.js +++ b/lib/rules/prefer-exact-props.js @@ -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.'; // ----------------------------------------------------------------------------- @@ -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 && @@ -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() }); } }, @@ -83,6 +95,7 @@ module.exports = { if (!utils.getParentStatelessComponent(node)) { return; } + if (hasNonExactObjectTypeAnnotation(node)) { context.report({ node: node, @@ -101,7 +114,7 @@ module.exports = { }, MemberExpression: function(node) { - if (!propsUtil.isPropTypesDeclaration(node)) { + if (!propsUtil.isPropTypesDeclaration(node) || exactWrappers.size === 0) { return; } @@ -109,7 +122,8 @@ module.exports = { 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; @@ -117,7 +131,8 @@ module.exports = { if (isNonEmptyObjectExpression(propsDefinition)) { context.report({ node: node, - message: PROP_TYPES_MESSAGE + message: PROP_TYPES_MESSAGE, + data: getPropTypesErrorMessage() }); } } diff --git a/lib/util/propWrapper.js b/lib/util/propWrapper.js index b41dd0933c..7a1db82949 100644 --- a/lib/util/propWrapper.js +++ b/lib/util/propWrapper.js @@ -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]) { @@ -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 }; diff --git a/tests/lib/rules/prefer-exact-props.js b/tests/lib/rules/prefer-exact-props.js index 49286ccf23..c159b4b3e5 100644 --- a/tests/lib/rules/prefer-exact-props.js +++ b/tests/lib/rules/prefer-exact-props.js @@ -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}); @@ -32,7 +38,8 @@ ruleTester.run('prefer-exact-props', rule, { } } Component.propTypes = {}; - ` + `, + settings: settings }, { code: ` class Component extends React.Component { @@ -42,7 +49,8 @@ ruleTester.run('prefer-exact-props', rule, { } } `, - parser: 'babel-eslint' + parser: 'babel-eslint', + settings: settings }, { code: ` class Component extends React.Component { @@ -52,14 +60,16 @@ ruleTester.run('prefer-exact-props', rule, { } } `, - parser: 'babel-eslint' + parser: 'babel-eslint', + settings: settings }, { code: ` function Component(props) { return
; } Component.propTypes = {}; - ` + `, + settings: settings }, { code: ` function Component(props: {}) { @@ -107,7 +117,8 @@ ruleTester.run('prefer-exact-props', rule, { return
; } Component.propTypes = props; - ` + `, + settings: settings }, { code: ` const props = {}; @@ -117,7 +128,8 @@ ruleTester.run('prefer-exact-props', rule, { } } Component.propTypes = props; - ` + `, + settings: settings }, { code: ` import props from 'foo'; @@ -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
{this.state.hi}
; + } + } + `, + parser: 'babel-eslint' + }, { + code: ` + import exact from "prop-types-exact"; + function Component({ foo, bar }) { + return
{foo}{bar}
; + } + Component.propTypes = exact({ + foo: PropTypes.string, + bar: PropTypes.string, + }); + `, + settings: settings + }, { + code: ` + function Component({ foo, bar }) { + return
{foo}{bar}
; + } + Component.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string, + }; + ` + }, { + code: ` + class Component extends React.Component { + render() { + const { foo, bar } = this.props; + return
{foo}{bar}
; + } + } + Component.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string, + }; ` }], invalid: [{ @@ -140,6 +198,7 @@ ruleTester.run('prefer-exact-props', rule, { foo: PropTypes.string }; `, + settings: settings, errors: [{message: PROP_TYPES_MESSAGE}] }, { code: ` @@ -152,6 +211,7 @@ ruleTester.run('prefer-exact-props', rule, { } } `, + settings: settings, parser: 'babel-eslint', errors: [{message: PROP_TYPES_MESSAGE}] }, { @@ -196,6 +256,7 @@ ruleTester.run('prefer-exact-props', rule, { } Component.propTypes = props; `, + settings: settings, errors: [{message: PROP_TYPES_MESSAGE}] }, { code: ` @@ -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
; + } + } + 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\'.'}] }] }); diff --git a/tests/util/propWrapper.js b/tests/util/propWrapper.js index 6cbef977ff..c5d224bc70 100644 --- a/tests/util/propWrapper.js +++ b/tests/util/propWrapper.js @@ -18,7 +18,7 @@ describe('PropWrapperFunctions', () => { assert.deepStrictEqual(propWrapperUtil.getPropWrapperFunctions(context), new Set(propWrapperFunctions)); }); - it('returns empty array if no setting', () => { + it('returns empty set if no setting', () => { const context = { settings: {} }; @@ -59,4 +59,95 @@ describe('PropWrapperFunctions', () => { assert.equal(propWrapperUtil.isPropWrapperFunction(context, 'forbidExtraProps'), true); }); }); + + describe('getExactPropWrapperFunctions', () => { + it('returns set of functions if setting exists', () => { + const propWrapperFunctions = ['Object.freeze', { + property: 'forbidExtraProps', + exact: true + }]; + const context = { + settings: { + propWrapperFunctions: propWrapperFunctions + } + }; + assert.deepStrictEqual(propWrapperUtil.getExactPropWrapperFunctions(context), new Set([{ + property: 'forbidExtraProps', + exact: true + }])); + }); + + it('returns empty set if no exact prop wrappers', () => { + const propWrapperFunctions = ['Object.freeze', { + property: 'forbidExtraProps' + }]; + const context = { + settings: { + propWrapperFunctions: propWrapperFunctions + } + }; + assert.deepStrictEqual(propWrapperUtil.getExactPropWrapperFunctions(context), new Set([])); + }); + + it('returns empty set if no setting', () => { + const context = { + settings: {} + }; + assert.deepStrictEqual(propWrapperUtil.getExactPropWrapperFunctions(context), new Set([])); + }); + }); + + describe('isExactPropWrapperFunction', () => { + it('with string', () => { + const context = { + settings: { + propWrapperFunctions: ['Object.freeze'] + } + }; + assert.equal(propWrapperUtil.isExactPropWrapperFunction(context, 'Object.freeze'), false); + }); + + it('with Object with object and property keys', () => { + const context = { + settings: { + propWrapperFunctions: [{ + property: 'freeze', + object: 'Object', + exact: true + }] + } + }; + assert.equal(propWrapperUtil.isExactPropWrapperFunction(context, 'Object.freeze'), true); + }); + + it('with Object with only property key', () => { + const context = { + settings: { + propWrapperFunctions: [{ + property: 'forbidExtraProps', + exact: true + }] + } + }; + assert.equal(propWrapperUtil.isExactPropWrapperFunction(context, 'forbidExtraProps'), true); + }); + }); + + describe('formatPropWrapperFunctions', () => { + it('with empty set', () => { + const propWrappers = new Set([]); + assert.equal(propWrapperUtil.formatPropWrapperFunctions(propWrappers), ''); + }); + + it('with all allowed values', () => { + const propWrappers = new Set(['Object.freeze', { + property: 'exact', + exact: true + }, { + property: 'bar', + object: 'foo' + }]); + assert.equal(propWrapperUtil.formatPropWrapperFunctions(propWrappers), '\'Object.freeze\', \'exact\', \'foo.bar\''); + }); + }); });