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

Issue:
"no-unused-prop-types" inside async functions (include arrow)
Hi!
I think there is a bug with async functions.
**property**: "no-unused-prop-types"
**parser**: "babel-eslint@7.1.1"
**where**: "async functions"
**example**:
```js
{
  code: [
    'export class Example extends Component {',
    '  static propTypes = {',
    '    failed: PropTypes.func.isRequired,',
    '    loadUserProfile: PropTypes.func.isRequired,',
    '  }',
    '  handleLogin = async () => {',
    '    await this.props.failed();',
    '    await this.props.loadUserProfile();',
    '  };',
    '}'
  ].join('\n'),
  parser: 'babel-eslint'
},
```

Pull request description:
fix no-unused-prop-types + async class properties and methods
There was an incosistency with access and storage of the usedPropTypes
of a component that would cause proptypes to not be correctly marked as
used if more than one prop was used in the body of an async function
class property or async class method.

Fixes #1053

---

bug source:

First time around
  - get the parentComponent using utils.getParentComponent() https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/rules/no-unused-prop-types.js#L594
  - save off the usedPropTypes array from what component was found.
  - modify the array with my new used props.
  - set the new array on the node https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/rules/no-unused-prop-types.js#L638
    Note that in this case the node is a MemberExpression
  - Components#set will then just crawl the parents of the current node
  (the MemberExpresion) until one was found in the internal this._list.
  - Because all async FunctionExpressions, ArrowFunctionExpressions, and
  FunctionDeclaration are treated as components of confidence 0 (not a
  component). The unusedPropTypes would be attached
  to this. (Which is usually fine).

However, if the component tries to mark a prop as used again, it will
read from the parentComponent using utils.getParentComponent() (which in
this case will return the class) which does not have the previously used
methods. The array would then be modified (created because it doesnt
exist), and then set them onto the async arrow function overwriting
anything that was previously there.

This change just attaches used props to the found component (where the
previous usedPropTypes are pulled from) if it exists, otherwise to the
current node.

Patch:
diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js
index 252e3495..5127971d 100644
--- a/lib/rules/no-unused-prop-types.js
+++ b/lib/rules/no-unused-prop-types.js
@@ -661,7 +661,7 @@ module.exports = {
           break;
       }
 
