diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dcfee4475..ec63cda6c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ### Added * [`jsx-no-useless-fragments`]: add option to allow single expressions in fragments ([#3006][] @mattdarveniza) +* add [`prefer-exact-props`] rule ([#1547][] @jomasti) ### Fixed * component detection: use `estraverse` to improve component detection ([#2992][] @Wesitos) @@ -29,6 +30,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel [#2998]: https://github.com/yannickcr/eslint-plugin-react/pull/2998 [#2994]: https://github.com/yannickcr/eslint-plugin-react/pull/2994 [#2992]: https://github.com/yannickcr/eslint-plugin-react/pull/2992 +[#1547]: https://github.com/yannickcr/eslint-plugin-react/pull/1547 ## [7.24.0] - 2021.05.27 @@ -3304,98 +3306,99 @@ If you're still not using React 15 you can keep the old behavior by setting the [`react/jsx-runtime`]: https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/index.js#L163-L176 +[`boolean-prop-naming`]: docs/rules/boolean-prop-naming.md +[`button-has-type`]: docs/rules/button-has-type.md +[`default-props-match-prop-types`]: docs/rules/default-props-match-prop-types.md +[`destructuring-assignment`]: docs/rules/destructuring-assignment.md [`display-name`]: docs/rules/display-name.md [`forbid-component-props`]: docs/rules/forbid-component-props.md +[`forbid-dom-props`]: docs/rules/forbid-dom-props.md [`forbid-elements`]: docs/rules/forbid-elements.md [`forbid-foreign-prop-types`]: docs/rules/forbid-foreign-prop-types.md [`forbid-prop-types`]: docs/rules/forbid-prop-types.md -[`no-array-index-key`]: docs/rules/no-array-index-key.md -[`no-children-prop`]: docs/rules/no-children-prop.md -[`no-danger`]: docs/rules/no-danger.md -[`no-danger-with-children`]: docs/rules/no-danger-with-children.md -[`no-deprecated`]: docs/rules/no-deprecated.md -[`no-did-mount-set-state`]: docs/rules/no-did-mount-set-state.md -[`no-did-update-set-state`]: docs/rules/no-did-update-set-state.md -[`no-direct-mutation-state`]: docs/rules/no-direct-mutation-state.md -[`no-find-dom-node`]: docs/rules/no-find-dom-node.md -[`no-is-mounted`]: docs/rules/no-is-mounted.md -[`no-multi-comp`]: docs/rules/no-multi-comp.md -[`no-render-return-value`]: docs/rules/no-render-return-value.md -[`no-set-state`]: docs/rules/no-set-state.md -[`no-string-refs`]: docs/rules/no-string-refs.md -[`no-unescaped-entities`]: docs/rules/no-unescaped-entities.md -[`no-unknown-property`]: docs/rules/no-unknown-property.md -[`no-unused-prop-types`]: docs/rules/no-unused-prop-types.md -[`no-will-update-set-state`]: docs/rules/no-will-update-set-state.md -[`prefer-es6-class`]: docs/rules/prefer-es6-class.md -[`prefer-stateless-function`]: docs/rules/prefer-stateless-function.md -[`prop-types`]: docs/rules/prop-types.md -[`react-in-jsx-scope`]: docs/rules/react-in-jsx-scope.md -[`require-optimization`]: docs/rules/require-optimization.md -[`require-render-return`]: docs/rules/require-render-return.md -[`self-closing-comp`]: docs/rules/self-closing-comp.md -[`sort-comp`]: docs/rules/sort-comp.md -[`sort-prop-types`]: docs/rules/sort-prop-types.md -[`style-prop-object`]: docs/rules/style-prop-object.md +[`function-component-definition`]: docs/rules/function-component-definition.md [`jsx-boolean-value`]: docs/rules/jsx-boolean-value.md +[`jsx-child-element-spacing`]: docs/rules/jsx-child-element-spacing.md [`jsx-closing-bracket-location`]: docs/rules/jsx-closing-bracket-location.md +[`jsx-closing-tag-location`]: docs/rules/jsx-closing-tag-location.md +[`jsx-curly-brace-presence`]: docs/rules/jsx-curly-brace-presence.md +[`jsx-curly-newline`]: docs/rules/jsx-curly-newline.md [`jsx-curly-spacing`]: docs/rules/jsx-curly-spacing.md [`jsx-equals-spacing`]: docs/rules/jsx-equals-spacing.md [`jsx-filename-extension`]: docs/rules/jsx-filename-extension.md [`jsx-first-prop-new-line`]: docs/rules/jsx-first-prop-new-line.md +[`jsx-fragments`]: docs/rules/jsx-fragments.md [`jsx-handler-names`]: docs/rules/jsx-handler-names.md -[`jsx-indent`]: docs/rules/jsx-indent.md [`jsx-indent-props`]: docs/rules/jsx-indent-props.md +[`jsx-indent`]: docs/rules/jsx-indent.md [`jsx-key`]: docs/rules/jsx-key.md +[`jsx-max-depth`]: docs/rules/jsx-max-depth.md [`jsx-max-props-per-line`]: docs/rules/jsx-max-props-per-line.md +[`jsx-newline`]: docs/rules/jsx-newline.md [`jsx-no-bind`]: docs/rules/jsx-no-bind.md [`jsx-no-comment-textnodes`]: docs/rules/jsx-no-comment-textnodes.md +[`jsx-no-constructed-context-values`]: docs/rules/jsx-no-constructed-context-values.md [`jsx-no-duplicate-props`]: docs/rules/jsx-no-duplicate-props.md [`jsx-no-literals`]: docs/rules/jsx-no-literals.md +[`jsx-no-script-url`]: docs/rules/jsx-no-script-url.md [`jsx-no-target-blank`]: docs/rules/jsx-no-target-blank.md [`jsx-no-undef`]: docs/rules/jsx-no-undef.md +[`jsx-no-useless-fragment`]: docs/rules/jsx-no-useless-fragment.md +[`jsx-one-expression-per-line`]: docs/rules/jsx-one-expression-per-line.md [`jsx-pascal-case`]: docs/rules/jsx-pascal-case.md -[`require-default-props`]: docs/rules/require-default-props.md +[`jsx-props-no-multi-spaces`]: docs/rules/jsx-props-no-multi-spaces.md +[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md +[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md +[`jsx-sort-default-props`]: docs/rules/jsx-sort-default-props.md +[`jsx-sort-prop-types`]: docs/rules/sort-prop-types.md [`jsx-sort-props`]: docs/rules/jsx-sort-props.md [`jsx-space-before-closing`]: docs/rules/jsx-space-before-closing.md [`jsx-tag-spacing`]: docs/rules/jsx-tag-spacing.md [`jsx-uses-react`]: docs/rules/jsx-uses-react.md [`jsx-uses-vars`]: docs/rules/jsx-uses-vars.md [`jsx-wrap-multilines`]: docs/rules/jsx-wrap-multilines.md -[`void-dom-elements-no-children`]: docs/rules/void-dom-elements-no-children.md -[`default-props-match-prop-types`]: docs/rules/default-props-match-prop-types.md -[`no-redundant-should-component-update`]: docs/rules/no-redundant-should-component-update.md -[`jsx-closing-tag-location`]: docs/rules/jsx-closing-tag-location.md -[`no-unused-state`]: docs/rules/no-unused-state.md -[`boolean-prop-naming`]: docs/rules/boolean-prop-naming.md -[`no-typos`]: docs/rules/no-typos.md -[`jsx-sort-prop-types`]: docs/rules/sort-prop-types.md -[`require-extension`]: docs/rules/require-extension.md -[`no-comment-textnodes`]: docs/rules/jsx-no-comment-textnodes.md -[`wrap-multilines`]: docs/rules/jsx-wrap-multilines.md -[`jsx-curly-brace-presence`]: docs/rules/jsx-curly-brace-presence.md -[`jsx-one-expression-per-line`]: docs/rules/jsx-one-expression-per-line.md -[`destructuring-assignment`]: docs/rules/destructuring-assignment.md [`no-access-state-in-setstate`]: docs/rules/no-access-state-in-setstate.md -[`button-has-type`]: docs/rules/button-has-type.md -[`forbid-dom-props`]: docs/rules/forbid-dom-props.md -[`jsx-child-element-spacing`]: docs/rules/jsx-child-element-spacing.md +[`no-adjacent-inline-elements`]: docs/rules/no-adjacent-inline-elements.md +[`no-array-index-key`]: docs/rules/no-array-index-key.md +[`no-children-prop`]: docs/rules/no-children-prop.md +[`no-comment-textnodes`]: docs/rules/jsx-no-comment-textnodes.md +[`no-danger-with-children`]: docs/rules/no-danger-with-children.md +[`no-danger`]: docs/rules/no-danger.md +[`no-deprecated`]: docs/rules/no-deprecated.md +[`no-did-mount-set-state`]: docs/rules/no-did-mount-set-state.md +[`no-did-update-set-state`]: docs/rules/no-did-update-set-state.md +[`no-direct-mutation-state`]: docs/rules/no-direct-mutation-state.md +[`no-find-dom-node`]: docs/rules/no-find-dom-node.md +[`no-is-mounted`]: docs/rules/no-is-mounted.md +[`no-multi-comp`]: docs/rules/no-multi-comp.md +[`no-redundant-should-component-update`]: docs/rules/no-redundant-should-component-update.md +[`no-render-return-value`]: docs/rules/no-render-return-value.md +[`no-set-state`]: docs/rules/no-set-state.md +[`no-string-refs`]: docs/rules/no-string-refs.md [`no-this-in-sfc`]: docs/rules/no-this-in-sfc.md -[`jsx-sort-default-props`]: docs/rules/jsx-sort-default-props.md -[`jsx-max-depth`]: docs/rules/jsx-max-depth.md -[`jsx-props-no-multi-spaces`]: docs/rules/jsx-props-no-multi-spaces.md +[`no-typos`]: docs/rules/no-typos.md +[`no-unescaped-entities`]: docs/rules/no-unescaped-entities.md +[`no-unknown-property`]: docs/rules/no-unknown-property.md [`no-unsafe`]: docs/rules/no-unsafe.md -[`jsx-fragments`]: docs/rules/jsx-fragments.md -[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md +[`no-unstable-nested-components`]: docs/rules/no-unstable-nested-components.md +[`no-unused-prop-types`]: docs/rules/no-unused-prop-types.md +[`no-unused-state`]: docs/rules/no-unused-state.md +[`no-will-update-set-state`]: docs/rules/no-will-update-set-state.md +[`prefer-es6-class`]: docs/rules/prefer-es6-class.md +[`prefer-exact-props`]: docs/rules/prefer-exact-props.md [`prefer-read-only-props`]: docs/rules/prefer-read-only-props.md +[`prefer-stateless-function`]: docs/rules/prefer-stateless-function.md +[`prop-types`]: docs/rules/prop-types.md +[`react-in-jsx-scope`]: docs/rules/react-in-jsx-scope.md +[`require-default-props`]: docs/rules/require-default-props.md +[`require-extension`]: docs/rules/require-extension.md +[`require-optimization`]: docs/rules/require-optimization.md +[`require-render-return`]: docs/rules/require-render-return.md +[`self-closing-comp`]: docs/rules/self-closing-comp.md +[`sort-comp`]: docs/rules/sort-comp.md +[`sort-prop-types`]: docs/rules/sort-prop-types.md [`state-in-constructor`]: docs/rules/state-in-constructor.md -[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md [`static-property-placement`]: docs/rules/static-property-placement.md -[`jsx-curly-newline`]: docs/rules/jsx-curly-newline.md -[`jsx-no-useless-fragment`]: docs/rules/jsx-no-useless-fragment.md -[`jsx-no-script-url`]: docs/rules/jsx-no-script-url.md -[`no-adjacent-inline-elements`]: docs/rules/no-adjacent-inline-elements.md -[`function-component-definition`]: docs/rules/function-component-definition.md -[`jsx-newline`]: docs/rules/jsx-newline.md -[`jsx-no-constructed-context-values`]: docs/rules/jsx-no-constructed-context-values.md -[`no-unstable-nested-components`]: docs/rules/no-unstable-nested-components.md +[`style-prop-object`]: docs/rules/style-prop-object.md +[`void-dom-elements-no-children`]: docs/rules/void-dom-elements-no-children.md +[`wrap-multilines`]: docs/rules/jsx-wrap-multilines.md diff --git a/README.md b/README.md index cc937d2803..30a0a90c66 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,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} ], "componentWrapperFunctions": [ // The name of any function used to wrap components, e.g. Mobx `observer` function. If this isn't set, components wrapped by these functions will be skipped. diff --git a/docs/rules/prefer-exact-props.md b/docs/rules/prefer-exact-props.md new file mode 100644 index 0000000000..297a5204ef --- /dev/null +++ b/docs/rules/prefer-exact-props.md @@ -0,0 +1,140 @@ +# Prefer exact proptype definitions (react/prefer-exact-props) + +Recommends options to ensure only exact prop definitions are used when writing components. This recommends solutions for PropTypes or for Flow types. + +In React, you can define prop types for components using propTypes. Such an example is below: + +```jsx +class Foo extends React.Component { + render() { + return

