Repository: yannickcr/eslint-plugin-react (js)

Issue:
Add a rule prefer-exact-props
With react and flow, it's very common to type props like this:

```javascript
type Props = {
  prop1: string,
  prop2: number
};
```

However this makes it possible to pass other props as well, as by default objects in Flow are not exact. So this can hide an issue where the developer removed a prop but the caller still use it:
```javascript
<Component prop1='foo' prop2={42} prop3='bar' />
```
Or made a typo (this example is possible is `prop2` is optional):
```javascript
<Component prop1='foo' porp2={42} />
```


To fix this with flow, the solution is to use exact objects:
```javascript
type Props = {|
  prop1: string,
  prop2: number
|};
```

It would be useful to have an eslint rule to force this pattern.


Pull request description:
[New] add `prefer-exact-props` rule
This rule checks if non-exact objects are used for defining prop types when using Flow along with using normal objects for prop types instead of something like `prop-types-exact` to enforce only passing the exact props to a component.

The exact prop wrapper functions for non-Flow usage are configurable by adding the `exact` property to an object setting in the `propWrapperFunctions` config option.

Closes #1455 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7dcfee44..ec63cda6 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 cc937d28..30a0a90c 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 00000000..297a5204
--- /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 <p>{this.props.bar}</p>;
+  }
+}
+
+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 <p>{this.props.bar}</p>;
+  }
+}
+```
+
+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 <p>{this.props.bar}</p>;
+  }
+}
+```
+
+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 <div />;
+    }
+  }
+  Component.propTypes = {
+    foo: PropTypes.string
+  };
+```
+
+```jsx
+  class Component extends React.Component {
+    static propTypes = {
+      foo: PropTypes.string
+    }
+    render() {
+      return <div />;
+    }
+  }
+```
+
+```jsx
+  class Component extends React.Component {
+    props: {
+      foo: string
+    }
+    render() {
+      return <div />;
+    }
+  }
+```
+
+```jsx
+  function Component(props: { foo: string }) {
+    return <div />;
+  }
+```
+
+```jsx
+  type Props = {
+    foo: string
+  }
+  function Component(props: Props) {
+    return <div />;
+  }
+```
+
+The following patterns are **not** considered warnings:
+
+```jsx
+  type Props = {|
+    foo: string
+  |}
+  function Component(props: Props) {
+    return <div />;
+  }
+```
+
+```jsx
+  import exact from 'prop-types-exact';
+  class Component extends React.Component {
+    render() {
+      return <div />;
+    }
+  }
+  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 8a0adf7f..e89b0c8e 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 00000000..bac275c6
--- /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 7202b13a..7ee681d9 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
 };


Test patch:
diff --git a/tests/lib/rules/prefer-exact-props.js b/tests/lib/rules/prefer-exact-props.js
new file mode 100644
index 00000000..884df3fe
--- /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 <div />;
+        }
+      }
+      Component.propTypes = {};
+    `,
+    settings
+  }, {
+    code: `
+      class Component extends React.Component {
+        static propTypes = {};
+        render() {
+          return <div />;
+        }
+      }
+    `,
+    parser: require.resolve('babel-eslint'),
+    settings
+  }, {
+    code: `
+      class Component extends React.Component {
+        props: {};
+        render() {
+          return <div />;
+        }
+      }
+    `,
+    parser: require.resolve('babel-eslint'),
+    settings
+  }, {
+    code: `
+      function Component(props) {
+        return <div />;
+      }
+      Component.propTypes = {};
+    `,
+    settings
+  }, {
+    code: `
+      function Component(props: {}) {
+        return <div />;
+      }
+    `,
+    parser: require.resolve('babel-eslint')
+  }, {
+    code: `
+      type Props = {|
+        foo: string
+      |}
+      function Component(props: Props) {
+        return <div />;
+      }
+    `,
+    parser: require.resolve('babel-eslint')
+  }, {
+    code: `
+      function Component(props: {| foo : string |}) {
+        return <div />;
+      }
+    `,
+    parser: require.resolve('babel-eslint')
+  }, {
+    code: `
+      type Props = {}
+      function Component(props: Props) {
+        return <div />;
+      }
+    `,
+    parser: require.resolve('babel-eslint')
+  }, {
+    code: `
+      import type Props from 'foo';
+      function Component(props: Props) {
+        return <div />;
+      }
+    `,
+    parser: require.resolve('babel-eslint')
+  }, {
+    code: `
+      const props = {};
+      function Component(props) {
+        return <div />;
+      }
+      Component.propTypes = props;
+    `,
+    settings
+  }, {
+    code: `
+      const props = {};
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = props;
+    `,
+    settings
+  }, {
+    code: `
+      import props from 'foo';
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = props;
+    `,
+    settings
+  }, {
+    code: `
+      class Component extends React.Component {
+        state = {hi: 'hi'}
+        render() {
+          return <div>{this.state.hi}</div>;
+        }
+      }
+    `,
+    parser: require.resolve('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
+  }, {
+    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,
+      };
+    `
+  }, {
+    code: `
+      import somethingElse from "something-else";
+      const props = {
+        foo: PropTypes.string,
+        bar: PropTypes.shape({
+          baz: PropTypes.string
+        })
+      };
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      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 <div />;
+        }
+      }
+    `,
+    parser: require.resolve('babel-eslint')
+  }],
+  invalid: [{
+    code: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        foo: PropTypes.string
+      };
+    `,
+    settings,
+    errors: [{message: PROP_TYPES_MESSAGE}]
+  }, {
+    code: `
+      class Component extends React.Component {
+        static propTypes = {
+          foo: PropTypes.string
+        }
+        render() {
+          return <div />;
+        }
+      }
+    `,
+    settings,
+    parser: require.resolve('babel-eslint'),
+    errors: [{message: PROP_TYPES_MESSAGE}]
+  }, {
+    code: `
+      class Component extends React.Component {
+        props: {
+          foo: string
+        }
+        render() {
+          return <div />;
+        }
+      }
+    `,
+    parser: require.resolve('babel-eslint'),
+    errors: [{message: FLOW_MESSAGE}]
+  }, {
+    code: `
+      function Component(props: { foo: string }) {
+        return <div />;
+      }
+    `,
+    parser: require.resolve('babel-eslint'),
+    errors: [{message: FLOW_MESSAGE}]
+  }, {
+    code: `
+      type Props = {
+        foo: string
+      }
+      function Component(props: Props) {
+        return <div />;
+      }
+    `,
+    parser: require.resolve('babel-eslint'),
+    errors: [{message: FLOW_MESSAGE}]
+  }, {
+    code: `
+      const props = {
+        foo: PropTypes.string
+      };
+      function Component(props) {
+        return <div />;
+      }
+      Component.propTypes = props;
+    `,
+    settings,
+    errors: [{message: PROP_TYPES_MESSAGE}]
+  }, {
+    code: `
+      const props = {
+        foo: PropTypes.string
+      };
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = props;
+    `,
+    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\'.'}]
+  }, {
+    code: `
+      const props = {
+        foo: PropTypes.string,
+        bar: PropTypes.shape({
+          baz: 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\'.'}]
+  }, {
+    code: `
+      import somethingElse from "something-else";
+      function Component({ foo, bar }) {
+        return <div>{foo}{bar}</div>;
+      }
+      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 <div />;
+        }
+      }
+      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 <div />;
+        }
+      }
+    `,
+    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 44729588..5664962e 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\'');
+    });
+  });
 });