-      components.set(node, {
+      components.set(component ? component.node : node, {
         usedPropTypes: usedPropTypes,
         ignorePropsValidation: ignorePropsValidation
       });


Test patch:
diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js
index 1984159c..eff0e9c9 100644
--- a/tests/lib/rules/no-unused-prop-types.js
+++ b/tests/lib/rules/no-unused-prop-types.js
@@ -1581,6 +1581,36 @@ ruleTester.run('no-unused-prop-types', rule, {
         '}'
       ].join('\n'),
       parser: 'babel-eslint'
+    }, {
+      // Props used inside of an async class property
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '  }',
+        '  classProperty = async () => {',
+        '    await this.props.foo();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
+    }, {
+      // Multiple props used inside of an async class property
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  classProperty = async () => {',
+        '    await this.props.foo();',
+        '    await this.props.bar();',
+        '    await this.props.baz();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
     }, {
       code: [
         'class Hello extends Component {',
@@ -1619,6 +1649,38 @@ ruleTester.run('no-unused-prop-types', rule, {
         '}'
       ].join('\n'),
       parser: 'babel-eslint'
+    }, {
+      // Destructured props inside of async class property
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '  }',
+        '  classProperty = async () => {',
+        '    const { foo } = this.props;',
+        '    await foo();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
+    }, {
+      // Multiple destructured props inside of async class property
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  classProperty = async () => {',
+        '    const { foo, bar, baz } = this.props;',
+        '    await foo();',
+        '    await bar();',
+        '    await baz();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
     }, {
       code: [
         'class Hello extends Component {',
@@ -1657,6 +1719,50 @@ ruleTester.run('no-unused-prop-types', rule, {
         '}'
       ].join('\n'),
       parser: 'babel-eslint'
+    }, {
+      // Props used inside of an async class method
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '  }',
+        '  async method() {',
+        '    await this.props.foo();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
+    }, {
+      // Multiple props used inside of an async class method
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  async method() {',
+        '    await this.props.foo();',
+        '    await this.props.bar();',
+        '    await this.props.baz();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
+    }, {
+      // Destrucuted props inside of async class method
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '  }',
+        '  async method() {',
+        '    const { foo } = this.props;',
+        '    await foo();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
     }, {
       code: [
         'class Hello extends Component {',
@@ -1695,6 +1801,62 @@ ruleTester.run('no-unused-prop-types', rule, {
         '}'
       ].join('\n'),
       parser: 'babel-eslint'
+    }, {
+      // Multiple destructured props inside of async class method
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  async method() {',
+        '    const { foo, bar, baz } = this.props;',
+        '    await foo();',
+        '    await bar();',
+        '    await baz();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
+    }, {
+      // factory functions that return async functions
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  factory() {',
+        '    return async () => {',
+        '      await this.props.foo();',
+        '      await this.props.bar();',
+        '      await this.props.baz();',
+        '    };',
+        '  }',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
+    }, {
+      // factory functions that return async functions
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  factory() {',
+        '    return async function onSubmit() {',
+        '      await this.props.foo();',
+        '      await this.props.bar();',
+        '      await this.props.baz();',
+        '    };',
+        '  }',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
     }, {
       code: [
         'class Hello extends Component {',
@@ -1713,6 +1875,57 @@ ruleTester.run('no-unused-prop-types', rule, {
         '  bar: PropTypes.string,',
         '};'
       ].join('\n')
+    }, {
+      // Multiple props used inside of an async method
+      code: [
+        'class Example extends Component {',
+        '  async method() {',
+        '    await this.props.foo();',
+        '    await this.props.bar();',
+        '  };',
+        '}',
+        'Example.propTypes = {',
+        '  foo: PropTypes.func,',
+        '  bar: PropTypes.func,',
+        '}'
+      ].join('\n'),
+      parserOptions: Object.assign({}, parserOptions, {ecmaVersion: 2017})
+    }, {
+      // Multiple props used inside of an async function
+      code: [
+        'class Example extends Component {',
+        '  render() {',
+        '    async function onSubmit() {',
+        '      await this.props.foo();',
+        '      await this.props.bar();',
+        '    }',
+        '    return <Form onSubmit={onSubmit} />',
+        '  };',
+        '}',
+        'Example.propTypes = {',
+        '  foo: PropTypes.func,',
+        '  bar: PropTypes.func,',
+        '}'
+      ].join('\n'),
+      parserOptions: Object.assign({}, parserOptions, {ecmaVersion: 2017})
+    }, {
+      // Multiple props used inside of an async arrow function
+      code: [
+        'class Example extends Component {',
+        '  render() {',
+        '    const onSubmit = async () => {',
+        '      await this.props.foo();',
+        '      await this.props.bar();',
+        '    }',
+        '    return <Form onSubmit={onSubmit} />',
+        '  };',
+        '}',
+        'Example.propTypes = {',
+        '  foo: PropTypes.func,',
+        '  bar: PropTypes.func,',
+        '}'
+      ].join('\n'),
+      parserOptions: Object.assign({}, parserOptions, {ecmaVersion: 2017})
     }
   ],
 
@@ -2680,6 +2893,27 @@ ruleTester.run('no-unused-prop-types', rule, {
         line: 4,
         column: 10
       }]
+    }, {
+      // Multiple props used inside of an async class property
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  classProperty = async () => {',
+        '    await this.props.foo();',
+        '    await this.props.bar();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      errors: [{
+        message: '\'baz\' PropType is defined but prop is never used',
+        line: 5,
+        column: 10
+      }]
     }, {
       code: [
         'class Hello extends Component {',
@@ -2720,6 +2954,47 @@ ruleTester.run('no-unused-prop-types', rule, {
         line: 4,
         column: 10
       }]
+    }, {
+      // Multiple destructured props inside of async class property
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  classProperty = async () => {',
+        '    const { bar, baz } = this.props;',
+        '    await bar();',
+        '    await baz();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      errors: [{
+        message: '\'foo\' PropType is defined but prop is never used'
+      }]
+    }, {
+      // Multiple props used inside of an async class method
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  async method() {',
+        '    await this.props.foo();',
+        '    await this.props.baz();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      errors: [{
+        message: '\'bar\' PropType is defined but prop is never used',
+        line: 4,
+        column: 10
+      }]
     }, {
       code: [
         'class Hello extends Component {',
@@ -2760,6 +3035,51 @@ ruleTester.run('no-unused-prop-types', rule, {
         line: 4,
         column: 10
       }]
+    }, {
+      // Multiple destructured props inside of async class method
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  async method() {',
+        '    const { foo, bar } = this.props;',
+        '    await foo();',
+        '    await bar();',
+        '  };',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      errors: [{
+        message: '\'baz\' PropType is defined but prop is never used',
+        line: 5,
+        column: 10
+      }]
+    }, {
+      // factory functions that return async functions
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  factory() {',
+        '    return async () => {',
+        '      await this.props.foo();',
+        '      await this.props.bar();',
+        '    };',
+        '  }',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      errors: [{
+        message: '\'baz\' PropType is defined but prop is never used',
+        line: 5,
+        column: 10
+      }]
     }, {
       code: [
         'class Hello extends Component {',
@@ -2824,6 +3144,77 @@ ruleTester.run('no-unused-prop-types', rule, {
       settings: {
         propWrapperFunctions: ['forbidExtraProps']
       }
+    }, {
+      // factory functions that return async functions
+      code: [
+        'export class Example extends Component {',
+        '  static propTypes = {',
+        '    foo: PropTypes.func,',
+        '    bar: PropTypes.func,',
+        '    baz: PropTypes.func,',
+        '  }',
+        '  factory() {',
+        '    return async function onSubmit() {',
+        '      await this.props.bar();',
+        '      await this.props.baz();',
+        '    };',
+        '  }',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      errors: [{
+        message: '\'foo\' PropType is defined but prop is never used',
+        line: 3,
+        column: 10
+      }]
+    }, {
+      // Multiple props used inside of an async function
+      code: [
+        'class Example extends Component {',
+        '  render() {',
+        '    async function onSubmit() {',
+        '      await this.props.foo();',
+        '      await this.props.bar();',
+        '    }',
+        '    return <Form onSubmit={onSubmit} />',
+        '  };',
+        '}',
+        'Example.propTypes = {',
+        '  foo: PropTypes.func,',
+        '  bar: PropTypes.func,',
+        '  baz: PropTypes.func,',
+        '}'
+      ].join('\n'),
+      parserOptions: Object.assign({}, parserOptions, {ecmaVersion: 2017}),
+      errors: [{
+        message: '\'baz\' PropType is defined but prop is never used',
+        line: 13,
+        column: 8
+      }]
+    }, {
+      // Multiple props used inside of an async arrow function
+      code: [
+        'class Example extends Component {',
+        '  render() {',
+        '    const onSubmit = async () => {',
+        '      await this.props.bar();',
+        '      await this.props.baz();',
+        '    }',
+        '    return <Form onSubmit={onSubmit} />',
+        '  };',
+        '}',
+        'Example.propTypes = {',
+        '  foo: PropTypes.func,',
+        '  bar: PropTypes.func,',
+        '  baz: PropTypes.func,',
+        '}'
+      ].join('\n'),
+      parserOptions: Object.assign({}, parserOptions, {ecmaVersion: 2017}),
+      errors: [{
+        message: '\'foo\' PropType is defined but prop is never used',
+        line: 11,
+        column: 8
+      }]
     }
     /* , {
       // Enable this when the following issue is fixed