{this.props.bar}

; + } +} + +Foo.propTypes = { + bar: PropTypes.string +}; +``` + +The problem with this is that the consumer of the component could still pass in extra props. There could even be a typo for expected props. In order to prevent those situations, one could use the npm package [prop-types-exact](https://www.npmjs.com/package/prop-types-exact) to warn when unexpected props are passed to the component. + +One can also define props for a component using Flow types. Such an example is below: + +```jsx +class Foo extends React.Component { + props: { + bar: string + } + + render() { + return

{this.props.bar}

; + } +} +``` + +In this case, one could instead enforce only the exact props being used by using exact type objects, like below: + +```jsx +class Foo extends React.Component { + props: {| + bar: string + }| + + render() { + return

{this.props.bar}

; + } +} +``` + +See the [Flow docs](https://flow.org/en/docs/types/objects/#toc-exact-object-types) on exact object types for more information. + +## 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 + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = { + foo: PropTypes.string + }; +``` + +```jsx + class Component extends React.Component { + static propTypes = { + foo: PropTypes.string + } + render() { + return
; + } + } +``` + +```jsx + class Component extends React.Component { + props: { + foo: string + } + render() { + return
; + } + } +``` + +```jsx + function Component(props: { foo: string }) { + return
; + } +``` + +```jsx + type Props = { + foo: string + } + function Component(props: Props) { + return
; + } +``` + +The following patterns are **not** considered warnings: + +```jsx + type Props = {| + foo: string + |} + function Component(props: Props) { + return
; + } +``` + +```jsx + import exact from 'prop-types-exact'; + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = exact({ + foo: PropTypes.string + }); +``` + +## When Not To Use It + +If you aren't concerned about extra props being passed to a component or potential spelling errors for existing props aren't a common nuisance, then you can leave this rule off. diff --git a/index.js b/index.js index 8a0adf7f32..e89b0c8ea4 100644 --- a/index.js +++ b/index.js @@ -81,6 +81,7 @@ const allRules = { 'no-unused-state': require('./lib/rules/no-unused-state'), 'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'), 'prefer-es6-class': require('./lib/rules/prefer-es6-class'), + 'prefer-exact-props': require('./lib/rules/prefer-exact-props'), 'prefer-read-only-props': require('./lib/rules/prefer-read-only-props'), 'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'), 'prop-types': require('./lib/rules/prop-types'), diff --git a/lib/rules/prefer-exact-props.js b/lib/rules/prefer-exact-props.js new file mode 100644 index 0000000000..bac275c6bc --- /dev/null +++ b/lib/rules/prefer-exact-props.js @@ -0,0 +1,159 @@ +/** + * @fileoverview Prefer exact proptype definitions + */ + +'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 {{exactPropWrappers}}.'; +const FLOW_MESSAGE = 'Component flow props should be set with exact objects.'; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = { + meta: { + docs: { + description: 'Prefer exact proptype definitions', + category: 'Possible Errors', + recommended: false, + url: docsUrl('prefer-exact-props') + }, + schema: [] + }, + + create: Components.detect((context, components, utils) => { + const typeAliases = {}; + const exactWrappers = propWrapperUtil.getExactPropWrapperFunctions(context); + const sourceCode = context.getSourceCode(); + + function getPropTypesErrorMessage() { + const formattedWrappers = propWrapperUtil.formatPropWrapperFunctions(exactWrappers); + const message = exactWrappers.size > 1 ? `one of ${formattedWrappers}` : formattedWrappers; + return {exactPropWrappers: message}; + } + + function isNonExactObjectTypeAnnotation(node) { + return ( + node + && node.type === 'ObjectTypeAnnotation' + && node.properties.length > 0 + && !node.exact + ); + } + + function hasNonExactObjectTypeAnnotation(node) { + const typeAnnotation = node.typeAnnotation; + return ( + typeAnnotation + && typeAnnotation.typeAnnotation + && isNonExactObjectTypeAnnotation(typeAnnotation.typeAnnotation) + ); + } + + function hasGenericTypeAnnotation(node) { + const typeAnnotation = node.typeAnnotation; + return ( + typeAnnotation + && typeAnnotation.typeAnnotation + && typeAnnotation.typeAnnotation.type === 'GenericTypeAnnotation' + ); + } + + function isNonEmptyObjectExpression(node) { + return ( + node + && node.type === 'ObjectExpression' + && node.properties.length > 0 + ); + } + + function isNonExactPropWrapperFunction(node) { + return ( + node + && node.type === 'CallExpression' + && !propWrapperUtil.isExactPropWrapperFunction(context, sourceCode.getText(node.callee)) + ); + } + + function reportPropTypesError(node) { + context.report({ + node, + message: PROP_TYPES_MESSAGE, + data: getPropTypesErrorMessage() + }); + } + + function reportFlowError(node) { + context.report({ + node, + message: FLOW_MESSAGE + }); + } + + return { + TypeAlias(node) { + // working around an issue with eslint@3 and babel-eslint not finding the TypeAlias in scope + typeAliases[node.id.name] = node; + }, + + ClassProperty(node) { + if (!propsUtil.isPropTypesDeclaration(node)) { + return; + } + + if (hasNonExactObjectTypeAnnotation(node)) { + reportFlowError(node); + } else if (exactWrappers.size > 0 && isNonEmptyObjectExpression(node.value)) { + reportPropTypesError(node); + } else if (exactWrappers.size > 0 && isNonExactPropWrapperFunction(node.value)) { + reportPropTypesError(node); + } + }, + + Identifier(node) { + if (!utils.getParentStatelessComponent(node)) { + return; + } + + if (hasNonExactObjectTypeAnnotation(node)) { + reportFlowError(node); + } else if (hasGenericTypeAnnotation(node)) { + const identifier = node.typeAnnotation.typeAnnotation.id.name; + const typeAlias = typeAliases[identifier]; + const propsDefinition = typeAlias ? typeAlias.right : null; + if (isNonExactObjectTypeAnnotation(propsDefinition)) { + reportFlowError(node); + } + } + }, + + MemberExpression(node) { + if (!propsUtil.isPropTypesDeclaration(node) || exactWrappers.size === 0) { + return; + } + + const right = node.parent.right; + if (isNonEmptyObjectExpression(right)) { + reportPropTypesError(node); + } else if (isNonExactPropWrapperFunction(right)) { + reportPropTypesError(node); + } else if (right.type === 'Identifier') { + const identifier = right.name; + const propsDefinition = variableUtil.findVariableByName(context, identifier); + if (isNonEmptyObjectExpression(propsDefinition)) { + reportPropTypesError(node); + } else if (isNonExactPropWrapperFunction(propsDefinition)) { + reportPropTypesError(node); + } + } + } + }; + }) +}; diff --git a/lib/util/propWrapper.js b/lib/util/propWrapper.js index 7202b13a16..7ee681d966 100644 --- a/lib/util/propWrapper.js +++ b/lib/util/propWrapper.js @@ -4,6 +4,16 @@ 'use strict'; +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]) { + return true; + } + return name === func || func.property === name; + }); +} + function getPropWrapperFunctions(context) { return new Set(context.settings.propWrapperFunctions || []); } @@ -13,16 +23,36 @@ function isPropWrapperFunction(context, name) { return false; } const propWrapperFunctions = getPropWrapperFunctions(context); - const splitName = name.split('.'); - return Array.from(propWrapperFunctions).some((func) => { - if (splitName.length === 2 && func.object === splitName[0] && func.property === splitName[1]) { - return true; + 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, (func) => { + if (func.object && func.property) { + return `'${func.object}.${func.property}'`; } - return name === func || func.property === name; - }); + if (func.property) { + return `'${func.property}'`; + } + return `'${func}'`; + }).join(', '); } module.exports = { + formatPropWrapperFunctions, + getExactPropWrapperFunctions, getPropWrapperFunctions, + isExactPropWrapperFunction, isPropWrapperFunction }; diff --git a/tests/lib/rules/prefer-exact-props.js b/tests/lib/rules/prefer-exact-props.js new file mode 100644 index 0000000000..884df3fe71 --- /dev/null +++ b/tests/lib/rules/prefer-exact-props.js @@ -0,0 +1,401 @@ +/** + * @fileoverview Prefer exact proptype definitions + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/prefer-exact-props'); + +const parserOptions = { + ecmaVersion: 8, + sourceType: 'module', + ecmaFeatures: { + experimentalObjectRestSpread: true, + jsx: true + } +}; + +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}); +ruleTester.run('prefer-exact-props', rule, { + valid: [{ + code: ` + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = {}; + `, + settings + }, { + code: ` + class Component extends React.Component { + static propTypes = {}; + render() { + return
; + } + } + `, + parser: require.resolve('babel-eslint'), + settings + }, { + code: ` + class Component extends React.Component { + props: {}; + render() { + return
; + } + } + `, + parser: require.resolve('babel-eslint'), + settings + }, { + code: ` + function Component(props) { + return
; + } + Component.propTypes = {}; + `, + settings + }, { + code: ` + function Component(props: {}) { + return
; + } + `, + parser: require.resolve('babel-eslint') + }, { + code: ` + type Props = {| + foo: string + |} + function Component(props: Props) { + return
; + } + `, + parser: require.resolve('babel-eslint') + }, { + code: ` + function Component(props: {| foo : string |}) { + return
; + } + `, + parser: require.resolve('babel-eslint') + }, { + code: ` + type Props = {} + function Component(props: Props) { + return
; + } + `, + parser: require.resolve('babel-eslint') + }, { + code: ` + import type Props from 'foo'; + function Component(props: Props) { + return
; + } + `, + parser: require.resolve('babel-eslint') + }, { + code: ` + const props = {}; + function Component(props) { + return
; + } + Component.propTypes = props; + `, + settings + }, { + code: ` + const props = {}; + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = props; + `, + settings + }, { + code: ` + import props from 'foo'; + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = props; + `, + settings + }, { + code: ` + class Component extends React.Component { + state = {hi: 'hi'} + render() { + return
{this.state.hi}
; + } + } + `, + parser: require.resolve('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 + }, { + 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, + }; + ` + }, { + code: ` + import somethingElse from "something-else"; + const props = { + foo: PropTypes.string, + bar: PropTypes.shape({ + baz: PropTypes.string + }) + }; + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = somethingElse(props); + ` + }, { + code: ` + import somethingElse from "something-else"; + const props = + class Component extends React.Component { + static propTypes = somethingElse({ + foo: PropTypes.string, + bar: PropTypes.shape({ + baz: PropTypes.string + }) + }); + render() { + return
; + } + } + `, + parser: require.resolve('babel-eslint') + }], + invalid: [{ + code: ` + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = { + foo: PropTypes.string + }; + `, + settings, + errors: [{message: PROP_TYPES_MESSAGE}] + }, { + code: ` + class Component extends React.Component { + static propTypes = { + foo: PropTypes.string + } + render() { + return
; + } + } + `, + settings, + parser: require.resolve('babel-eslint'), + errors: [{message: PROP_TYPES_MESSAGE}] + }, { + code: ` + class Component extends React.Component { + props: { + foo: string + } + render() { + return
; + } + } + `, + parser: require.resolve('babel-eslint'), + errors: [{message: FLOW_MESSAGE}] + }, { + code: ` + function Component(props: { foo: string }) { + return
; + } + `, + parser: require.resolve('babel-eslint'), + errors: [{message: FLOW_MESSAGE}] + }, { + code: ` + type Props = { + foo: string + } + function Component(props: Props) { + return
; + } + `, + parser: require.resolve('babel-eslint'), + errors: [{message: FLOW_MESSAGE}] + }, { + code: ` + const props = { + foo: PropTypes.string + }; + function Component(props) { + return
; + } + Component.propTypes = props; + `, + settings, + errors: [{message: PROP_TYPES_MESSAGE}] + }, { + code: ` + const props = { + foo: PropTypes.string + }; + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = props; + `, + 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\'.'}] + }, { + code: ` + const props = { + foo: PropTypes.string, + bar: PropTypes.shape({ + baz: 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\'.'}] + }, { + code: ` + import somethingElse from "something-else"; + function Component({ foo, bar }) { + return
{foo}{bar}
; + } + Component.propTypes = somethingElse({ + foo: PropTypes.string, + bar: PropTypes.string, + }); + `, + settings, + errors: [{message: 'Component propTypes should be exact by using \'exact\'.'}] + }, { + code: ` + import somethingElse from "something-else"; + const props = { + foo: PropTypes.string, + bar: PropTypes.shape({ + baz: PropTypes.string + }) + }; + class Component extends React.Component { + render() { + return
; + } + } + Component.propTypes = somethingElse(props); + `, + settings, + errors: [{message: 'Component propTypes should be exact by using \'exact\'.'}] + }, { + code: ` + import somethingElse from "something-else"; + const props = + class Component extends React.Component { + static propTypes = somethingElse({ + foo: PropTypes.string, + bar: PropTypes.shape({ + baz: PropTypes.string + }) + }); + render() { + return
; + } + } + `, + settings, + parser: require.resolve('babel-eslint'), + errors: [{message: 'Component propTypes should be exact by using \'exact\'.'}] + }] +}); diff --git a/tests/util/propWrapper.js b/tests/util/propWrapper.js index 4472958816..5664962ee5 100644 --- a/tests/util/propWrapper.js +++ b/tests/util/propWrapper.js @@ -17,7 +17,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: {} }; @@ -58,4 +58,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 + } + }; + 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 + } + }; + 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\''); + }); + }); });