diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..4d17014
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,29 @@
+module.exports = {
+ env: {
+ browser: true,
+ es6: true,
+ },
+ globals: {
+ Atomics: 'readonly',
+ SharedArrayBuffer: 'readonly',
+ },
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ },
+ plugins: [
+ 'react',
+ '@typescript-eslint',
+ 'react-hooks',
+ '@react-hook-utilities',
+ ],
+ rules: {
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'warn',
+ '@react-hook-utilities/exhaustive-deps': 'warn',
+ },
+};
diff --git a/.gitignore b/.gitignore
index a5107aa..242f43b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@ node_modules/
dist/
*.log
coverage/
-docs/
\ No newline at end of file
+docs/
+pageDocs/
\ No newline at end of file
diff --git a/README.md b/README.md
index 70ef41d..a7a0da7 100644
--- a/README.md
+++ b/README.md
@@ -169,7 +169,21 @@ A state that only resolves after setting truthy values.
# Typescript
-react-hook-utilities sees Typescript is a first-class citizen. The library is built for and around Typescript and you'll get bonus points for using it. Nonetheless, pure JavaScript files are also available if you're _that_ guy.
+**react-hook-utilities** sees Typescript is a first-class citizen. The library is built for and around Typescript and you'll get bonus points for using it. Nonetheless, pure JavaScript files are also available if you're _that_ guy.
+
+# ESLint
+
+If you're using ESLint and don't want to lose your errors and warnings regarding dependencies, **react-hook-utilities** comes packaged with an [ESLint plugin](eslint-plugin/README.md) to lint it's own hooks. It is recommended to install the plugin as a local dependency:
+
+```sh
+$ yarn add -D ./node_modules/react-hook-utilities/eslint-plugin
+```
+
+We recommend you read the [full documentation](eslint-plugin/README.md) on how to use the ESLint plugin
+
+# Documentation
+
+The documentation is available at: https://fjcaetano.github.com/react-hook-utilities
# [Full Documentation](https://fjcaetano.github.com/react-hook-utilities)
diff --git a/eslint-plugin/README.md b/eslint-plugin/README.md
new file mode 100644
index 0000000..a1cea19
--- /dev/null
+++ b/eslint-plugin/README.md
@@ -0,0 +1,51 @@
+# @react-hook-utilities/eslint-plugin
+
+ESLint rules for hook react-hook-utilities
+
+## Installation
+
+You'll first need to install [ESLint](http://eslint.org) and [react-hook-utilities](https://www.npmjs.com/package/react-hook-utilities)
+
+```
+$ npm i eslint react-hook-utilities --save-dev
+```
+
+Next, install `@react-hook-utilities/eslint-plugin` directly from the host dependency:
+
+```
+$ npm install ./node_modules/react-hook-utilities/eslint-plugin --save-dev
+```
+
+**Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `react-hook-utilities` globally.
+
+## Usage
+
+Add `@react-hook-utilities` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
+
+```json
+{
+ "plugins": [
+ "@react-hook-utilities"
+ ]
+}
+```
+
+
+Then configure the rules you want to use under the rules section.
+
+```json
+{
+ "rules": {
+ "@react-hook-utilities/exhaustive-deps": 2
+ }
+}
+```
+
+## Supported Rules
+
+* exhaustive-deps: Ensures all hooks external references have been declared as dependencies.
+
+
+
+
+
diff --git a/eslint-plugin/lib/__tests__/ESLintRuleExhaustiveDeps-test.js b/eslint-plugin/lib/__tests__/ESLintRuleExhaustiveDeps-test.js
new file mode 100644
index 0000000..be2e969
--- /dev/null
+++ b/eslint-plugin/lib/__tests__/ESLintRuleExhaustiveDeps-test.js
@@ -0,0 +1,5067 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * Original Source: https://github.com/facebook/react/blob/9e64bf18e11828d6b4c0363bff5ed2eca1ccd838/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+ */
+'use strict';
+
+const ESLintTester = require('eslint').RuleTester;
+
+const ReactHooksESLintPlugin = require('..');
+
+const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['exhaustive-deps'];
+ESLintTester.setDefaultConfig({
+ parser: require.resolve('babel-eslint'),
+ parserOptions: {
+ ecmaVersion: 6,
+ sourceType: 'module',
+ },
+});
+
+// ***************************************************
+// For easier local testing, you can add to any case:
+// {
+// skip: true,
+// --or--
+// only: true,
+// ...
+// }
+// ***************************************************
+
+const tests = {
+ valid: [
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ });
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useDidMount(() => {
+ console.log(local);
+ });
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useDidMount(() => {
+ return history.listen();
+ });
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useDidMount(async () => {
+ console.log(local);
+ });
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useDidUnmount(() => {
+ console.log(local);
+ });
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useDidUnmount(async () => {
+ console.log(local);
+ });
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ useAsyncEffect(async () => {
+ const local = {};
+ console.log(local);
+ }, []);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ },
+ {
+ // OK because `props` wasn't defined.
+ // We don't technically know if `props` is supposed
+ // to be an import that hasn't been added yet, or
+ // a component-level variable. Ignore it until it
+ // gets defined (a different rule would flag it anyway).
+ code: `
+ function MyComponent() {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, []);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ {
+ const local2 = {};
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ });
+ }
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ {
+ const local2 = {};
+ useWorker(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local1, local2]);
+ }
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ function MyNestedComponent() {
+ const local2 = {};
+ useWorker(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local2]);
+ }
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ {
+ const local2 = {};
+ useWorkerState(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local1, local2]);
+ }
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ function MyNestedComponent() {
+ const local2 = {};
+ useWorkerState(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local2]);
+ }
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ useAsyncEffect(async () => {
+ console.log(unresolved);
+ }, []);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [,,,local,,,]);
+ }
+ `,
+ },
+ {
+ // Regression test
+ code: `
+ function MyComponent({ foo }) {
+ useAsyncEffect(async () => {
+ console.log(foo.length);
+ }, [foo]);
+ }
+ `,
+ },
+ {
+ // Regression test
+ code: `
+ function MyComponent({ foo }) {
+ useAsyncEffect(async () => {
+ console.log(foo.length);
+ console.log(foo.slice(0));
+ }, [foo]);
+ }
+ `,
+ },
+ {
+ // Regression test
+ code: `
+ function MyComponent({ history }) {
+ useAsyncEffect(async () => {
+ return history.listen();
+ }, [history]);
+ }
+ `,
+ },
+ {
+ // Valid because they have meaning without deps.
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {});
+ useAsyncLayoutEffect(async () => {});
+ useConditionalEffect(props.innerRef, () => {});
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props.bar, props.foo]);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props.foo, props.bar]);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ console.log(local);
+ }, [props.foo, props.bar, local]);
+ }
+ `,
+ },
+ {
+ // [props, props.foo] is technically unnecessary ('props' covers 'props.foo').
+ // However, it's valid for effects to over-specify their deps.
+ // So we don't warn about this. We *would* warn about useEffectUpdate/useWorker.
+ code: `
+ function MyComponent(props) {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props, props.foo]);
+
+ let color = {}
+ useAsyncEffect(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(color);
+ }, [props.foo, props.foo.bar.baz, color]);
+ }
+ `,
+ },
+ {
+ // Valid because we don't care about hooks outside of components.
+ code: `
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, []);
+ `,
+ },
+ {
+ // Valid because we don't care about hooks outside of components.
+ code: `
+ const local1 = {};
+ {
+ const local2 = {};
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, []);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useRef();
+ useAsyncEffect(async () => {
+ console.log(ref.current);
+ }, [ref]);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useRef();
+ useAsyncEffect(async () => {
+ console.log(ref.current);
+ }, []);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent({ maybeRef2, foo }) {
+ const definitelyRef1 = useRef();
+ const definitelyRef2 = useRef();
+ const maybeRef1 = useSomeOtherRefyThing();
+ const [state1, setState1] = useState();
+ const [state2, setState2] = React.useState();
+ const [state3, dispatch1] = useReducer();
+ const [state4, dispatch2] = React.useReducer();
+ const [state5, maybeSetState] = useFunnyState();
+ const [state6, maybeDispatch] = useFunnyReducer();
+ const mySetState = useWorker(async () => {}, []);
+ let myDispatch = useWorker(async () => {}, []);
+
+ useAsyncEffect(async () => {
+ // Known to be static
+ console.log(definitelyRef1.current);
+ console.log(definitelyRef2.current);
+ console.log(maybeRef1.current);
+ console.log(maybeRef2.current);
+ setState1();
+ setState2();
+ dispatch1();
+ dispatch2();
+
+ // Dynamic
+ console.log(state1);
+ console.log(state2);
+ console.log(state3);
+ console.log(state4);
+ console.log(state5);
+ console.log(state6);
+ mySetState();
+ myDispatch();
+
+ // Not sure; assume dynamic
+ maybeSetState();
+ maybeDispatch();
+ }, [
+ // Dynamic
+ state1, state2, state3, state4, state5, state6,
+ maybeRef1, maybeRef2,
+
+ // Not sure; assume dynamic
+ mySetState, myDispatch,
+ maybeSetState, maybeDispatch
+
+ // In this test, we don't specify static deps.
+ // That should be okay.
+ ]);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent({ maybeRef2 }) {
+ const definitelyRef1 = useRef();
+ const definitelyRef2 = useRef();
+ const maybeRef1 = useSomeOtherRefyThing();
+
+ const [state1, setState1] = useState();
+ const [state2, setState2] = React.useState();
+ const [state3, dispatch1] = useReducer();
+ const [state4, dispatch2] = React.useReducer();
+
+ const [state5, maybeSetState] = useFunnyState();
+ const [state6, maybeDispatch] = useFunnyReducer();
+
+ const mySetState = useWorker(async () => {}, []);
+ let myDispatch = useWorker(async () => {}, []);
+
+ useAsyncEffect(async () => {
+ // Known to be static
+ console.log(definitelyRef1.current);
+ console.log(definitelyRef2.current);
+ console.log(maybeRef1.current);
+ console.log(maybeRef2.current);
+ setState1();
+ setState2();
+ dispatch1();
+ dispatch2();
+
+ // Dynamic
+ console.log(state1);
+ console.log(state2);
+ console.log(state3);
+ console.log(state4);
+ console.log(state5);
+ console.log(state6);
+ mySetState();
+ myDispatch();
+
+ // Not sure; assume dynamic
+ maybeSetState();
+ maybeDispatch();
+ }, [
+ // Dynamic
+ state1, state2, state3, state4, state5, state6,
+ maybeRef1, maybeRef2,
+
+ // Not sure; assume dynamic
+ mySetState, myDispatch,
+ maybeSetState, maybeDispatch,
+
+ // In this test, we specify static deps.
+ // That should be okay too!
+ definitelyRef1, definitelyRef2, setState1, setState2, dispatch1, dispatch2
+ ]);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent({ maybeRef2, foo }) {
+ const definitelyRef1 = useRef();
+ const definitelyRef2 = useRef();
+ const maybeRef1 = useSomeOtherRefyThing();
+ const [state1, setState1] = useState();
+ const [state2, setState2] = React.useState();
+ const [state3, dispatch1] = useReducer();
+ const [state4, dispatch2] = React.useReducer();
+ const [state5, maybeSetState] = useFunnyState();
+ const [state6, maybeDispatch] = useFunnyReducer();
+ const mySetState = useWorkerState(async () => {}, []);
+ let myDispatch = useWorkerState(async () => {}, []);
+
+ useAsyncEffect(async () => {
+ // Known to be static
+ console.log(definitelyRef1.current);
+ console.log(definitelyRef2.current);
+ console.log(maybeRef1.current);
+ console.log(maybeRef2.current);
+ setState1();
+ setState2();
+ dispatch1();
+ dispatch2();
+
+ // Dynamic
+ console.log(state1);
+ console.log(state2);
+ console.log(state3);
+ console.log(state4);
+ console.log(state5);
+ console.log(state6);
+ mySetState();
+ myDispatch();
+
+ // Not sure; assume dynamic
+ maybeSetState();
+ maybeDispatch();
+ }, [
+ // Dynamic
+ state1, state2, state3, state4, state5, state6,
+ maybeRef1, maybeRef2,
+
+ // Not sure; assume dynamic
+ mySetState, myDispatch,
+ maybeSetState, maybeDispatch
+
+ // In this test, we don't specify static deps.
+ // That should be okay.
+ ]);
+ }
+ `,
+ },
+ {
+ code: `
+ function MyComponent({ maybeRef2 }) {
+ const definitelyRef1 = useRef();
+ const definitelyRef2 = useRef();
+ const maybeRef1 = useSomeOtherRefyThing();
+
+ const [state1, setState1] = useState();
+ const [state2, setState2] = React.useState();
+ const [state3, dispatch1] = useReducer();
+ const [state4, dispatch2] = React.useReducer();
+
+ const [state5, maybeSetState] = useFunnyState();
+ const [state6, maybeDispatch] = useFunnyReducer();
+
+ const mySetState = useWorkerState(async () => {}, []);
+ let myDispatch = useWorkerState(async () => {}, []);
+
+ useAsyncEffect(async () => {
+ // Known to be static
+ console.log(definitelyRef1.current);
+ console.log(definitelyRef2.current);
+ console.log(maybeRef1.current);
+ console.log(maybeRef2.current);
+ setState1();
+ setState2();
+ dispatch1();
+ dispatch2();
+
+ // Dynamic
+ console.log(state1);
+ console.log(state2);
+ console.log(state3);
+ console.log(state4);
+ console.log(state5);
+ console.log(state6);
+ mySetState();
+ myDispatch();
+
+ // Not sure; assume dynamic
+ maybeSetState();
+ maybeDispatch();
+ }, [
+ // Dynamic
+ state1, state2, state3, state4, state5, state6,
+ maybeRef1, maybeRef2,
+
+ // Not sure; assume dynamic
+ mySetState, myDispatch,
+ maybeSetState, maybeDispatch,
+
+ // In this test, we specify static deps.
+ // That should be okay too!
+ definitelyRef1, definitelyRef2, setState1, setState2, dispatch1, dispatch2
+ ]);
+ }
+ `,
+ },
+ {
+ code: `
+ const MyComponent = forwardRef((props, ref) => {
+ useConditionalEffect(ref, () => ({
+ focus() {
+ alert(props.hello);
+ }
+ }))
+ });
+ `,
+ },
+ {
+ code: `
+ const MyComponent = forwardRef((props, ref) => {
+ useConditionalEffect(ref, () => ({
+ focus() {
+ alert(props.hello);
+ }
+ }), [props.hello])
+ });
+ `,
+ },
+ {
+ // This is not ideal but warning would likely create
+ // too many false positives. We do, however, prevent
+ // direct assignments.
+ code: `
+ function MyComponent(props) {
+ let obj = {};
+ useAsyncEffect(async () => {
+ obj.foo = true;
+ }, [obj]);
+ }
+ `,
+ },
+ {
+ // Valid because we assign ref.current
+ // ourselves. Therefore it's likely not
+ // a ref managed by React.
+ code: `
+ function MyComponent() {
+ const myRef = useRef();
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ myRef.current = {};
+ return () => {
+ console.log(myRef.current.toString())
+ };
+ }, []);
+ return
;
+ }
+ `,
+ },
+ {
+ // Valid because we assign ref.current
+ // ourselves. Therefore it's likely not
+ // a ref managed by React.
+ code: `
+ function useMyThing(myRef) {
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ myRef.current = {};
+ return () => {
+ console.log(myRef.current.toString())
+ };
+ }, [myRef]);
+ }
+ `,
+ },
+ {
+ // Valid because the ref is captured.
+ code: `
+ function MyComponent() {
+ const myRef = useRef();
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ const node = myRef.current;
+ node.addEventListener('mousemove', handleMove);
+ return () => node.removeEventListener('mousemove', handleMove);
+ }, []);
+ return
;
+ }
+ `,
+ },
+ {
+ // Valid because the ref is captured.
+ code: `
+ function useMyThing(myRef) {
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ const node = myRef.current;
+ node.addEventListener('mousemove', handleMove);
+ return () => node.removeEventListener('mousemove', handleMove);
+ }, [myRef]);
+ return
;
+ }
+ `,
+ },
+ {
+ // Valid because it's not an effect.
+ code: `
+ function useMyThing(myRef) {
+ useWorker(async () => {
+ const handleMouse = () => {};
+ myRef.current.addEventListener('mousemove', handleMouse);
+ myRef.current.addEventListener('mousein', handleMouse);
+ return function() {
+ setTimeout(() => {
+ myRef.current.removeEventListener('mousemove', handleMouse);
+ myRef.current.removeEventListener('mousein', handleMouse);
+ });
+ }
+ }, [myRef]);
+ }
+ `,
+ },
+ {
+ // Valid because it's not an effect.
+ code: `
+ function useMyThing(myRef) {
+ useWorkerState(async () => {
+ const handleMouse = () => {};
+ myRef.current.addEventListener('mousemove', handleMouse);
+ myRef.current.addEventListener('mousein', handleMouse);
+ return function() {
+ setTimeout(() => {
+ myRef.current.removeEventListener('mousemove', handleMouse);
+ myRef.current.removeEventListener('mousein', handleMouse);
+ });
+ }
+ }, [myRef]);
+ }
+ `,
+ },
+ {
+ // Valid because we read ref.current in a function that isn't cleanup.
+ code: `
+ function useMyThing() {
+ const myRef = useRef();
+ useAsyncEffect(async () => {
+ const handleMove = () => {
+ console.log(myRef.current)
+ };
+ window.addEventListener('mousemove', handleMove);
+ return () => window.removeEventListener('mousemove', handleMove);
+ }, []);
+ return
;
+ }
+ `,
+ },
+ {
+ // Valid because we read ref.current in a function that isn't cleanup.
+ code: `
+ function useMyThing() {
+ const myRef = useRef();
+ useAsyncEffect(async () => {
+ const handleMove = () => {
+ return () => window.removeEventListener('mousemove', handleMove);
+ };
+ window.addEventListener('mousemove', handleMove);
+ return () => {};
+ }, []);
+ return
;
+ }
+ `,
+ },
+ {
+ // Valid because it's a primitive constant.
+ code: `
+ function MyComponent() {
+ const local1 = 42;
+ const local2 = '42';
+ const local3 = null;
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ console.log(local3);
+ }, []);
+ }
+ `,
+ },
+ {
+ // It's not a mistake to specify constant values though.
+ code: `
+ function MyComponent() {
+ const local1 = 42;
+ const local2 = '42';
+ const local3 = null;
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ console.log(local3);
+ }, [local1, local2, local3]);
+ }
+ `,
+ },
+ {
+ // It is valid for effects to over-specify their deps.
+ code: `
+ function MyComponent(props) {
+ const local = props.local;
+ useAsyncEffect(async () => {}, [local]);
+ }
+ `,
+ },
+ {
+ // Valid even though activeTab is "unused".
+ // We allow over-specifying deps for effects, but not callbacks or memo.
+ code: `
+ function Foo({ activeTab }) {
+ useAsyncEffect(async () => {
+ window.scrollTo(0, 0);
+ }, [activeTab]);
+ }
+ `,
+ },
+ {
+ // It is valid to specify broader effect deps than strictly necessary.
+ // Don't warn for this.
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props]);
+ useAsyncEffect(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo]);
+ useAsyncEffect(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar]);
+ useAsyncEffect(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ },
+ {
+ // It is *also* valid to specify broader memo/callback deps than strictly necessary.
+ // Don't warn for this either.
+ code: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props]);
+ const fn2 = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo]);
+ const fn3 = useEffectUpdate(() => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar]);
+ const fn4 = useEffectUpdate(() => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ },
+ {
+ // It is *also* valid to specify broader memo/callback deps than strictly necessary.
+ // Don't warn for this either.
+ code: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props]);
+ const fn2 = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo]);
+ const fn3 = useEffectUpdate(() => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar]);
+ const fn4 = useEffectUpdate(() => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ },
+ {
+ // Declaring handleNext is optional because
+ // it doesn't use anything in the function scope.
+ code: `
+ function MyComponent(props) {
+ function handleNext1() {
+ console.log('hello');
+ }
+ const handleNext2 = () => {
+ console.log('hello');
+ };
+ let handleNext3 = function() {
+ console.log('hello');
+ };
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext1);
+ }, []);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext2);
+ }, []);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext3);
+ }, []);
+ }
+ `,
+ },
+ {
+ // Declaring handleNext is optional because
+ // it doesn't use anything in the function scope.
+ code: `
+ function MyComponent(props) {
+ function handleNext() {
+ console.log('hello');
+ }
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext);
+ }, []);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext);
+ }, []);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext);
+ }, []);
+ }
+ `,
+ },
+ {
+ // Declaring handleNext is optional because
+ // everything they use is fully static.
+ code: `
+ function MyComponent(props) {
+ let [, setState] = useState();
+ let [, dispatch] = React.useReducer();
+
+ function handleNext1(value) {
+ let value2 = value * 100;
+ setState(value2);
+ console.log('hello');
+ }
+ const handleNext2 = (value) => {
+ setState(foo(value));
+ console.log('hello');
+ };
+ let handleNext3 = function(value) {
+ console.log(value);
+ dispatch({ type: 'x', value });
+ };
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext1);
+ }, []);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext2);
+ }, []);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext3);
+ }, []);
+ }
+ `,
+ },
+ {
+ code: `
+ function useInterval(callback, delay) {
+ const savedCallback = useRef();
+ useAsyncEffect(async () => {
+ savedCallback.current = callback;
+ });
+ useAsyncEffect(async () => {
+ function tick() {
+ savedCallback.current();
+ }
+ if (delay !== null) {
+ let id = setInterval(tick, delay);
+ return () => clearInterval(id);
+ }
+ }, [delay]);
+ }
+ `,
+ },
+ {
+ code: `
+ function Counter() {
+ const [count, setCount] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(c => c + 1);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ },
+ {
+ code: `
+ function Counter() {
+ const [count, setCount] = useState(0);
+
+ function tick() {
+ setCount(c => c + 1);
+ }
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ tick();
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ },
+ {
+ code: `
+ function Counter() {
+ const [count, dispatch] = useReducer((state, action) => {
+ if (action === 'inc') {
+ return state + 1;
+ }
+ }, 0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ dispatch('inc');
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ },
+ {
+ code: `
+ function Counter() {
+ const [count, dispatch] = useReducer((state, action) => {
+ if (action === 'inc') {
+ return state + 1;
+ }
+ }, 0);
+
+ const tick = () => {
+ dispatch('inc');
+ };
+
+ useAsyncEffect(async () => {
+ let id = setInterval(tick, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ },
+ {
+ // Regression test for a crash
+ code: `
+ function Podcasts() {
+ useAsyncEffect(async () => {
+ setPodcasts([]);
+ }, []);
+ let [podcasts, setPodcasts] = useState(null);
+ }
+ `,
+ },
+ {
+ code: `
+ function withFetch(fetchPodcasts) {
+ return function Podcasts({ id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ fetchPodcasts(id).then(setPodcasts);
+ }, [id]);
+ }
+ }
+ `,
+ },
+ {
+ code: `
+ function Podcasts({ id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ function doFetch({ fetchPodcasts }) {
+ fetchPodcasts(id).then(setPodcasts);
+ }
+ doFetch({ fetchPodcasts: API.fetchPodcasts });
+ }, [id]);
+ }
+ `,
+ },
+ {
+ code: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+
+ function increment(x) {
+ return x + 1;
+ }
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ },
+ {
+ code: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+
+ function increment(x) {
+ return x + 1;
+ }
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => increment(count));
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ },
+ {
+ code: `
+ import increment from './increment';
+ function Counter() {
+ let [count, setCount] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ },
+ {
+ code: `
+ function withStuff(increment) {
+ return function Counter() {
+ let [count, setCount] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ }
+ `,
+ },
+ {
+ code: `
+ function App() {
+ const [query, setQuery] = useState('react');
+ const [state, setState] = useState(null);
+ useAsyncEffect(async () => {
+ let ignore = false;
+ fetchSomething();
+ async function fetchSomething() {
+ const result = await (await fetch('http://hn.algolia.com/api/v1/search?query=' + query)).json();
+ if (!ignore) setState(result);
+ }
+ return () => { ignore = true; };
+ }, [query]);
+ return (
+ <>
+ setQuery(e.target.value)} />
+ {JSON.stringify(state)}
+ >
+ );
+ }
+ `,
+ },
+ {
+ code: `
+ function Example() {
+ const foo = useWorker(async () => {
+ foo();
+ }, []);
+ }
+ `,
+ },
+ {
+ code: `
+ function Example({ prop }) {
+ const foo = useWorker(async () => {
+ if (prop) {
+ foo();
+ }
+ }, [prop]);
+ }
+ `,
+ },
+ {
+ code: `
+ function Example() {
+ const foo = useWorkerState(async () => {
+ foo();
+ }, []);
+ }
+ `,
+ },
+ {
+ code: `
+ function Example({ prop }) {
+ const foo = useWorkerState(async () => {
+ if (prop) {
+ foo();
+ }
+ }, [prop]);
+ }
+ `,
+ },
+ {
+ code: `
+ function Hello() {
+ const [state, setState] = useState(0);
+ useAsyncEffect(async () => {
+ const handleResize = () => setState(window.innerWidth);
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ });
+ }
+ `,
+ }, // Ignore Generic Type Variables for arrow functions
+ {
+ code: `
+ function Example({ prop }) {
+ const bar = useAsyncEffect((a: T): Hello => {
+ prop();
+ }, [prop]);
+ }
+ `,
+ }, // Ignore arguments keyword for arrow functions.
+ {
+ code: `
+ function Example() {
+ useAsyncEffect(async () => {
+ arguments
+ }, [])
+ }
+ `,
+ },
+ {
+ code: `
+ function Example() {
+ useAsyncEffect(async () => {
+ const bar = () => {
+ arguments;
+ };
+ bar();
+ }, [])
+ }
+ `,
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ function MyComponent() {
+ const ref = useLazyRef();
+ const [state, setState] = useState();
+ useAsyncEffect(async () => {
+ ref.current = {};
+ setState(state + 1);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const ref = useLazyRef();
+ const [state, setState] = useState();
+ useAsyncEffect(async () => {
+ ref.current = {};
+ setState(state + 1);
+ }, [state]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'state'. " +
+ 'Either include it or remove the dependency array. ' +
+ `You can also do a functional update 'setState(s => ...)' ` +
+ `if you only need 'state' in the 'setState' call.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useLazyRef();
+ const [state, setState] = useState();
+ useAsyncEffect(() => {
+ ref.current = {};
+ setState(state + 1);
+ }, [ref]);
+ }
+ `,
+ // We don't ask to remove static deps but don't add them either.
+ // Don't suggest removing "ref" (it's fine either way)
+ // but *do* add "state". *Don't* add "setState" ourselves.
+ output: `
+ function MyComponent() {
+ const ref = useLazyRef();
+ const [state, setState] = useState();
+ useAsyncEffect(() => {
+ ref.current = {};
+ setState(state + 1);
+ }, [ref, state]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'state'. " +
+ 'Either include it or remove the dependency array. ' +
+ `You can also do a functional update 'setState(s => ...)' ` +
+ `if you only need 'state' in the 'setState' call.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const ref1 = useLazyRef();
+ const ref2 = useLazyRef();
+ useAsyncEffect(() => {
+ ref1.current.focus();
+ console.log(ref2.current.textContent);
+ alert(props.someOtherRefs.current.innerHTML);
+ fetch(props.color);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const ref1 = useLazyRef();
+ const ref2 = useLazyRef();
+ useAsyncEffect(() => {
+ ref1.current.focus();
+ console.log(ref2.current.textContent);
+ alert(props.someOtherRefs.current.innerHTML);
+ fetch(props.color);
+ }, [props.color, props.someOtherRefs]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'props.color' and 'props.someOtherRefs'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const ref1 = useLazyRef();
+ const ref2 = useLazyRef();
+ useAsyncEffect(() => {
+ ref1.current.focus();
+ console.log(ref2.current.textContent);
+ alert(props.someOtherRefs.current.innerHTML);
+ fetch(props.color);
+ }, [ref1.current, ref2.current, props.someOtherRefs, props.color]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const ref1 = useLazyRef();
+ const ref2 = useLazyRef();
+ useAsyncEffect(() => {
+ ref1.current.focus();
+ console.log(ref2.current.textContent);
+ alert(props.someOtherRefs.current.innerHTML);
+ fetch(props.color);
+ }, [props.someOtherRefs, props.color]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Mutable values like 'ref1.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useLazyRef();
+ useAsyncEffect(() => {
+ console.log(ref.current);
+ }, [ref.current]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const ref = useLazyRef();
+ useAsyncEffect(() => {
+ console.log(ref.current);
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has an unnecessary dependency: 'ref.current'. " +
+ 'Either exclude it or remove the dependency array. ' +
+ "Mutable values like 'ref.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ activeTab }) {
+ const ref1 = useLazyRef();
+ const ref2 = useLazyRef();
+ useAsyncEffect(() => {
+ ref1.current.scrollTop = 0;
+ ref2.current.scrollTop = 0;
+ }, [ref1.current, ref2.current, activeTab]);
+ }
+ `,
+ output: `
+ function MyComponent({ activeTab }) {
+ const ref1 = useLazyRef();
+ const ref2 = useLazyRef();
+ useAsyncEffect(() => {
+ ref1.current.scrollTop = 0;
+ ref2.current.scrollTop = 0;
+ }, [activeTab]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Mutable values like 'ref1.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ activeTab, initY }) {
+ const ref1 = useLazyRef();
+ const ref2 = useLazyRef();
+ const fn = useWorker(() => {
+ ref1.current.scrollTop = initY;
+ ref2.current.scrollTop = initY;
+ }, [ref1.current, ref2.current, activeTab, initY]);
+ }
+ `,
+ output: `
+ function MyComponent({ activeTab, initY }) {
+ const ref1 = useLazyRef();
+ const ref2 = useLazyRef();
+ const fn = useWorker(() => {
+ ref1.current.scrollTop = initY;
+ ref2.current.scrollTop = initY;
+ }, [initY]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Mutable values like 'ref1.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useLazyRef();
+ useAsyncEffect(() => {
+ console.log(ref.current);
+ }, [ref.current, ref]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const ref = useLazyRef();
+ useAsyncEffect(() => {
+ console.log(ref.current);
+ }, [ref]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has an unnecessary dependency: 'ref.current'. " +
+ 'Either exclude it or remove the dependency array. ' +
+ "Mutable values like 'ref.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Note: we *could* detect it's a primitive and never assigned
+ // even though it's not a constant -- but we currently don't.
+ // So this is an error.
+ code: `
+ function MyComponent() {
+ let local = 42;
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ let local = 42;
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Regexes are literals but potentially stateful.
+ code: `
+ function MyComponent() {
+ const local = /foo/;
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = /foo/;
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Regression test
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ if (true) {
+ console.log(local);
+ }
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ if (true) {
+ console.log(local);
+ }
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Regression test
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ try {
+ console.log(local);
+ } finally {}
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ try {
+ console.log(local);
+ } finally {}
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Regression test
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ function inner() {
+ console.log(local);
+ }
+ inner();
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ function inner() {
+ console.log(local);
+ }
+ inner();
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ {
+ const local2 = {};
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, []);
+ }
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = {};
+ {
+ const local2 = {};
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local1, local2]);
+ }
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'local1' and 'local2'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ const local2 = {};
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local1]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = {};
+ const local2 = {};
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local1, local2]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local2'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ function MyNestedComponent() {
+ const local2 = {};
+ useWorker(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local1]);
+ }
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = {};
+ function MyNestedComponent() {
+ const local2 = {};
+ useWorker(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local2]);
+ }
+ }
+ `,
+ errors: [
+ "React Hook useWorker has a missing dependency: 'local2'. " +
+ 'Either include it or remove the dependency array. ' +
+ "Outer scope values like 'local1' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ function MyNestedComponent() {
+ const local2 = {};
+ useWorkerState(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local1]);
+ }
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = {};
+ function MyNestedComponent() {
+ const local2 = {};
+ useWorkerState(async () => {
+ console.log(local1);
+ console.log(local2);
+ }, [local2]);
+ }
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has a missing dependency: 'local2'. " +
+ 'Either include it or remove the dependency array. ' +
+ "Outer scope values like 'local1' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ console.log(local);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ console.log(local);
+ }, [local, local]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a duplicate dependency: 'local'. " +
+ 'Either omit it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ useWorker(async () => {}, [window]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ useWorker(async () => {}, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has an unnecessary dependency: 'window'. " +
+ 'Either exclude it or remove the dependency array. ' +
+ "Outer scope values like 'window' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ useWorkerState(async () => {}, [window]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ useWorkerState(async () => {}, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has an unnecessary dependency: 'window'. " +
+ 'Either exclude it or remove the dependency array. ' +
+ "Outer scope values like 'window' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ // It is not valid for useWorker to specify extraneous deps
+ // because it doesn't serve as a side effect trigger unlike useAsyncEffect.
+ code: `
+ function MyComponent(props) {
+ let local = props.foo;
+ useWorker(async () => {}, [local]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ let local = props.foo;
+ useWorker(async () => {}, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has an unnecessary dependency: 'local'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ // It is not valid for useWorkerState to specify extraneous deps
+ // because it doesn't serve as a side effect trigger unlike useAsyncEffect.
+ code: `
+ function MyComponent(props) {
+ let local = props.foo;
+ useWorkerState(async () => {}, [local]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ let local = props.foo;
+ useWorkerState(async () => {}, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has an unnecessary dependency: 'local'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ history }) {
+ useAsyncEffect(async () => {
+ return history.listen();
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent({ history }) {
+ useAsyncEffect(async () => {
+ return history.listen();
+ }, [history]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'history'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ history }) {
+ useAsyncEffect(async () => {
+ return [
+ history.foo.bar[2].dobedo.listen(),
+ history.foo.bar().dobedo.listen[2]
+ ];
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent({ history }) {
+ useAsyncEffect(async () => {
+ return [
+ history.foo.bar[2].dobedo.listen(),
+ history.foo.bar().dobedo.listen[2]
+ ];
+ }, [history.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'history.foo'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ useAsyncEffect(async () => {}, ['foo']);
+ }
+ `,
+ // TODO: we could autofix this.
+ output: `
+ function MyComponent() {
+ useAsyncEffect(async () => {}, ['foo']);
+ }
+ `,
+ errors: [
+ // Don't assume user meant `foo` because it's not used in the effect.
+ "The 'foo' literal is not a valid dependency because it never changes. " +
+ 'You can safely remove it.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ foo, bar, baz }) {
+ useAsyncEffect(async () => {
+ console.log(foo, bar, baz);
+ }, ['foo', 'bar']);
+ }
+ `,
+ output: `
+ function MyComponent({ foo, bar, baz }) {
+ useAsyncEffect(async () => {
+ console.log(foo, bar, baz);
+ }, [bar, baz, foo]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'bar', 'baz', and 'foo'. " +
+ 'Either include them or remove the dependency array.',
+ "The 'foo' literal is not a valid dependency because it never changes. " +
+ 'Did you mean to include foo in the array instead?',
+ "The 'bar' literal is not a valid dependency because it never changes. " +
+ 'Did you mean to include bar in the array instead?',
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ foo, bar, baz }) {
+ useAsyncEffect(async () => {
+ console.log(foo, bar, baz);
+ }, [42, false, null]);
+ }
+ `,
+ output: `
+ function MyComponent({ foo, bar, baz }) {
+ useAsyncEffect(async () => {
+ console.log(foo, bar, baz);
+ }, [bar, baz, foo]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'bar', 'baz', and 'foo'. " +
+ 'Either include them or remove the dependency array.',
+ 'The 42 literal is not a valid dependency because it never changes. You can safely remove it.',
+ 'The false literal is not a valid dependency because it never changes. You can safely remove it.',
+ 'The null literal is not a valid dependency because it never changes. You can safely remove it.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const dependencies = [];
+ useAsyncEffect(async () => {}, dependencies);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const dependencies = [];
+ useAsyncEffect(async () => {}, dependencies);
+ }
+ `,
+ errors: [
+ 'React Hook useAsyncEffect was passed a dependency list that is not an ' +
+ "array literal. This means we can't statically verify whether you've " +
+ 'passed the correct dependencies.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ const dependencies = [local];
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, dependencies);
+ }
+ `,
+ // TODO: should this autofix or bail out?
+ output: `
+ function MyComponent() {
+ const local = {};
+ const dependencies = [local];
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ 'React Hook useAsyncEffect was passed a dependency list that is not an ' +
+ "array literal. This means we can't statically verify whether you've " +
+ 'passed the correct dependencies.',
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ const dependencies = [local];
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [...dependencies]);
+ }
+ `,
+ // TODO: should this autofix or bail out?
+ output: `
+ function MyComponent() {
+ const local = {};
+ const dependencies = [local];
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ 'React Hook useAsyncEffect has a spread element in its dependency array. ' +
+ "This means we can't statically verify whether you've passed the " +
+ 'correct dependencies.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local, ...dependencies]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local, ...dependencies]);
+ }
+ `,
+ errors: [
+ 'React Hook useAsyncEffect has a spread element in its dependency array. ' +
+ "This means we can't statically verify whether you've passed the " +
+ 'correct dependencies.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [computeCacheKey(local)]);
+ }
+ `,
+ // TODO: I'm not sure this is a good idea.
+ // Maybe bail out?
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ 'React Hook useAsyncEffect has a complex expression in the dependency array. ' +
+ 'Extract it to a separate variable so it can be statically checked.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.items[0]);
+ }, [props.items[0]]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.items[0]);
+ }, [props.items]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props.items'. " +
+ 'Either include it or remove the dependency array.',
+ 'React Hook useAsyncEffect has a complex expression in the dependency array. ' +
+ 'Extract it to a separate variable so it can be statically checked.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.items[0]);
+ }, [props.items, props.items[0]]);
+ }
+ `,
+ // TODO: ideally autofix would remove the bad expression?
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.items[0]);
+ }, [props.items, props.items[0]]);
+ }
+ `,
+ errors: [
+ 'React Hook useAsyncEffect has a complex expression in the dependency array. ' +
+ 'Extract it to a separate variable so it can be statically checked.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ items }) {
+ useAsyncEffect(async () => {
+ console.log(items[0]);
+ }, [items[0]]);
+ }
+ `,
+ output: `
+ function MyComponent({ items }) {
+ useAsyncEffect(async () => {
+ console.log(items[0]);
+ }, [items]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'items'. " +
+ 'Either include it or remove the dependency array.',
+ 'React Hook useAsyncEffect has a complex expression in the dependency array. ' +
+ 'Extract it to a separate variable so it can be statically checked.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ items }) {
+ useAsyncEffect(async () => {
+ console.log(items[0]);
+ }, [items, items[0]]);
+ }
+ `,
+ // TODO: ideally autofix would remove the bad expression?
+ output: `
+ function MyComponent({ items }) {
+ useAsyncEffect(async () => {
+ console.log(items[0]);
+ }, [items, items[0]]);
+ }
+ `,
+ errors: [
+ 'React Hook useAsyncEffect has a complex expression in the dependency array. ' +
+ 'Extract it to a separate variable so it can be statically checked.',
+ ],
+ },
+ {
+ // It is not valid for useWorker to specify extraneous deps
+ // because it doesn't serve as a side effect trigger unlike useAsyncEffect.
+ // However, we generally allow specifying *broader* deps as escape hatch.
+ // So while [props, props.foo] is unnecessary, 'props' wins here as the
+ // broader one, and this is why 'props.foo' is reported as unnecessary.
+ code: `
+ function MyComponent(props) {
+ const local = {};
+ useWorker(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props, props.foo]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const local = {};
+ useWorker(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has an unnecessary dependency: 'props.foo'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ // Since we don't have 'props' in the list, we'll suggest narrow dependencies.
+ code: `
+ function MyComponent(props) {
+ const local = {};
+ useWorker(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const local = {};
+ useWorker(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props.bar, props.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has missing dependencies: 'props.bar' and 'props.foo'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ // It is not valid for useWorkerState to specify extraneous deps
+ // because it doesn't serve as a side effect trigger unlike useAsyncEffect.
+ // However, we generally allow specifying *broader* deps as escape hatch.
+ // So while [props, props.foo] is unnecessary, 'props' wins here as the
+ // broader one, and this is why 'props.foo' is reported as unnecessary.
+ code: `
+ function MyComponent(props) {
+ const local = {};
+ useWorkerState(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props, props.foo]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const local = {};
+ useWorkerState(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has an unnecessary dependency: 'props.foo'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ // Since we don't have 'props' in the list, we'll suggest narrow dependencies.
+ code: `
+ function MyComponent(props) {
+ const local = {};
+ useWorkerState(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const local = {};
+ useWorkerState(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props.bar, props.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has missing dependencies: 'props.bar' and 'props.foo'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ // Effects are allowed to over-specify deps. We'll complain about missing
+ // 'local', but we won't remove the already-specified 'local.id' from your list.
+ code: `
+ function MyComponent() {
+ const local = {id: 42};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local.id]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {id: 42};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local, local.id]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Callbacks are not allowed to over-specify deps. So we'll complain about missing
+ // 'local' and we will also *remove* 'local.id' from your list.
+ code: `
+ function MyComponent() {
+ const local = {id: 42};
+ const fn = useWorker(async () => {
+ console.log(local);
+ }, [local.id]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {id: 42};
+ const fn = useWorker(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Callbacks are not allowed to over-specify deps. So we'll complain about
+ // the unnecessary 'local.id'.
+ code: `
+ function MyComponent() {
+ const local = {id: 42};
+ const fn = useWorker(async () => {
+ console.log(local);
+ }, [local.id, local]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {id: 42};
+ const fn = useWorker(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has an unnecessary dependency: 'local.id'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has a missing dependency: 'props.foo.bar.baz'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ let color = {}
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(color);
+ }, [props.foo, props.foo.bar.baz]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ let color = {}
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(color);
+ }, [color, props.foo.bar.baz]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has a missing dependency: 'color'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Callbacks are not allowed to over-specify deps. So one of these is extra.
+ // However, it *is* allowed to specify broader deps then strictly necessary.
+ // So in this case we ask you to remove 'props.foo.bar.baz' because 'props.foo'
+ // already covers it, and having both is unnecessary.
+ // TODO: maybe consider suggesting a narrower one by default in these cases.
+ code: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar.baz, props.foo]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has an unnecessary dependency: 'props.foo.bar.baz'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(props.foo.fizz.bizz);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(props.foo.fizz.bizz);
+ }, [props.foo.bar.baz, props.foo.fizz.bizz]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has missing dependencies: 'props.foo.bar.baz' and 'props.foo.fizz.bizz'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ // Normally we allow specifying deps too broadly.
+ // So we'd be okay if 'props.foo.bar' was there rather than 'props.foo.bar.baz'.
+ // However, 'props.foo.bar.baz' is missing. So we know there is a mistake.
+ // When we're sure there is a mistake, for callbacks we will rebuild the list
+ // from scratch. This will set the user on a better path by default.
+ // This is why we end up with just 'props.foo.bar', and not them both.
+ code: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props.foo.bar);
+ }, [props.foo.bar]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has a missing dependency: 'props.foo.bar'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props);
+ console.log(props.hello);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorker(async () => {
+ console.log(props);
+ console.log(props.hello);
+ }, [props]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has a missing dependency: 'props'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Callbacks are not allowed to over-specify deps. So we'll complain about missing
+ // 'local' and we will also *remove* 'local.id' from your list.
+ code: `
+ function MyComponent() {
+ const local = {id: 42};
+ const fn = useWorkerState(async () => {
+ console.log(local);
+ }, [local.id]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {id: 42};
+ const fn = useWorkerState(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Callbacks are not allowed to over-specify deps. So we'll complain about
+ // the unnecessary 'local.id'.
+ code: `
+ function MyComponent() {
+ const local = {id: 42};
+ const fn = useWorkerState(async () => {
+ console.log(local);
+ }, [local.id, local]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {id: 42};
+ const fn = useWorkerState(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has an unnecessary dependency: 'local.id'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has a missing dependency: 'props.foo.bar.baz'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ let color = {}
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(color);
+ }, [props.foo, props.foo.bar.baz]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ let color = {}
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(color);
+ }, [color, props.foo.bar.baz]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has a missing dependency: 'color'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Callbacks are not allowed to over-specify deps. So one of these is extra.
+ // However, it *is* allowed to specify broader deps then strictly necessary.
+ // So in this case we ask you to remove 'props.foo.bar.baz' because 'props.foo'
+ // already covers it, and having both is unnecessary.
+ // TODO: maybe consider suggesting a narrower one by default in these cases.
+ code: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo.bar.baz, props.foo]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ }, [props.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has an unnecessary dependency: 'props.foo.bar.baz'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(props.foo.fizz.bizz);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar.baz);
+ console.log(props.foo.fizz.bizz);
+ }, [props.foo.bar.baz, props.foo.fizz.bizz]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has missing dependencies: 'props.foo.bar.baz' and 'props.foo.fizz.bizz'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ // Normally we allow specifying deps too broadly.
+ // So we'd be okay if 'props.foo.bar' was there rather than 'props.foo.bar.baz'.
+ // However, 'props.foo.bar.baz' is missing. So we know there is a mistake.
+ // When we're sure there is a mistake, for callbacks we will rebuild the list
+ // from scratch. This will set the user on a better path by default.
+ // This is why we end up with just 'props.foo.bar', and not them both.
+ code: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props.foo.bar);
+ }, [props.foo.bar]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has a missing dependency: 'props.foo.bar'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props);
+ console.log(props.hello);
+ }, [props.foo.bar.baz]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const fn = useWorkerState(async () => {
+ console.log(props);
+ console.log(props.hello);
+ }, [props]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has a missing dependency: 'props'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local, local]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a duplicate dependency: 'local'. " +
+ 'Either omit it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ useWorker(async () => {
+ const local1 = {};
+ console.log(local1);
+ }, [local1]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = {};
+ useWorker(async () => {
+ const local1 = {};
+ console.log(local1);
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has an unnecessary dependency: 'local1'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ useWorker(async () => {}, [local1]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = {};
+ useWorker(async () => {}, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has an unnecessary dependency: 'local1'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ useWorkerState(async () => {
+ const local1 = {};
+ console.log(local1);
+ }, [local1]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = {};
+ useWorkerState(async () => {
+ const local1 = {};
+ console.log(local1);
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has an unnecessary dependency: 'local1'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local1 = {};
+ useWorkerState(async () => {}, [local1]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = {};
+ useWorkerState(async () => {}, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has an unnecessary dependency: 'local1'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ }, [props.bar, props.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'props.bar' and 'props.foo'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ let a, b, c, d, e, f, g;
+ useAsyncEffect(async () => {
+ console.log(b, e, d, c, a, g, f);
+ }, [c, a, g]);
+ }
+ `,
+ // Don't alphabetize if it wasn't alphabetized in the first place.
+ output: `
+ function MyComponent(props) {
+ let a, b, c, d, e, f, g;
+ useAsyncEffect(async () => {
+ console.log(b, e, d, c, a, g, f);
+ }, [c, a, g, b, e, d, f]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ let a, b, c, d, e, f, g;
+ useAsyncEffect(async () => {
+ console.log(b, e, d, c, a, g, f);
+ }, [a, c, g]);
+ }
+ `,
+ // Alphabetize if it was alphabetized.
+ output: `
+ function MyComponent(props) {
+ let a, b, c, d, e, f, g;
+ useAsyncEffect(async () => {
+ console.log(b, e, d, c, a, g, f);
+ }, [a, b, c, d, e, f, g]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ let a, b, c, d, e, f, g;
+ useAsyncEffect(async () => {
+ console.log(b, e, d, c, a, g, f);
+ }, []);
+ }
+ `,
+ // Alphabetize if it was empty.
+ output: `
+ function MyComponent(props) {
+ let a, b, c, d, e, f, g;
+ useAsyncEffect(async () => {
+ console.log(b, e, d, c, a, g, f);
+ }, [a, b, c, d, e, f, g]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'a', 'b', 'c', 'd', 'e', 'f', and 'g'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ console.log(local);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ console.log(local);
+ }, [local, props.bar, props.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'local', 'props.bar', and 'props.foo'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ console.log(local);
+ }, [props]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ console.log(props.bar);
+ console.log(local);
+ }, [local, props]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, []);
+ useWorker(async () => {
+ console.log(props.foo);
+ }, []);
+ useEffectUpdate(() => {
+ console.log(props.foo);
+ }, []);
+ React.useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, []);
+ React.useWorker(async () => {
+ console.log(props.foo);
+ }, []);
+ React.useEffectUpdate(() => {
+ console.log(props.foo);
+ }, []);
+ React.notReactiveHook(() => {
+ console.log(props.foo);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ useWorker(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ useEffectUpdate(() => {
+ console.log(props.foo);
+ }, [props.foo]);
+ React.useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ React.useWorker(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ React.useEffectUpdate(() => {
+ console.log(props.foo);
+ }, [props.foo]);
+ React.notReactiveHook(() => {
+ console.log(props.foo);
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useWorker has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useEffectUpdate has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook React.useAsyncEffect has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook React.useWorker has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook React.useEffectUpdate has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, []);
+ useWorkerState(async () => {
+ console.log(props.foo);
+ }, []);
+ useEffectUpdate(() => {
+ console.log(props.foo);
+ }, []);
+ React.useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, []);
+ React.useWorkerState(async () => {
+ console.log(props.foo);
+ }, []);
+ React.useEffectUpdate(() => {
+ console.log(props.foo);
+ }, []);
+ React.notReactiveHook(() => {
+ console.log(props.foo);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ useWorkerState(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ useEffectUpdate(() => {
+ console.log(props.foo);
+ }, [props.foo]);
+ React.useAsyncEffect(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ React.useWorkerState(async () => {
+ console.log(props.foo);
+ }, [props.foo]);
+ React.useEffectUpdate(() => {
+ console.log(props.foo);
+ }, [props.foo]);
+ React.notReactiveHook(() => {
+ console.log(props.foo);
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useWorkerState has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useEffectUpdate has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook React.useAsyncEffect has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook React.useWorkerState has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook React.useEffectUpdate has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [a ? local : b]);
+ }
+ `,
+ // TODO: should we bail out instead?
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ 'React Hook useAsyncEffect has a complex expression in the dependency array. ' +
+ 'Extract it to a separate variable so it can be statically checked.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [a && local]);
+ }
+ `,
+ // TODO: should we bail out instead?
+ output: `
+ function MyComponent() {
+ const local = {};
+ useAsyncEffect(async () => {
+ console.log(local);
+ }, [local]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local'. " +
+ 'Either include it or remove the dependency array.',
+ 'React Hook useAsyncEffect has a complex expression in the dependency array. ' +
+ 'Extract it to a separate variable so it can be statically checked.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useRef();
+ const [state, setState] = useState();
+ useAsyncEffect(async () => {
+ ref.current = {};
+ setState(state + 1);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const ref = useRef();
+ const [state, setState] = useState();
+ useAsyncEffect(async () => {
+ ref.current = {};
+ setState(state + 1);
+ }, [state]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'state'. " +
+ 'Either include it or remove the dependency array. ' +
+ `You can also do a functional update 'setState(s => ...)' ` +
+ `if you only need 'state' in the 'setState' call.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useRef();
+ const [state, setState] = useState();
+ useAsyncEffect(async () => {
+ ref.current = {};
+ setState(state + 1);
+ }, [ref]);
+ }
+ `,
+ // We don't ask to remove static deps but don't add them either.
+ // Don't suggest removing "ref" (it's fine either way)
+ // but *do* add "state". *Don't* add "setState" ourselves.
+ output: `
+ function MyComponent() {
+ const ref = useRef();
+ const [state, setState] = useState();
+ useAsyncEffect(async () => {
+ ref.current = {};
+ setState(state + 1);
+ }, [ref, state]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'state'. " +
+ 'Either include it or remove the dependency array. ' +
+ `You can also do a functional update 'setState(s => ...)' ` +
+ `if you only need 'state' in the 'setState' call.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ useAsyncEffect(async () => {
+ ref1.current.focus();
+ console.log(ref2.current.textContent);
+ alert(props.someOtherRefs.current.innerHTML);
+ fetch(props.color);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ useAsyncEffect(async () => {
+ ref1.current.focus();
+ console.log(ref2.current.textContent);
+ alert(props.someOtherRefs.current.innerHTML);
+ fetch(props.color);
+ }, [props.color, props.someOtherRefs]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'props.color' and 'props.someOtherRefs'. " +
+ 'Either include them or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ useAsyncEffect(async () => {
+ ref1.current.focus();
+ console.log(ref2.current.textContent);
+ alert(props.someOtherRefs.current.innerHTML);
+ fetch(props.color);
+ }, [ref1.current, ref2.current, props.someOtherRefs, props.color]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ useAsyncEffect(async () => {
+ ref1.current.focus();
+ console.log(ref2.current.textContent);
+ alert(props.someOtherRefs.current.innerHTML);
+ fetch(props.color);
+ }, [props.someOtherRefs, props.color]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Mutable values like 'ref1.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useRef();
+ useAsyncEffect(async () => {
+ console.log(ref.current);
+ }, [ref.current]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const ref = useRef();
+ useAsyncEffect(async () => {
+ console.log(ref.current);
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has an unnecessary dependency: 'ref.current'. " +
+ 'Either exclude it or remove the dependency array. ' +
+ "Mutable values like 'ref.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ activeTab }) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ useAsyncEffect(async () => {
+ ref1.current.scrollTop = 0;
+ ref2.current.scrollTop = 0;
+ }, [ref1.current, ref2.current, activeTab]);
+ }
+ `,
+ output: `
+ function MyComponent({ activeTab }) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ useAsyncEffect(async () => {
+ ref1.current.scrollTop = 0;
+ ref2.current.scrollTop = 0;
+ }, [activeTab]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Mutable values like 'ref1.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ activeTab, initY }) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ const fn = useWorker(async () => {
+ ref1.current.scrollTop = initY;
+ ref2.current.scrollTop = initY;
+ }, [ref1.current, ref2.current, activeTab, initY]);
+ }
+ `,
+ output: `
+ function MyComponent({ activeTab, initY }) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ const fn = useWorker(async () => {
+ ref1.current.scrollTop = initY;
+ ref2.current.scrollTop = initY;
+ }, [initY]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Mutable values like 'ref1.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent({ activeTab, initY }) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ const fn = useWorkerState(async () => {
+ ref1.current.scrollTop = initY;
+ ref2.current.scrollTop = initY;
+ }, [ref1.current, ref2.current, activeTab, initY]);
+ }
+ `,
+ output: `
+ function MyComponent({ activeTab, initY }) {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ const fn = useWorkerState(async () => {
+ ref1.current.scrollTop = initY;
+ ref2.current.scrollTop = initY;
+ }, [initY]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Mutable values like 'ref1.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const ref = useRef();
+ useAsyncEffect(async () => {
+ console.log(ref.current);
+ }, [ref.current, ref]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const ref = useRef();
+ useAsyncEffect(async () => {
+ console.log(ref.current);
+ }, [ref]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has an unnecessary dependency: 'ref.current'. " +
+ 'Either exclude it or remove the dependency array. ' +
+ "Mutable values like 'ref.current' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ const MyComponent = forwardRef((props, ref) => {
+ useConditionalEffect(ref, () => ({
+ focus() {
+ alert(props.hello);
+ }
+ }), [])
+ });
+ `,
+ output: `
+ const MyComponent = forwardRef((props, ref) => {
+ useConditionalEffect(ref, () => ({
+ focus() {
+ alert(props.hello);
+ }
+ }), [props.hello])
+ });
+ `,
+ errors: [
+ "React Hook useConditionalEffect has a missing dependency: 'props.hello'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ if (props.onChange) {
+ props.onChange();
+ }
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ if (props.onChange) {
+ props.onChange();
+ }
+ }, [props]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props'. " +
+ 'Either include it or remove the dependency array. ' +
+ `However, 'props' will change when *any* prop changes, so the ` +
+ `preferred fix is to destructure the 'props' object outside ` +
+ `of the useAsyncEffect call and refer to those specific ` +
+ `props inside useAsyncEffect.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ function play() {
+ props.onPlay();
+ }
+ function pause() {
+ props.onPause();
+ }
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ function play() {
+ props.onPlay();
+ }
+ function pause() {
+ props.onPause();
+ }
+ }, [props]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props'. " +
+ 'Either include it or remove the dependency array. ' +
+ `However, 'props' will change when *any* prop changes, so the ` +
+ `preferred fix is to destructure the 'props' object outside ` +
+ `of the useAsyncEffect call and refer to those specific ` +
+ `props inside useAsyncEffect.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ if (props.foo.onChange) {
+ props.foo.onChange();
+ }
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ if (props.foo.onChange) {
+ props.foo.onChange();
+ }
+ }, [props.foo]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props.foo'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ props.onChange();
+ if (props.foo.onChange) {
+ props.foo.onChange();
+ }
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ props.onChange();
+ if (props.foo.onChange) {
+ props.foo.onChange();
+ }
+ }, [props]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props'. " +
+ 'Either include it or remove the dependency array. ' +
+ `However, 'props' will change when *any* prop changes, so the ` +
+ `preferred fix is to destructure the 'props' object outside ` +
+ `of the useAsyncEffect call and refer to those specific ` +
+ `props inside useAsyncEffect.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const [skillsCount] = useState();
+ useAsyncEffect(async () => {
+ if (skillsCount === 0 && !props.isEditMode) {
+ props.toggleEditMode();
+ }
+ }, [skillsCount, props.isEditMode, props.toggleEditMode]);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const [skillsCount] = useState();
+ useAsyncEffect(async () => {
+ if (skillsCount === 0 && !props.isEditMode) {
+ props.toggleEditMode();
+ }
+ }, [skillsCount, props.isEditMode, props.toggleEditMode, props]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props'. " +
+ 'Either include it or remove the dependency array. ' +
+ `However, 'props' will change when *any* prop changes, so the ` +
+ `preferred fix is to destructure the 'props' object outside ` +
+ `of the useAsyncEffect call and refer to those specific ` +
+ `props inside useAsyncEffect.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ const [skillsCount] = useState();
+ useAsyncEffect(async () => {
+ if (skillsCount === 0 && !props.isEditMode) {
+ props.toggleEditMode();
+ }
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ const [skillsCount] = useState();
+ useAsyncEffect(async () => {
+ if (skillsCount === 0 && !props.isEditMode) {
+ props.toggleEditMode();
+ }
+ }, [props, skillsCount]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'props' and 'skillsCount'. " +
+ 'Either include them or remove the dependency array. ' +
+ `However, 'props' will change when *any* prop changes, so the ` +
+ `preferred fix is to destructure the 'props' object outside ` +
+ `of the useAsyncEffect call and refer to those specific ` +
+ `props inside useAsyncEffect.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ externalCall(props);
+ props.onChange();
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ externalCall(props);
+ props.onChange();
+ }, [props]);
+ }
+ `,
+ // Don't suggest to destructure props here since you can't.
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ props.onChange();
+ externalCall(props);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ useAsyncEffect(async () => {
+ props.onChange();
+ externalCall(props);
+ }, [props]);
+ }
+ `,
+ // Don't suggest to destructure props here since you can't.
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'props'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ let value;
+ let value2;
+ let value3;
+ let value4;
+ let asyncValue;
+ useAsyncEffect(async () => {
+ if (value4) {
+ value = {};
+ }
+ value2 = 100;
+ value = 43;
+ value4 = true;
+ console.log(value2);
+ console.log(value3);
+ setTimeout(() => {
+ asyncValue = 100;
+ });
+ }, []);
+ }
+ `,
+ // This is a separate warning unrelated to others.
+ // We could've made a separate rule for it but it's rare enough to name it.
+ // No autofix suggestion because the intent isn't clear.
+ output: `
+ function MyComponent(props) {
+ let value;
+ let value2;
+ let value3;
+ let value4;
+ let asyncValue;
+ useAsyncEffect(async () => {
+ if (value4) {
+ value = {};
+ }
+ value2 = 100;
+ value = 43;
+ value4 = true;
+ console.log(value2);
+ console.log(value3);
+ setTimeout(() => {
+ asyncValue = 100;
+ });
+ }, []);
+ }
+ `,
+ errors: [
+ // value2
+ `Assignments to the 'value2' variable from inside React Hook useAsyncEffect ` +
+ `will be lost after each render. To preserve the value over time, ` +
+ `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` +
+ `Otherwise, you can move this variable directly inside useAsyncEffect.`, // value
+ `Assignments to the 'value' variable from inside React Hook useAsyncEffect ` +
+ `will be lost after each render. To preserve the value over time, ` +
+ `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` +
+ `Otherwise, you can move this variable directly inside useAsyncEffect.`, // value4
+ `Assignments to the 'value4' variable from inside React Hook useAsyncEffect ` +
+ `will be lost after each render. To preserve the value over time, ` +
+ `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` +
+ `Otherwise, you can move this variable directly inside useAsyncEffect.`, // asyncValue
+ `Assignments to the 'asyncValue' variable from inside React Hook useAsyncEffect ` +
+ `will be lost after each render. To preserve the value over time, ` +
+ `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` +
+ `Otherwise, you can move this variable directly inside useAsyncEffect.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent(props) {
+ let value;
+ let value2;
+ let value3;
+ let asyncValue;
+ useAsyncEffect(async () => {
+ value = {};
+ value2 = 100;
+ value = 43;
+ console.log(value2);
+ console.log(value3);
+ setTimeout(() => {
+ asyncValue = 100;
+ });
+ }, [value, value2, value3]);
+ }
+ `,
+ // This is a separate warning unrelated to others.
+ // We could've made a separate rule for it but it's rare enough to name it.
+ // No autofix suggestion because the intent isn't clear.
+ output: `
+ function MyComponent(props) {
+ let value;
+ let value2;
+ let value3;
+ let asyncValue;
+ useAsyncEffect(async () => {
+ value = {};
+ value2 = 100;
+ value = 43;
+ console.log(value2);
+ console.log(value3);
+ setTimeout(() => {
+ asyncValue = 100;
+ });
+ }, [value, value2, value3]);
+ }
+ `,
+ errors: [
+ // value
+ `Assignments to the 'value' variable from inside React Hook useAsyncEffect ` +
+ `will be lost after each render. To preserve the value over time, ` +
+ `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` +
+ `Otherwise, you can move this variable directly inside useAsyncEffect.`, // value2
+ `Assignments to the 'value2' variable from inside React Hook useAsyncEffect ` +
+ `will be lost after each render. To preserve the value over time, ` +
+ `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` +
+ `Otherwise, you can move this variable directly inside useAsyncEffect.`, // asyncValue
+ `Assignments to the 'asyncValue' variable from inside React Hook useAsyncEffect ` +
+ `will be lost after each render. To preserve the value over time, ` +
+ `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` +
+ `Otherwise, you can move this variable directly inside useAsyncEffect.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const myRef = useRef();
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ myRef.current.addEventListener('mousemove', handleMove);
+ return () => myRef.current.removeEventListener('mousemove', handleMove);
+ }, []);
+ return
;
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const myRef = useRef();
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ myRef.current.addEventListener('mousemove', handleMove);
+ return () => myRef.current.removeEventListener('mousemove', handleMove);
+ }, []);
+ return
;
+ }
+ `,
+ errors: [
+ `The ref value 'myRef.current' will likely have changed by the time ` +
+ `this effect cleanup function runs. If this ref points to a node ` +
+ `rendered by React, copy 'myRef.current' to a variable inside the effect, ` +
+ `and use that variable in the cleanup function.`,
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ const myRef = useRef();
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ myRef.current.addEventListener('mousemove', handleMove);
+ return () => myRef.current.removeEventListener('mousemove', handleMove);
+ });
+ return
;
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const myRef = useRef();
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ myRef.current.addEventListener('mousemove', handleMove);
+ return () => myRef.current.removeEventListener('mousemove', handleMove);
+ });
+ return
;
+ }
+ `,
+ errors: [
+ `The ref value 'myRef.current' will likely have changed by the time ` +
+ `this effect cleanup function runs. If this ref points to a node ` +
+ `rendered by React, copy 'myRef.current' to a variable inside the effect, ` +
+ `and use that variable in the cleanup function.`,
+ ],
+ },
+ {
+ code: `
+ function useMyThing(myRef) {
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ myRef.current.addEventListener('mousemove', handleMove);
+ return () => myRef.current.removeEventListener('mousemove', handleMove);
+ }, [myRef]);
+ }
+ `,
+ output: `
+ function useMyThing(myRef) {
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ myRef.current.addEventListener('mousemove', handleMove);
+ return () => myRef.current.removeEventListener('mousemove', handleMove);
+ }, [myRef]);
+ }
+ `,
+ errors: [
+ `The ref value 'myRef.current' will likely have changed by the time ` +
+ `this effect cleanup function runs. If this ref points to a node ` +
+ `rendered by React, copy 'myRef.current' to a variable inside the effect, ` +
+ `and use that variable in the cleanup function.`,
+ ],
+ },
+ {
+ code: `
+ function useMyThing(myRef) {
+ useAsyncEffect(async () => {
+ const handleMouse = () => {};
+ myRef.current.addEventListener('mousemove', handleMouse);
+ myRef.current.addEventListener('mousein', handleMouse);
+ return function() {
+ setTimeout(() => {
+ myRef.current.removeEventListener('mousemove', handleMouse);
+ myRef.current.removeEventListener('mousein', handleMouse);
+ });
+ }
+ }, [myRef]);
+ }
+ `,
+ output: `
+ function useMyThing(myRef) {
+ useAsyncEffect(async () => {
+ const handleMouse = () => {};
+ myRef.current.addEventListener('mousemove', handleMouse);
+ myRef.current.addEventListener('mousein', handleMouse);
+ return function() {
+ setTimeout(() => {
+ myRef.current.removeEventListener('mousemove', handleMouse);
+ myRef.current.removeEventListener('mousein', handleMouse);
+ });
+ }
+ }, [myRef]);
+ }
+ `,
+ errors: [
+ `The ref value 'myRef.current' will likely have changed by the time ` +
+ `this effect cleanup function runs. If this ref points to a node ` +
+ `rendered by React, copy 'myRef.current' to a variable inside the effect, ` +
+ `and use that variable in the cleanup function.`,
+ ],
+ },
+ {
+ code: `
+ function useMyThing(myRef, active) {
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ if (active) {
+ myRef.current.addEventListener('mousemove', handleMove);
+ return function() {
+ setTimeout(() => {
+ myRef.current.removeEventListener('mousemove', handleMove);
+ });
+ }
+ }
+ }, [myRef, active]);
+ }
+ `,
+ output: `
+ function useMyThing(myRef, active) {
+ useAsyncEffect(async () => {
+ const handleMove = () => {};
+ if (active) {
+ myRef.current.addEventListener('mousemove', handleMove);
+ return function() {
+ setTimeout(() => {
+ myRef.current.removeEventListener('mousemove', handleMove);
+ });
+ }
+ }
+ }, [myRef, active]);
+ }
+ `,
+ errors: [
+ `The ref value 'myRef.current' will likely have changed by the time ` +
+ `this effect cleanup function runs. If this ref points to a node ` +
+ `rendered by React, copy 'myRef.current' to a variable inside the effect, ` +
+ `and use that variable in the cleanup function.`,
+ ],
+ },
+ {
+ // Autofix ignores constant primitives (leaving the ones that are there).
+ code: `
+ function MyComponent() {
+ const local1 = 42;
+ const local2 = '42';
+ const local3 = null;
+ const local4 = {};
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ console.log(local3);
+ console.log(local4);
+ }, [local1, local3]);
+ }
+ `,
+ output: `
+ function MyComponent() {
+ const local1 = 42;
+ const local2 = '42';
+ const local3 = null;
+ const local4 = {};
+ useAsyncEffect(async () => {
+ console.log(local1);
+ console.log(local2);
+ console.log(local3);
+ console.log(local4);
+ }, [local1, local3, local4]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'local4'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function MyComponent() {
+ useAsyncEffect(async () => {
+ window.scrollTo(0, 0);
+ }, [window]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has an unnecessary dependency: 'window'. " +
+ 'Either exclude it or remove the dependency array. ' +
+ "Outer scope values like 'window' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ import MutableStore from 'store';
+
+ function MyComponent() {
+ useAsyncEffect(async () => {
+ console.log(MutableStore.hello);
+ }, [MutableStore.hello]);
+ }
+ `,
+ output: `
+ import MutableStore from 'store';
+
+ function MyComponent() {
+ useAsyncEffect(async () => {
+ console.log(MutableStore.hello);
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has an unnecessary dependency: 'MutableStore.hello'. " +
+ 'Either exclude it or remove the dependency array. ' +
+ "Outer scope values like 'MutableStore.hello' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ import MutableStore from 'store';
+ let z = {};
+
+ function MyComponent(props) {
+ let x = props.foo;
+ {
+ let y = props.bar;
+ useAsyncEffect(async () => {
+ console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff);
+ }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]);
+ }
+ }
+ `,
+ output: `
+ import MutableStore from 'store';
+ let z = {};
+
+ function MyComponent(props) {
+ let x = props.foo;
+ {
+ let y = props.bar;
+ useAsyncEffect(async () => {
+ console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff);
+ }, [props.foo, x, y]);
+ }
+ }
+ `,
+ errors: [
+ 'React Hook useAsyncEffect has unnecessary dependencies: ' +
+ "'MutableStore.hello.world', 'global.stuff', and 'z'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ import MutableStore from 'store';
+ let z = {};
+
+ function MyComponent(props) {
+ let x = props.foo;
+ {
+ let y = props.bar;
+ useAsyncEffect(async () => {
+ // nothing
+ }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]);
+ }
+ }
+ `,
+ // The output should contain the ones that are inside a component
+ // since there are legit reasons to over-specify them for effects.
+ output: `
+ import MutableStore from 'store';
+ let z = {};
+
+ function MyComponent(props) {
+ let x = props.foo;
+ {
+ let y = props.bar;
+ useAsyncEffect(async () => {
+ // nothing
+ }, [props.foo, x, y]);
+ }
+ }
+ `,
+ errors: [
+ 'React Hook useAsyncEffect has unnecessary dependencies: ' +
+ "'MutableStore.hello.world', 'global.stuff', and 'z'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ import MutableStore from 'store';
+ let z = {};
+
+ function MyComponent(props) {
+ let x = props.foo;
+ {
+ let y = props.bar;
+ const fn = useWorker(async () => {
+ // nothing
+ }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]);
+ }
+ }
+ `,
+ output: `
+ import MutableStore from 'store';
+ let z = {};
+
+ function MyComponent(props) {
+ let x = props.foo;
+ {
+ let y = props.bar;
+ const fn = useWorker(async () => {
+ // nothing
+ }, []);
+ }
+ }
+ `,
+ errors: [
+ 'React Hook useWorker has unnecessary dependencies: ' +
+ "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ code: `
+ import MutableStore from 'store';
+ let z = {};
+
+ function MyComponent(props) {
+ let x = props.foo;
+ {
+ let y = props.bar;
+ const fn = useWorkerState(async () => {
+ // nothing
+ }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]);
+ }
+ }
+ `,
+ output: `
+ import MutableStore from 'store';
+ let z = {};
+
+ function MyComponent(props) {
+ let x = props.foo;
+ {
+ let y = props.bar;
+ const fn = useWorkerState(async () => {
+ // nothing
+ }, []);
+ }
+ }
+ `,
+ errors: [
+ 'React Hook useWorkerState has unnecessary dependencies: ' +
+ "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " +
+ 'Either exclude them or remove the dependency array. ' +
+ "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " +
+ "because mutating them doesn't re-render the component.",
+ ],
+ },
+ {
+ // Every almost-static function is tainted by a dynamic value.
+ code: `
+ function MyComponent(props) {
+ let [, setState] = useState();
+ let [, dispatch] = React.useReducer();
+ let taint = props.foo;
+
+ function handleNext1(value) {
+ let value2 = value * taint;
+ setState(value2);
+ console.log('hello');
+ }
+ const handleNext2 = (value) => {
+ setState(taint(value));
+ console.log('hello');
+ };
+ let handleNext3 = function(value) {
+ setTimeout(() => console.log(taint));
+ dispatch({ type: 'x', value });
+ };
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext1);
+ }, []);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext2);
+ }, []);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext3);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ let [, setState] = useState();
+ let [, dispatch] = React.useReducer();
+ let taint = props.foo;
+
+ function handleNext1(value) {
+ let value2 = value * taint;
+ setState(value2);
+ console.log('hello');
+ }
+ const handleNext2 = (value) => {
+ setState(taint(value));
+ console.log('hello');
+ };
+ let handleNext3 = function(value) {
+ setTimeout(() => console.log(taint));
+ dispatch({ type: 'x', value });
+ };
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext1);
+ }, [handleNext1]);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext2);
+ }, [handleNext2]);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext3);
+ }, [handleNext3]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'handleNext1'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useAsyncLayoutEffect has a missing dependency: 'handleNext2'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useEffectUpdate has a missing dependency: 'handleNext3'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Regression test
+ code: `
+ function MyComponent(props) {
+ let [, setState] = useState();
+ let [, dispatch] = React.useReducer();
+ let taint = props.foo;
+
+ // Shouldn't affect anything
+ function handleChange() {}
+
+ function handleNext1(value) {
+ let value2 = value * taint;
+ setState(value2);
+ console.log('hello');
+ }
+ const handleNext2 = (value) => {
+ setState(taint(value));
+ console.log('hello');
+ };
+ let handleNext3 = function(value) {
+ console.log(taint);
+ dispatch({ type: 'x', value });
+ };
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext1);
+ }, []);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext2);
+ }, []);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext3);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ let [, setState] = useState();
+ let [, dispatch] = React.useReducer();
+ let taint = props.foo;
+
+ // Shouldn't affect anything
+ function handleChange() {}
+
+ function handleNext1(value) {
+ let value2 = value * taint;
+ setState(value2);
+ console.log('hello');
+ }
+ const handleNext2 = (value) => {
+ setState(taint(value));
+ console.log('hello');
+ };
+ let handleNext3 = function(value) {
+ console.log(taint);
+ dispatch({ type: 'x', value });
+ };
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext1);
+ }, [handleNext1]);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext2);
+ }, [handleNext2]);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext3);
+ }, [handleNext3]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'handleNext1'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useAsyncLayoutEffect has a missing dependency: 'handleNext2'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useEffectUpdate has a missing dependency: 'handleNext3'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Regression test
+ code: `
+ function MyComponent(props) {
+ let [, setState] = useState();
+ let [, dispatch] = React.useReducer();
+ let taint = props.foo;
+
+ // Shouldn't affect anything
+ const handleChange = () => {};
+
+ function handleNext1(value) {
+ let value2 = value * taint;
+ setState(value2);
+ console.log('hello');
+ }
+ const handleNext2 = (value) => {
+ setState(taint(value));
+ console.log('hello');
+ };
+ let handleNext3 = function(value) {
+ console.log(taint);
+ dispatch({ type: 'x', value });
+ };
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext1);
+ }, []);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext2);
+ }, []);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext3);
+ }, []);
+ }
+ `,
+ output: `
+ function MyComponent(props) {
+ let [, setState] = useState();
+ let [, dispatch] = React.useReducer();
+ let taint = props.foo;
+
+ // Shouldn't affect anything
+ const handleChange = () => {};
+
+ function handleNext1(value) {
+ let value2 = value * taint;
+ setState(value2);
+ console.log('hello');
+ }
+ const handleNext2 = (value) => {
+ setState(taint(value));
+ console.log('hello');
+ };
+ let handleNext3 = function(value) {
+ console.log(taint);
+ dispatch({ type: 'x', value });
+ };
+ useAsyncEffect(async () => {
+ return Store.subscribe(handleNext1);
+ }, [handleNext1]);
+ useAsyncLayoutEffect(async () => {
+ return Store.subscribe(handleNext2);
+ }, [handleNext2]);
+ useEffectUpdate(() => {
+ return Store.subscribe(handleNext3);
+ }, [handleNext3]);
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'handleNext1'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useAsyncLayoutEffect has a missing dependency: 'handleNext2'. " +
+ 'Either include it or remove the dependency array.',
+ "React Hook useEffectUpdate has a missing dependency: 'handleNext3'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count + 1);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ output: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count + 1);
+ }, 1000);
+ return () => clearInterval(id);
+ }, [count]);
+
+ return {count} ;
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'count'. " +
+ 'Either include it or remove the dependency array. ' +
+ `You can also do a functional update 'setCount(c => ...)' if you ` +
+ `only need 'count' in the 'setCount' call.`,
+ ],
+ },
+ {
+ code: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+ let [increment, setIncrement] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ output: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+ let [increment, setIncrement] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, [count, increment]);
+
+ return {count} ;
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has missing dependencies: 'count' and 'increment'. " +
+ 'Either include them or remove the dependency array. ' +
+ `You can also do a functional update 'setCount(c => ...)' if you ` +
+ `only need 'count' in the 'setCount' call.`,
+ ],
+ },
+ {
+ code: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+ let [increment, setIncrement] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ output: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+ let [increment, setIncrement] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, [increment]);
+
+ return {count} ;
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'increment'. " +
+ 'Either include it or remove the dependency array. ' +
+ `You can also replace multiple useState variables with useReducer ` +
+ `if 'setCount' needs the current value of 'increment'.`,
+ ],
+ },
+ {
+ code: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+ let increment = useCustomHook();
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ output: `
+ function Counter() {
+ let [count, setCount] = useState(0);
+ let increment = useCustomHook();
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, [increment]);
+
+ return {count} ;
+ }
+ `,
+ // This intentionally doesn't show the reducer message
+ // because we don't know if it's safe for it to close over a value.
+ // We only show it for state variables (and possibly props).
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'increment'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function Counter({ step }) {
+ let [count, setCount] = useState(0);
+
+ function increment(x) {
+ return x + step;
+ }
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => increment(count));
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ output: `
+ function Counter({ step }) {
+ let [count, setCount] = useState(0);
+
+ function increment(x) {
+ return x + step;
+ }
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => increment(count));
+ }, 1000);
+ return () => clearInterval(id);
+ }, [increment]);
+
+ return {count} ;
+ }
+ `,
+ // This intentionally doesn't show the reducer message
+ // because we don't know if it's safe for it to close over a value.
+ // We only show it for state variables (and possibly props).
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'increment'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function Counter({ increment }) {
+ let [count, setCount] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ output: `
+ function Counter({ increment }) {
+ let [count, setCount] = useState(0);
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ setCount(count => count + increment);
+ }, 1000);
+ return () => clearInterval(id);
+ }, [increment]);
+
+ return {count} ;
+ }
+ `,
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'increment'. " +
+ 'Either include it or remove the dependency array. ' +
+ `If 'setCount' needs the current value of 'increment', ` +
+ `you can also switch to useReducer instead of useState and read 'increment' in the reducer.`,
+ ],
+ },
+ {
+ code: `
+ function Counter() {
+ const [count, setCount] = useState(0);
+
+ function tick() {
+ setCount(count + 1);
+ }
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ tick();
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+
+ return {count} ;
+ }
+ `,
+ output: `
+ function Counter() {
+ const [count, setCount] = useState(0);
+
+ function tick() {
+ setCount(count + 1);
+ }
+
+ useAsyncEffect(async () => {
+ let id = setInterval(() => {
+ tick();
+ }, 1000);
+ return () => clearInterval(id);
+ }, [tick]);
+
+ return {count} ;
+ }
+ `,
+ // TODO: ideally this should suggest useState updater form
+ // since this code doesn't actually work. The autofix could
+ // at least avoid suggesting 'tick' since it's obviously
+ // always different, and thus useless.
+ errors: [
+ "React Hook useAsyncEffect has a missing dependency: 'tick'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ // Regression test for a crash
+ code: `
+ function Podcasts() {
+ useAsyncEffect(async () => {
+ alert(podcasts);
+ }, []);
+ let [podcasts, setPodcasts] = useState(null);
+ }
+ `,
+ // Note: this autofix is shady because
+ // the variable is used before declaration.
+ // TODO: Maybe we can catch those fixes and not autofix.
+ output: `
+ function Podcasts() {
+ useAsyncEffect(async () => {
+ alert(podcasts);
+ }, [podcasts]);
+ let [podcasts, setPodcasts] = useState(null);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect has a missing dependency: 'podcasts'. ` +
+ `Either include it or remove the dependency array.`,
+ ],
+ },
+ {
+ code: `
+ function Podcasts({ fetchPodcasts, id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ fetchPodcasts(id).then(setPodcasts);
+ }, [id]);
+ }
+ `,
+ output: `
+ function Podcasts({ fetchPodcasts, id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ fetchPodcasts(id).then(setPodcasts);
+ }, [fetchPodcasts, id]);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect has a missing dependency: 'fetchPodcasts'. ` +
+ `Either include it or remove the dependency array. ` +
+ `If 'fetchPodcasts' changes too often, ` +
+ `find the parent component that defines it and wrap that definition in useWorker.`,
+ ],
+ },
+ {
+ code: `
+ function Podcasts({ api: { fetchPodcasts }, id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ fetchPodcasts(id).then(setPodcasts);
+ }, [id]);
+ }
+ `,
+ output: `
+ function Podcasts({ api: { fetchPodcasts }, id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ fetchPodcasts(id).then(setPodcasts);
+ }, [fetchPodcasts, id]);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect has a missing dependency: 'fetchPodcasts'. ` +
+ `Either include it or remove the dependency array. ` +
+ `If 'fetchPodcasts' changes too often, ` +
+ `find the parent component that defines it and wrap that definition in useWorker.`,
+ ],
+ },
+ {
+ code: `
+ function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ setTimeout(() => {
+ console.log(id);
+ fetchPodcasts(id).then(setPodcasts);
+ fetchPodcasts2(id).then(setPodcasts);
+ });
+ }, [id]);
+ }
+ `,
+ output: `
+ function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ setTimeout(() => {
+ console.log(id);
+ fetchPodcasts(id).then(setPodcasts);
+ fetchPodcasts2(id).then(setPodcasts);
+ });
+ }, [fetchPodcasts, fetchPodcasts2, id]);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect has missing dependencies: 'fetchPodcasts' and 'fetchPodcasts2'. ` +
+ `Either include them or remove the dependency array. ` +
+ `If 'fetchPodcasts' changes too often, ` +
+ `find the parent component that defines it and wrap that definition in useWorker.`,
+ ],
+ },
+ {
+ code: `
+ function Podcasts({ fetchPodcasts, id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ console.log(fetchPodcasts);
+ fetchPodcasts(id).then(setPodcasts);
+ }, [id]);
+ }
+ `,
+ output: `
+ function Podcasts({ fetchPodcasts, id }) {
+ let [podcasts, setPodcasts] = useState(null);
+ useAsyncEffect(async () => {
+ console.log(fetchPodcasts);
+ fetchPodcasts(id).then(setPodcasts);
+ }, [fetchPodcasts, id]);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect has a missing dependency: 'fetchPodcasts'. ` +
+ `Either include it or remove the dependency array. ` +
+ `If 'fetchPodcasts' changes too often, ` +
+ `find the parent component that defines it and wrap that definition in useWorker.`,
+ ],
+ },
+ {
+ // The mistake here is that it was moved inside the effect
+ // so it can't be referenced in the deps array.
+ code: `
+ function Thing() {
+ useAsyncEffect(async () => {
+ const fetchData = async () => {};
+ fetchData();
+ }, [fetchData]);
+ }
+ `,
+ output: `
+ function Thing() {
+ useAsyncEffect(async () => {
+ const fetchData = async () => {};
+ fetchData();
+ }, []);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect has an unnecessary dependency: 'fetchData'. ` +
+ `Either exclude it or remove the dependency array.`,
+ ],
+ },
+ {
+ code: `
+ function Hello() {
+ const [state, setState] = useState(0);
+ useAsyncEffect(async () => {
+ setState({});
+ });
+ }
+ `,
+ output: `
+ function Hello() {
+ const [state, setState] = useState(0);
+ useAsyncEffect(async () => {
+ setState({});
+ }, []);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect contains a call to 'setState'. ` +
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
+ `To fix this, pass [] as a second argument to the useAsyncEffect Hook.`,
+ ],
+ },
+ {
+ code: `
+ function Hello() {
+ const [data, setData] = useState(0);
+ useAsyncEffect(async () => {
+ fetchData.then(setData);
+ });
+ }
+ `,
+ output: `
+ function Hello() {
+ const [data, setData] = useState(0);
+ useAsyncEffect(async () => {
+ fetchData.then(setData);
+ }, []);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect contains a call to 'setData'. ` +
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
+ `To fix this, pass [] as a second argument to the useAsyncEffect Hook.`,
+ ],
+ },
+ {
+ code: `
+ function Hello({ country }) {
+ const [data, setData] = useState(0);
+ useAsyncEffect(async () => {
+ fetchData(country).then(setData);
+ });
+ }
+ `,
+ output: `
+ function Hello({ country }) {
+ const [data, setData] = useState(0);
+ useAsyncEffect(async () => {
+ fetchData(country).then(setData);
+ }, [country]);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect contains a call to 'setData'. ` +
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
+ `To fix this, pass [country] as a second argument to the useAsyncEffect Hook.`,
+ ],
+ },
+ {
+ code: `
+ function Hello({ prop1, prop2 }) {
+ const [state, setState] = useState(0);
+ useAsyncEffect(async () => {
+ if (prop1) {
+ setState(prop2);
+ }
+ });
+ }
+ `,
+ output: `
+ function Hello({ prop1, prop2 }) {
+ const [state, setState] = useState(0);
+ useAsyncEffect(async () => {
+ if (prop1) {
+ setState(prop2);
+ }
+ }, [prop1, prop2]);
+ }
+ `,
+ errors: [
+ `React Hook useAsyncEffect contains a call to 'setState'. ` +
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
+ `To fix this, pass [prop1, prop2] as a second argument to the useAsyncEffect Hook.`,
+ ],
+ },
+ {
+ code: `
+ function Example() {
+ const foo = useWorker(async () => {
+ foo();
+ }, [foo]);
+ }
+ `,
+ output: `
+ function Example() {
+ const foo = useWorker(async () => {
+ foo();
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has an unnecessary dependency: 'foo'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function Example({ prop }) {
+ const foo = useWorker(async () => {
+ prop.hello(foo);
+ }, [foo]);
+ const bar = useWorker(async () => {
+ foo();
+ }, [foo]);
+ }
+ `,
+ output: `
+ function Example({ prop }) {
+ const foo = useWorker(async () => {
+ prop.hello(foo);
+ }, [prop]);
+ const bar = useWorker(async () => {
+ foo();
+ }, [foo]);
+ }
+ `,
+ errors: [
+ "React Hook useWorker has a missing dependency: 'prop'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function Example() {
+ const foo = useWorkerState(async () => {
+ foo();
+ }, [foo]);
+ }
+ `,
+ output: `
+ function Example() {
+ const foo = useWorkerState(async () => {
+ foo();
+ }, []);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has an unnecessary dependency: 'foo'. " +
+ 'Either exclude it or remove the dependency array.',
+ ],
+ },
+ {
+ code: `
+ function Example({ prop }) {
+ const foo = useWorkerState(async () => {
+ prop.hello(foo);
+ }, [foo]);
+ const bar = useWorkerState(async () => {
+ foo();
+ }, [foo]);
+ }
+ `,
+ output: `
+ function Example({ prop }) {
+ const foo = useWorkerState(async () => {
+ prop.hello(foo);
+ }, [prop]);
+ const bar = useWorkerState(async () => {
+ foo();
+ }, [foo]);
+ }
+ `,
+ errors: [
+ "React Hook useWorkerState has a missing dependency: 'prop'. " +
+ 'Either include it or remove the dependency array.',
+ ],
+ },
+ ],
+}; // For easier local testing
+
+if (!process.env.CI) {
+ let only = [];
+ let skipped = [];
+ [...tests.valid, ...tests.invalid].forEach(t => {
+ if (t.skip) {
+ delete t.skip;
+ skipped.push(t);
+ }
+
+ if (t.only) {
+ delete t.only;
+ only.push(t);
+ } // if (!t.options) {
+ // t.options = [{ additionalHooks: 'useAsyncLayoutEffect' }];
+ // }
+ });
+
+ const predicate = t => {
+ if (only.length > 0) {
+ return only.indexOf(t) !== -1;
+ }
+
+ if (skipped.length > 0) {
+ return skipped.indexOf(t) === -1;
+ }
+
+ return true;
+ };
+
+ tests.valid = tests.valid.filter(predicate);
+ tests.invalid = tests.invalid.filter(predicate);
+}
+
+const eslintTester = new ESLintTester();
+eslintTester.run(
+ '@react-hook-utilities/eslint-plugin',
+ ReactHooksESLintRule,
+ tests,
+);
diff --git a/eslint-plugin/lib/index.js b/eslint-plugin/lib/index.js
new file mode 100644
index 0000000..196d98c
--- /dev/null
+++ b/eslint-plugin/lib/index.js
@@ -0,0 +1,10 @@
+/**
+ * @fileoverview ESLint rules for hook react-hook-utilities
+ * @author Flávio Caetano
+ */
+'use strict';
+
+const ExhaustiveDeps = require('./rules/ExaustiveDeps');
+exports.rules = {
+ 'exhaustive-deps': ExhaustiveDeps,
+};
diff --git a/eslint-plugin/lib/rules/ExaustiveDeps.js b/eslint-plugin/lib/rules/ExaustiveDeps.js
new file mode 100644
index 0000000..97036db
--- /dev/null
+++ b/eslint-plugin/lib/rules/ExaustiveDeps.js
@@ -0,0 +1,1327 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * Original source: https://github.com/facebook/react/blob/9e64bf18e11828d6b4c0363bff5ed2eca1ccd838/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
+ */
+
+'use strict';
+
+module.exports = {
+ meta: {
+ fixable: 'code',
+ schema: [
+ {
+ type: 'object',
+ additionalProperties: false,
+ },
+ ],
+ },
+
+ create(context) {
+ // Should be shared between visitors.
+ let setStateCallSites = new WeakMap();
+ let stateVariables = new WeakSet();
+ let staticKnownValueCache = new WeakMap();
+ let functionWithoutCapturedValueCache = new WeakMap();
+
+ function memoizeWithWeakMap(fn, map) {
+ return function(arg) {
+ if (map.has(arg)) {
+ // to verify cache hits:
+ // console.log(arg.name)
+ return map.get(arg);
+ }
+
+ const result = fn(arg);
+ map.set(arg, result);
+ return result;
+ };
+ }
+
+ return {
+ FunctionExpression: visitFunctionExpression,
+ ArrowFunctionExpression: visitFunctionExpression,
+ };
+ /**
+ * Visitor for both function expressions and arrow function expressions.
+ */
+
+ function visitFunctionExpression(node) {
+ // We only want to lint nodes which are reactive hook callbacks.
+ if (
+ (node.type !== 'FunctionExpression' &&
+ node.type !== 'ArrowFunctionExpression') ||
+ node.parent.type !== 'CallExpression'
+ ) {
+ return;
+ }
+
+ const callbackIndex = getReactiveHookCallbackIndex(node.parent.callee);
+
+ if (node.parent.arguments[callbackIndex] !== node) {
+ return;
+ } // Get the reactive hook node.
+
+ const reactiveHook = node.parent.callee;
+ const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
+ const isEffect = reactiveHookName.endsWith('Effect'); // Get the declared dependencies for this reactive hook. If there is no
+ // second argument then the reactive callback will re-run on every render.
+ // So no need to check for dependency inclusion.
+
+ const depsIndex = callbackIndex + 1;
+ const declaredDependenciesNode = node.parent.arguments[depsIndex]; // Get the current scope.
+
+ const scope = context.getScope(); // Find all our "pure scopes". On every re-render of a component these
+ // pure scopes may have changes to the variables declared within. So all
+ // variables used in our reactive hook callback but declared in a pure
+ // scope need to be listed as dependencies of our reactive hook callback.
+ //
+ // According to the rules of React you can't read a mutable value in pure
+ // scope. We can't enforce this in a lint so we trust that all variables
+ // declared outside of pure scope are indeed frozen.
+
+ const pureScopes = new Set();
+ let componentScope = null;
+ {
+ let currentScope = scope.upper;
+
+ while (currentScope) {
+ pureScopes.add(currentScope);
+
+ if (currentScope.type === 'function') {
+ break;
+ }
+
+ currentScope = currentScope.upper;
+ } // If there is no parent function scope then there are no pure scopes.
+ // The ones we've collected so far are incorrect. So don't continue with
+ // the lint.
+
+ if (!currentScope) {
+ return;
+ }
+
+ componentScope = currentScope;
+ } // Next we'll define a few helpers that helps us
+ // tell if some values don't have to be declared as deps.
+ // Some are known to be static based on Hook calls.
+ // const [state, setState] = useState() / React.useState()
+ // ^^^ true for this reference
+ // const [state, dispatch] = useReducer() / React.useReducer()
+ // ^^^ true for this reference
+ // const ref = useRef()
+ // ^^^ true for this reference
+ // False for everything else.
+
+ function isStaticKnownHookValue(resolved) {
+ if (!Array.isArray(resolved.defs)) {
+ return false;
+ }
+
+ const def = resolved.defs[0];
+
+ if (def == null) {
+ return false;
+ } // Look for `let stuff = ...`
+
+ if (def.node.type !== 'VariableDeclarator') {
+ return false;
+ }
+
+ const init = def.node.init;
+
+ if (init == null) {
+ return false;
+ } // Detect primitive constants
+ // const foo = 42
+
+ let declaration = def.node.parent;
+
+ if (declaration == null) {
+ // This might happen if variable is declared after the callback.
+ // In that case ESLint won't set up .parent refs.
+ // So we'll set them up manually.
+ fastFindReferenceWithParent(componentScope.block, def.node.id);
+ declaration = def.node.parent;
+
+ if (declaration == null) {
+ return false;
+ }
+ }
+
+ if (
+ declaration.kind === 'const' &&
+ init.type === 'Literal' &&
+ (typeof init.value === 'string' ||
+ typeof init.value === 'number' ||
+ init.value === null)
+ ) {
+ // Definitely static
+ return true;
+ } // Detect known Hook calls
+ // const [_, setState] = useState()
+
+ if (init.type !== 'CallExpression') {
+ return false;
+ }
+
+ let callee = init.callee; // Step into `= React.something` initializer.
+
+ if (
+ callee.type === 'MemberExpression' &&
+ callee.object.name === 'React' &&
+ callee.property != null &&
+ !callee.computed
+ ) {
+ callee = callee.property;
+ }
+
+ if (callee.type !== 'Identifier') {
+ return false;
+ }
+
+ const id = def.node.id;
+ const { name } = callee;
+
+ if (
+ (name === 'useRef' || name === 'useLazyRef') &&
+ id.type === 'Identifier'
+ ) {
+ // useRef() return value is static.
+ return true;
+ } else if (name === 'useState' || name === 'useReducer') {
+ // Only consider second value in initializing tuple static.
+ if (
+ id.type === 'ArrayPattern' &&
+ id.elements.length === 2 &&
+ Array.isArray(resolved.identifiers)
+ ) {
+ // Is second tuple value the same reference we're checking?
+ if (id.elements[1] === resolved.identifiers[0]) {
+ if (name === 'useState') {
+ const references = resolved.references;
+
+ for (let i = 0; i < references.length; i++) {
+ setStateCallSites.set(
+ references[i].identifier,
+ id.elements[0],
+ );
+ }
+ } // Setter is static.
+
+ return true;
+ } else if (id.elements[0] === resolved.identifiers[0]) {
+ if (name === 'useState') {
+ const references = resolved.references;
+
+ for (let i = 0; i < references.length; i++) {
+ stateVariables.add(references[i].identifier);
+ }
+ } // State variable itself is dynamic.
+
+ return false;
+ }
+ }
+ } // By default assume it's dynamic.
+
+ return false;
+ } // Some are just functions that don't reference anything dynamic.
+
+ function isFunctionWithoutCapturedValues(resolved) {
+ if (!Array.isArray(resolved.defs)) {
+ return false;
+ }
+
+ const def = resolved.defs[0];
+
+ if (def == null) {
+ return false;
+ }
+
+ if (def.node == null || def.node.id == null) {
+ return false;
+ } // Search the direct component subscopes for
+ // top-level function definitions matching this reference.
+
+ const fnNode = def.node;
+ let childScopes = componentScope.childScopes;
+ let fnScope = null;
+ let i;
+
+ for (i = 0; i < childScopes.length; i++) {
+ let childScope = childScopes[i];
+ let childScopeBlock = childScope.block;
+
+ if (
+ // function handleChange() {}
+ (fnNode.type === 'FunctionDeclaration' &&
+ childScopeBlock === fnNode) || // const handleChange = () => {}
+ // const handleChange = function() {}
+ (fnNode.type === 'VariableDeclarator' &&
+ childScopeBlock.parent === fnNode)
+ ) {
+ // Found it!
+ fnScope = childScope;
+ break;
+ }
+ }
+
+ if (fnScope == null) {
+ return false;
+ } // Does this function capture any values
+ // that are in pure scopes (aka render)?
+
+ for (i = 0; i < fnScope.through.length; i++) {
+ const ref = fnScope.through[i];
+
+ if (ref.resolved == null) {
+ continue;
+ }
+
+ if (
+ pureScopes.has(ref.resolved.scope) && // Static values are fine though,
+ // although we won't check functions deeper.
+ !memoizedIsStaticKnownHookValue(ref.resolved)
+ ) {
+ return false;
+ }
+ } // If we got here, this function doesn't capture anything
+ // from render--or everything it captures is known static.
+
+ return true;
+ } // Remember such values. Avoid re-running extra checks on them.
+
+ const memoizedIsStaticKnownHookValue = memoizeWithWeakMap(
+ isStaticKnownHookValue,
+ staticKnownValueCache,
+ );
+ const memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(
+ isFunctionWithoutCapturedValues,
+ functionWithoutCapturedValueCache,
+ ); // These are usually mistaken. Collect them.
+
+ const currentRefsInEffectCleanup = new Map(); // Is this reference inside a cleanup function for this effect node?
+ // We can check by traversing scopes upwards from the reference, and checking
+ // if the last "return () => " we encounter is located directly inside the effect.
+
+ function isInsideEffectCleanup(reference) {
+ let curScope = reference.from;
+ let isInReturnedFunction = false;
+
+ while (curScope.block !== node) {
+ if (curScope.type === 'function') {
+ isInReturnedFunction =
+ curScope.block.parent != null &&
+ curScope.block.parent.type === 'ReturnStatement';
+ }
+
+ curScope = curScope.upper;
+ }
+
+ return isInReturnedFunction;
+ } // Get dependencies from all our resolved references in pure scopes.
+ // Key is dependency string, value is whether it's static.
+
+ const dependencies = new Map();
+ gatherDependenciesRecursively(scope);
+
+ function gatherDependenciesRecursively(currentScope) {
+ for (const reference of currentScope.references) {
+ // If this reference is not resolved or it is not declared in a pure
+ // scope then we don't care about this reference.
+ if (!reference.resolved) {
+ continue;
+ }
+
+ if (!pureScopes.has(reference.resolved.scope)) {
+ continue;
+ } // Narrow the scope of a dependency if it is, say, a member expression.
+ // Then normalize the narrowed dependency.
+
+ const referenceNode = fastFindReferenceWithParent(
+ node,
+ reference.identifier,
+ );
+ const dependencyNode = getDependency(referenceNode);
+ const dependency = toPropertyAccessString(dependencyNode); // Accessing ref.current inside effect cleanup is bad.
+
+ if (
+ // We're in an effect...
+ isEffect && // ... and this look like accessing .current...
+ dependencyNode.type === 'Identifier' &&
+ dependencyNode.parent.type === 'MemberExpression' &&
+ !dependencyNode.parent.computed &&
+ dependencyNode.parent.property.type === 'Identifier' &&
+ dependencyNode.parent.property.name === 'current' && // ...in a cleanup function or below...
+ isInsideEffectCleanup(reference)
+ ) {
+ currentRefsInEffectCleanup.set(dependency, {
+ reference,
+ dependencyNode,
+ });
+ }
+
+ const def = reference.resolved.defs[0];
+
+ if (def == null) {
+ continue;
+ } // Ignore references to the function itself as it's not defined yet.
+
+ if (def.node != null && def.node.init === node.parent) {
+ continue;
+ } // Ignore Flow type parameters
+
+ if (def.type === 'TypeParameter') {
+ continue;
+ } // Add the dependency to a map so we can make sure it is referenced
+ // again in our dependencies array. Remember whether it's static.
+
+ if (!dependencies.has(dependency)) {
+ const resolved = reference.resolved;
+ const isStatic =
+ memoizedIsStaticKnownHookValue(resolved) ||
+ memoizedIsFunctionWithoutCapturedValues(resolved);
+ dependencies.set(dependency, {
+ isStatic,
+ references: [reference],
+ });
+ } else {
+ dependencies.get(dependency).references.push(reference);
+ }
+ }
+
+ for (const childScope of currentScope.childScopes) {
+ gatherDependenciesRecursively(childScope);
+ }
+ } // Warn about accessing .current in cleanup effects.
+
+ currentRefsInEffectCleanup.forEach(
+ ({ reference, dependencyNode }, dependency) => {
+ const references = reference.resolved.references; // Is React managing this ref or us?
+ // Let's see if we can find a .current assignment.
+
+ let foundCurrentAssignment = false;
+
+ for (let i = 0; i < references.length; i++) {
+ const { identifier } = references[i];
+ const { parent } = identifier;
+
+ if (
+ parent != null && // ref.current
+ parent.type === 'MemberExpression' &&
+ !parent.computed &&
+ parent.property.type === 'Identifier' &&
+ parent.property.name === 'current' && // ref.current =
+ parent.parent.type === 'AssignmentExpression' &&
+ parent.parent.left === parent
+ ) {
+ foundCurrentAssignment = true;
+ break;
+ }
+ } // We only want to warn about React-managed refs.
+
+ if (foundCurrentAssignment) {
+ return;
+ }
+
+ context.report({
+ node: dependencyNode.parent.property,
+ message:
+ `The ref value '${dependency}.current' will likely have ` +
+ `changed by the time this effect cleanup function runs. If ` +
+ `this ref points to a node rendered by React, copy ` +
+ `'${dependency}.current' to a variable inside the effect, and ` +
+ `use that variable in the cleanup function.`,
+ });
+ },
+ ); // Warn about assigning to variables in the outer scope.
+ // Those are usually bugs.
+
+ let staleAssignments = new Set();
+
+ function reportStaleAssignment(writeExpr, key) {
+ if (staleAssignments.has(key)) {
+ return;
+ }
+
+ staleAssignments.add(key);
+ context.report({
+ node: writeExpr,
+ message:
+ `Assignments to the '${key}' variable from inside React Hook ` +
+ `${context.getSource(reactiveHook)} will be lost after each ` +
+ `render. To preserve the value over time, store it in a useRef or useLazyRef ` +
+ `Hook and keep the mutable value in the '.current' property. ` +
+ `Otherwise, you can move this variable directly inside ` +
+ `${context.getSource(reactiveHook)}.`,
+ });
+ } // Remember which deps are optional and report bad usage first.
+
+ const optionalDependencies = new Set();
+ dependencies.forEach(({ isStatic, references }, key) => {
+ if (isStatic) {
+ optionalDependencies.add(key);
+ }
+
+ references.forEach(reference => {
+ if (reference.writeExpr) {
+ reportStaleAssignment(reference.writeExpr, key);
+ }
+ });
+ });
+
+ if (staleAssignments.size > 0) {
+ // The intent isn't clear so we'll wait until you fix those first.
+ return;
+ }
+
+ if (!declaredDependenciesNode) {
+ // Check if there are any top-level setState() calls.
+ // Those tend to lead to infinite loops.
+ let setStateInsideEffectWithoutDeps = null;
+ dependencies.forEach(({ isStatic, references }, key) => {
+ if (setStateInsideEffectWithoutDeps) {
+ return;
+ }
+
+ references.forEach(reference => {
+ if (setStateInsideEffectWithoutDeps) {
+ return;
+ }
+
+ const id = reference.identifier;
+ const isSetState = setStateCallSites.has(id);
+
+ if (!isSetState) {
+ return;
+ }
+
+ let fnScope = reference.from;
+
+ while (fnScope.type !== 'function') {
+ fnScope = fnScope.upper;
+ }
+
+ const isDirectlyInsideEffect = fnScope.block === node;
+
+ if (isDirectlyInsideEffect) {
+ // TODO: we could potentially ignore early returns.
+ setStateInsideEffectWithoutDeps = key;
+ }
+ });
+ });
+
+ if (setStateInsideEffectWithoutDeps) {
+ let { suggestedDependencies } = collectRecommendations({
+ dependencies,
+ declaredDependencies: [],
+ optionalDependencies,
+ externalDependencies: new Set(),
+ isEffect: true,
+ });
+ context.report({
+ node: node.parent.callee,
+ message:
+ `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` +
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
+ `To fix this, pass [` +
+ suggestedDependencies.join(', ') +
+ `] as a second argument to the ${reactiveHookName} Hook.`,
+
+ fix(fixer) {
+ return fixer.insertTextAfter(
+ node,
+ `, [${suggestedDependencies.join(', ')}]`,
+ );
+ },
+ });
+ }
+
+ return;
+ }
+
+ const declaredDependencies = [];
+ const externalDependencies = new Set();
+
+ if (declaredDependenciesNode.type !== 'ArrayExpression') {
+ // If the declared dependencies are not an array expression then we
+ // can't verify that the user provided the correct dependencies. Tell
+ // the user this in an error.
+ context.report({
+ node: declaredDependenciesNode,
+ message:
+ `React Hook ${context.getSource(reactiveHook)} was passed a ` +
+ 'dependency list that is not an array literal. This means we ' +
+ "can't statically verify whether you've passed the correct " +
+ 'dependencies.',
+ });
+ } else {
+ declaredDependenciesNode.elements.forEach(declaredDependencyNode => {
+ // Skip elided elements.
+ if (declaredDependencyNode === null) {
+ return;
+ } // If we see a spread element then add a special warning.
+
+ if (declaredDependencyNode.type === 'SpreadElement') {
+ context.report({
+ node: declaredDependencyNode,
+ message:
+ `React Hook ${context.getSource(reactiveHook)} has a spread ` +
+ "element in its dependency array. This means we can't " +
+ "statically verify whether you've passed the " +
+ 'correct dependencies.',
+ });
+ return;
+ } // Try to normalize the declared dependency. If we can't then an error
+ // will be thrown. We will catch that error and report an error.
+
+ let declaredDependency;
+
+ try {
+ declaredDependency = toPropertyAccessString(declaredDependencyNode);
+ } catch (error) {
+ if (/Unsupported node type/.test(error.message)) {
+ if (declaredDependencyNode.type === 'Literal') {
+ if (dependencies.has(declaredDependencyNode.value)) {
+ context.report({
+ node: declaredDependencyNode,
+ message:
+ `The ${declaredDependencyNode.raw} literal is not a valid dependency ` +
+ `because it never changes. ` +
+ `Did you mean to include ${declaredDependencyNode.value} in the array instead?`,
+ });
+ } else {
+ context.report({
+ node: declaredDependencyNode,
+ message:
+ `The ${declaredDependencyNode.raw} literal is not a valid dependency ` +
+ 'because it never changes. You can safely remove it.',
+ });
+ }
+ } else {
+ context.report({
+ node: declaredDependencyNode,
+ message:
+ `React Hook ${context.getSource(reactiveHook)} has a ` +
+ `complex expression in the dependency array. ` +
+ 'Extract it to a separate variable so it can be statically checked.',
+ });
+ }
+
+ return;
+ } else {
+ throw error;
+ }
+ }
+
+ let maybeID = declaredDependencyNode;
+
+ while (maybeID.type === 'MemberExpression') {
+ maybeID = maybeID.object;
+ }
+
+ const isDeclaredInComponent = !componentScope.through.some(
+ ref => ref.identifier === maybeID,
+ ); // Add the dependency to our declared dependency map.
+
+ declaredDependencies.push({
+ key: declaredDependency,
+ node: declaredDependencyNode,
+ });
+
+ if (!isDeclaredInComponent) {
+ externalDependencies.add(declaredDependency);
+ }
+ });
+ }
+
+ let {
+ suggestedDependencies,
+ unnecessaryDependencies,
+ missingDependencies,
+ duplicateDependencies,
+ } = collectRecommendations({
+ dependencies,
+ declaredDependencies,
+ optionalDependencies,
+ externalDependencies,
+ isEffect,
+ });
+ const problemCount =
+ duplicateDependencies.size +
+ missingDependencies.size +
+ unnecessaryDependencies.size; // If we're going to report a missing dependency,
+ // we might as well recalculate the list ignoring
+ // the currently specified deps. This can result
+ // in some extra deduplication. We can't do this
+ // for effects though because those have legit
+ // use cases for over-specifying deps.
+
+ if (!isEffect && missingDependencies.size > 0) {
+ suggestedDependencies = collectRecommendations({
+ dependencies,
+ declaredDependencies: [],
+ // Pretend we don't know
+ optionalDependencies,
+ externalDependencies,
+ isEffect,
+ }).suggestedDependencies;
+ } // Alphabetize the suggestions, but only if deps were already alphabetized.
+
+ function areDeclaredDepsAlphabetized() {
+ if (declaredDependencies.length === 0) {
+ return true;
+ }
+
+ const declaredDepKeys = declaredDependencies.map(dep => dep.key);
+ const sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
+ return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
+ }
+
+ if (areDeclaredDepsAlphabetized()) {
+ suggestedDependencies.sort();
+ }
+
+ function getWarningMessage(deps, singlePrefix, label, fixVerb) {
+ if (deps.size === 0) {
+ return null;
+ }
+
+ return (
+ (deps.size > 1 ? '' : singlePrefix + ' ') +
+ label +
+ ' ' +
+ (deps.size > 1 ? 'dependencies' : 'dependency') +
+ ': ' +
+ joinEnglish(
+ Array.from(deps)
+ .sort()
+ .map(name => "'" + name + "'"),
+ ) +
+ `. Either ${fixVerb} ${
+ deps.size > 1 ? 'them' : 'it'
+ } or remove the dependency array.`
+ );
+ }
+
+ let extraWarning = '';
+
+ if (unnecessaryDependencies.size > 0) {
+ let badRef = null;
+ Array.from(unnecessaryDependencies.keys()).forEach(key => {
+ if (badRef !== null) {
+ return;
+ }
+
+ if (key.endsWith('.current')) {
+ badRef = key;
+ }
+ });
+
+ if (badRef !== null) {
+ extraWarning =
+ ` Mutable values like '${badRef}' aren't valid dependencies ` +
+ "because mutating them doesn't re-render the component.";
+ } else if (externalDependencies.size > 0) {
+ const dep = Array.from(externalDependencies)[0]; // Don't show this warning for things that likely just got moved *inside* the callback
+ // because in that case they're clearly not referring to globals.
+
+ if (!scope.set.has(dep)) {
+ extraWarning =
+ ` Outer scope values like '${dep}' aren't valid dependencies ` +
+ `because mutating them doesn't re-render the component.`;
+ }
+ }
+ } // `props.foo()` marks `props` as a dependency because it has
+ // a `this` value. This warning can be confusing.
+ // So if we're going to show it, append a clarification.
+
+ if (!extraWarning && missingDependencies.has('props')) {
+ let propDep = dependencies.get('props');
+
+ if (propDep == null) {
+ return;
+ }
+
+ const refs = propDep.references;
+
+ if (!Array.isArray(refs)) {
+ return;
+ }
+
+ let isPropsOnlyUsedInMembers = true;
+
+ for (let i = 0; i < refs.length; i++) {
+ const ref = refs[i];
+ const id = fastFindReferenceWithParent(
+ componentScope.block,
+ ref.identifier,
+ );
+
+ if (!id) {
+ isPropsOnlyUsedInMembers = false;
+ break;
+ }
+
+ const parent = id.parent;
+
+ if (parent == null) {
+ isPropsOnlyUsedInMembers = false;
+ break;
+ }
+
+ if (parent.type !== 'MemberExpression') {
+ isPropsOnlyUsedInMembers = false;
+ break;
+ }
+ }
+
+ if (isPropsOnlyUsedInMembers) {
+ extraWarning =
+ ` However, 'props' will change when *any* prop changes, so the ` +
+ `preferred fix is to destructure the 'props' object outside of ` +
+ `the ${reactiveHookName} call and refer to those specific props ` +
+ `inside ${context.getSource(reactiveHook)}.`;
+ }
+ }
+
+ if (!extraWarning && missingDependencies.size > 0) {
+ // See if the user is trying to avoid specifying a callable prop.
+ // This usually means they're unaware of useWorker.
+ let missingCallbackDep = null;
+ missingDependencies.forEach(missingDep => {
+ if (missingCallbackDep) {
+ return;
+ } // Is this a variable from top scope?
+
+ const topScopeRef = componentScope.set.get(missingDep);
+ const usedDep = dependencies.get(missingDep);
+
+ if (usedDep.references[0].resolved !== topScopeRef) {
+ return;
+ } // Is this a destructured prop?
+
+ const def = topScopeRef.defs[0];
+
+ if (def == null || def.name == null || def.type !== 'Parameter') {
+ return;
+ } // Was it called in at least one case? Then it's a function.
+
+ let isFunctionCall = false;
+ let id;
+
+ for (let i = 0; i < usedDep.references.length; i++) {
+ id = usedDep.references[i].identifier;
+
+ if (
+ id != null &&
+ id.parent != null &&
+ id.parent.type === 'CallExpression' &&
+ id.parent.callee === id
+ ) {
+ isFunctionCall = true;
+ break;
+ }
+ }
+
+ if (!isFunctionCall) {
+ return;
+ } // If it's missing (i.e. in component scope) *and* it's a parameter
+ // then it is definitely coming from props destructuring.
+ // (It could also be props itself but we wouldn't be calling it then.)
+
+ missingCallbackDep = missingDep;
+ });
+
+ if (missingCallbackDep !== null) {
+ extraWarning =
+ ` If '${missingCallbackDep}' changes too often, ` +
+ `find the parent component that defines it ` +
+ `and wrap that definition in useWorker.`;
+ }
+ }
+
+ if (!extraWarning && missingDependencies.size > 0) {
+ let setStateRecommendation = null;
+ missingDependencies.forEach(missingDep => {
+ if (setStateRecommendation !== null) {
+ return;
+ }
+
+ const usedDep = dependencies.get(missingDep);
+ const references = usedDep.references;
+ let id;
+ let maybeCall;
+
+ for (let i = 0; i < references.length; i++) {
+ id = references[i].identifier;
+ maybeCall = id.parent; // Try to see if we have setState(someExpr(missingDep)).
+
+ while (maybeCall != null && maybeCall !== componentScope.block) {
+ if (maybeCall.type === 'CallExpression') {
+ const correspondingStateVariable = setStateCallSites.get(
+ maybeCall.callee,
+ );
+
+ if (correspondingStateVariable != null) {
+ if (correspondingStateVariable.name === missingDep) {
+ // setCount(count + 1)
+ setStateRecommendation = {
+ missingDep,
+ setter: maybeCall.callee.name,
+ form: 'updater',
+ };
+ } else if (stateVariables.has(id)) {
+ // setCount(count + increment)
+ setStateRecommendation = {
+ missingDep,
+ setter: maybeCall.callee.name,
+ form: 'reducer',
+ };
+ } else {
+ const resolved = references[i].resolved;
+
+ if (resolved != null) {
+ // If it's a parameter *and* a missing dep,
+ // it must be a prop or something inside a prop.
+ // Therefore, recommend an inline reducer.
+ const def = resolved.defs[0];
+
+ if (def != null && def.type === 'Parameter') {
+ setStateRecommendation = {
+ missingDep,
+ setter: maybeCall.callee.name,
+ form: 'inlineReducer',
+ };
+ }
+ }
+ }
+
+ break;
+ }
+ }
+
+ maybeCall = maybeCall.parent;
+ }
+
+ if (setStateRecommendation !== null) {
+ break;
+ }
+ }
+ });
+
+ if (setStateRecommendation !== null) {
+ switch (setStateRecommendation.form) {
+ case 'reducer':
+ extraWarning =
+ ` You can also replace multiple useState variables with useReducer ` +
+ `if '${setStateRecommendation.setter}' needs the ` +
+ `current value of '${setStateRecommendation.missingDep}'.`;
+ break;
+
+ case 'inlineReducer':
+ extraWarning =
+ ` If '${setStateRecommendation.setter}' needs the ` +
+ `current value of '${setStateRecommendation.missingDep}', ` +
+ `you can also switch to useReducer instead of useState and ` +
+ `read '${setStateRecommendation.missingDep}' in the reducer.`;
+ break;
+
+ case 'updater':
+ extraWarning =
+ ` You can also do a functional update '${
+ setStateRecommendation.setter
+ }(${setStateRecommendation.missingDep.substring(
+ 0,
+ 1,
+ )} => ...)' if you only need '${
+ setStateRecommendation.missingDep
+ }'` + ` in the '${setStateRecommendation.setter}' call.`;
+ break;
+
+ default:
+ throw new Error('Unknown case.');
+ }
+ }
+ }
+
+ if (problemCount !== 0) {
+ context.report({
+ node: declaredDependenciesNode,
+ message:
+ `React Hook ${context.getSource(reactiveHook)} has ` + // To avoid a long message, show the next actionable item.
+ (getWarningMessage(
+ missingDependencies,
+ 'a',
+ 'missing',
+ 'include',
+ ) ||
+ getWarningMessage(
+ unnecessaryDependencies,
+ 'an',
+ 'unnecessary',
+ 'exclude',
+ ) ||
+ getWarningMessage(
+ duplicateDependencies,
+ 'a',
+ 'duplicate',
+ 'omit',
+ )) +
+ extraWarning,
+
+ fix(fixer) {
+ // TODO: consider preserving the comments or formatting?
+ return fixer.replaceText(
+ declaredDependenciesNode,
+ `[${suggestedDependencies.join(', ')}]`,
+ );
+ },
+ });
+ }
+ }
+ },
+}; // The meat of the logic.
+
+function collectRecommendations({
+ dependencies,
+ declaredDependencies,
+ optionalDependencies,
+ externalDependencies,
+ isEffect,
+}) {
+ // Our primary data structure.
+ // It is a logical representation of property chains:
+ // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
+ // -> `props.lol`
+ // -> `props.huh` -> `props.huh.okay`
+ // -> `props.wow`
+ // We'll use it to mark nodes that are *used* by the programmer,
+ // and the nodes that were *declared* as deps. Then we will
+ // traverse it to learn which deps are missing or unnecessary.
+ const depTree = createDepTree();
+
+ function createDepTree() {
+ return {
+ isRequired: false,
+ // True if used in code
+ isSatisfiedRecursively: false,
+ // True if specified in deps
+ hasRequiredNodesBelow: false,
+ // True if something deeper is used by code
+ children: new Map(), // Nodes for properties
+ };
+ } // Mark all required nodes first.
+ // Imagine exclamation marks next to each used deep property.
+
+ dependencies.forEach((_, key) => {
+ const node = getOrCreateNodeByPath(depTree, key);
+ node.isRequired = true;
+ markAllParentsByPath(depTree, key, parent => {
+ parent.hasRequiredNodesBelow = true;
+ });
+ }); // Mark all satisfied nodes.
+ // Imagine checkmarks next to each declared dependency.
+
+ declaredDependencies.forEach(({ key }) => {
+ const node = getOrCreateNodeByPath(depTree, key);
+ node.isSatisfiedRecursively = true;
+ });
+ optionalDependencies.forEach(key => {
+ const node = getOrCreateNodeByPath(depTree, key);
+ node.isSatisfiedRecursively = true;
+ }); // Tree manipulation helpers.
+
+ function getOrCreateNodeByPath(rootNode, path) {
+ let keys = path.split('.');
+ let node = rootNode;
+
+ for (let key of keys) {
+ let child = node.children.get(key);
+
+ if (!child) {
+ child = createDepTree();
+ node.children.set(key, child);
+ }
+
+ node = child;
+ }
+
+ return node;
+ }
+
+ function markAllParentsByPath(rootNode, path, fn) {
+ let keys = path.split('.');
+ let node = rootNode;
+
+ for (let key of keys) {
+ let child = node.children.get(key);
+
+ if (!child) {
+ return;
+ }
+
+ fn(child);
+ node = child;
+ }
+ } // Now we can learn which dependencies are missing or necessary.
+
+ let missingDependencies = new Set();
+ let satisfyingDependencies = new Set();
+ scanTreeRecursively(
+ depTree,
+ missingDependencies,
+ satisfyingDependencies,
+ key => key,
+ );
+
+ function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
+ node.children.forEach((child, key) => {
+ const path = keyToPath(key);
+
+ if (child.isSatisfiedRecursively) {
+ if (child.hasRequiredNodesBelow) {
+ // Remember this dep actually satisfied something.
+ satisfyingPaths.add(path);
+ } // It doesn't matter if there's something deeper.
+ // It would be transitively satisfied since we assume immutability.
+ // `props.foo` is enough if you read `props.foo.id`.
+
+ return;
+ }
+
+ if (child.isRequired) {
+ // Remember that no declared deps satisfied this node.
+ missingPaths.add(path); // If we got here, nothing in its subtree was satisfied.
+ // No need to search further.
+
+ return;
+ }
+
+ scanTreeRecursively(
+ child,
+ missingPaths,
+ satisfyingPaths,
+ childKey => path + '.' + childKey,
+ );
+ });
+ } // Collect suggestions in the order they were originally specified.
+
+ let suggestedDependencies = [];
+ let unnecessaryDependencies = new Set();
+ let duplicateDependencies = new Set();
+ declaredDependencies.forEach(({ key }) => {
+ // Does this declared dep satisfy a real need?
+ if (satisfyingDependencies.has(key)) {
+ if (suggestedDependencies.indexOf(key) === -1) {
+ // Good one.
+ suggestedDependencies.push(key);
+ } else {
+ // Duplicate.
+ duplicateDependencies.add(key);
+ }
+ } else {
+ if (
+ isEffect &&
+ !key.endsWith('.current') &&
+ !externalDependencies.has(key)
+ ) {
+ // Effects are allowed extra "unnecessary" deps.
+ // Such as resetting scroll when ID changes.
+ // Consider them legit.
+ // The exception is ref.current which is always wrong.
+ if (suggestedDependencies.indexOf(key) === -1) {
+ suggestedDependencies.push(key);
+ }
+ } else {
+ // It's definitely not needed.
+ unnecessaryDependencies.add(key);
+ }
+ }
+ }); // Then add the missing ones at the end.
+
+ missingDependencies.forEach(key => {
+ suggestedDependencies.push(key);
+ });
+ return {
+ suggestedDependencies,
+ unnecessaryDependencies,
+ duplicateDependencies,
+ missingDependencies,
+ };
+}
+/**
+ * Assuming () means the passed/returned node:
+ * (props) => (props)
+ * props.(foo) => (props.foo)
+ * props.foo.(bar) => (props).foo.bar
+ * props.foo.bar.(baz) => (props).foo.bar.baz
+ */
+
+function getDependency(node) {
+ if (
+ node.parent.type === 'MemberExpression' &&
+ node.parent.object === node &&
+ node.parent.property.name !== 'current' &&
+ !node.parent.computed &&
+ !(
+ node.parent.parent != null &&
+ node.parent.parent.type === 'CallExpression' &&
+ node.parent.parent.callee === node.parent
+ )
+ ) {
+ return getDependency(node.parent);
+ } else {
+ return node;
+ }
+}
+/**
+ * Assuming () means the passed node.
+ * (foo) -> 'foo'
+ * foo.(bar) -> 'foo.bar'
+ * foo.bar.(baz) -> 'foo.bar.baz'
+ * Otherwise throw.
+ */
+
+function toPropertyAccessString(node) {
+ if (node.type === 'Identifier') {
+ return node.name;
+ } else if (node.type === 'MemberExpression' && !node.computed) {
+ const object = toPropertyAccessString(node.object);
+ const property = toPropertyAccessString(node.property);
+ return `${object}.${property}`;
+ } else {
+ throw new Error(`Unsupported node type: ${node.type}`);
+ }
+}
+
+function getNodeWithoutReactNamespace(node) {
+ if (
+ node.type === 'MemberExpression' &&
+ node.object.type === 'Identifier' &&
+ node.object.name === 'React' &&
+ node.property.type === 'Identifier' &&
+ !node.computed
+ ) {
+ return node.property;
+ }
+
+ return node;
+} // What's the index of callback that needs to be analyzed for a given Hook?
+// -1 if it's not a Hook we care about (e.g. useState).
+// 0 for useEffect/useMemo/useCallback(fn).
+// 1 for useImperativeHandle(ref, fn).
+// For additionally configured Hooks, assume that they're like useEffect (0).
+
+function getReactiveHookCallbackIndex(calleeNode) {
+ let node = getNodeWithoutReactNamespace(calleeNode);
+
+ if (node.type !== 'Identifier') {
+ return null;
+ }
+
+ switch (node.name) {
+ case 'useAsyncEffect':
+ case 'useAsyncLayoutEffect':
+ case 'useWorker':
+ case 'useWorkerState':
+ case 'useEffectUpdate':
+ return 0;
+
+ case 'useConditionalEffect':
+ return 1;
+
+ default:
+ return -1;
+ }
+}
+/**
+ * ESLint won't assign node.parent to references from context.getScope()
+ *
+ * So instead we search for the node from an ancestor assigning node.parent
+ * as we go. This mutates the AST.
+ *
+ * This traversal is:
+ * - optimized by only searching nodes with a range surrounding our target node
+ * - agnostic to AST node types, it looks for `{ type: string, ... }`
+ */
+
+function fastFindReferenceWithParent(start, target) {
+ let queue = [start];
+ let item = null;
+
+ while (queue.length) {
+ item = queue.shift();
+
+ if (isSameIdentifier(item, target)) {
+ return item;
+ }
+
+ if (!isAncestorNodeOf(item, target)) {
+ continue;
+ }
+
+ for (let [key, value] of Object.entries(item)) {
+ if (key === 'parent') {
+ continue;
+ }
+
+ if (isNodeLike(value)) {
+ value.parent = item;
+ queue.push(value);
+ } else if (Array.isArray(value)) {
+ value.forEach(val => {
+ if (isNodeLike(val)) {
+ val.parent = item;
+ queue.push(val);
+ }
+ });
+ }
+ }
+ }
+
+ return null;
+}
+
+function joinEnglish(arr) {
+ let s = '';
+
+ for (let i = 0; i < arr.length; i++) {
+ s += arr[i];
+
+ if (i === 0 && arr.length === 2) {
+ s += ' and ';
+ } else if (i === arr.length - 2 && arr.length > 2) {
+ s += ', and ';
+ } else if (i < arr.length - 1) {
+ s += ', ';
+ }
+ }
+
+ return s;
+}
+
+function isNodeLike(val) {
+ return (
+ typeof val === 'object' &&
+ val !== null &&
+ !Array.isArray(val) &&
+ typeof val.type === 'string'
+ );
+}
+
+function isSameIdentifier(a, b) {
+ return (
+ a.type === 'Identifier' &&
+ a.name === b.name &&
+ a.range[0] === b.range[0] &&
+ a.range[1] === b.range[1]
+ );
+}
+
+function isAncestorNodeOf(a, b) {
+ return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
+}
diff --git a/eslint-plugin/package.json b/eslint-plugin/package.json
new file mode 100644
index 0000000..e48bbf4
--- /dev/null
+++ b/eslint-plugin/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@react-hook-utilities/eslint-plugin",
+ "version": "0.1.0",
+ "description": "ESLint rules for hook react-hook-utilities",
+ "keywords": [
+ "eslint",
+ "eslintplugin",
+ "eslint-plugin"
+ ],
+ "author": "Flávio Caetano",
+ "main": "lib/index.js",
+ "scripts": {
+ "test": "mocha tests --recursive src/__tests__/**/*",
+ "babel": "rm -rf lib && babel src/ -d ./lib"
+ },
+ "devDependencies": {
+ "mocha": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "license": "ISC"
+}
diff --git a/eslint-plugin/yarn.lock b/eslint-plugin/yarn.lock
new file mode 100644
index 0000000..cdcf854
--- /dev/null
+++ b/eslint-plugin/yarn.lock
@@ -0,0 +1,227 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+browser-stdout@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
+ integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8=
+
+commander@2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+ integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=
+ dependencies:
+ graceful-readlink ">= 1.0.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+debug@2.6.8:
+ version "2.6.8"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
+ integrity sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=
+ dependencies:
+ ms "2.0.0"
+
+diff@3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
+ integrity sha1-yc45Okt8vQsFinJck98pkCeGj/k=
+
+escape-string-regexp@1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+glob@7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+ integrity sha1-gFIR3wT6rxxjo2ADBs31reULLsg=
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.2"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+"graceful-readlink@>= 1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+ integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
+
+growl@1.9.2:
+ version "1.9.2"
+ resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
+ integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=
+
+has-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+ integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=
+
+he@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+ integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+json3@3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
+ integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=
+
+lodash._baseassign@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
+ integrity sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=
+ dependencies:
+ lodash._basecopy "^3.0.0"
+ lodash.keys "^3.0.0"
+
+lodash._basecopy@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
+ integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=
+
+lodash._basecreate@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821"
+ integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=
+
+lodash._getnative@^3.0.0:
+ version "3.9.1"
+ resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+ integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
+
+lodash._isiterateecall@^3.0.0:
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
+ integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=
+
+lodash.create@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
+ integrity sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=
+ dependencies:
+ lodash._baseassign "^3.0.0"
+ lodash._basecreate "^3.0.0"
+ lodash._isiterateecall "^3.0.0"
+
+lodash.isarguments@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
+ integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=
+
+lodash.isarray@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+ integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=
+
+lodash.keys@^3.0.0:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
+ integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=
+ dependencies:
+ lodash._getnative "^3.0.0"
+ lodash.isarguments "^3.0.0"
+ lodash.isarray "^3.0.0"
+
+minimatch@^3.0.2:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+ integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+
+mkdirp@0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+ dependencies:
+ minimist "0.0.8"
+
+mocha@^3.1.2:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d"
+ integrity sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==
+ dependencies:
+ browser-stdout "1.3.0"
+ commander "2.9.0"
+ debug "2.6.8"
+ diff "3.2.0"
+ escape-string-regexp "1.0.5"
+ glob "7.1.1"
+ growl "1.9.2"
+ he "1.1.1"
+ json3 "3.3.2"
+ lodash.create "3.1.1"
+ mkdirp "0.5.1"
+ supports-color "3.1.2"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+supports-color@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
+ integrity sha1-cqJiiU2dQIuVbKBf83su2KbiotU=
+ dependencies:
+ has-flag "^1.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
diff --git a/jest.config.js b/jest.config.js
index 749c55a..2fbe9b0 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,8 +1,6 @@
module.exports = {
- roots: [
- "/src"
- ],
+ roots: ['/src', '/eslint-plugin/lib'],
transform: {
- "^.+\\.tsx?$": "ts-jest"
- }
-}
\ No newline at end of file
+ '^.+\\.tsx?$': 'ts-jest',
+ },
+};
diff --git a/package.json b/package.json
index 441345e..2fc0f89 100644
--- a/package.json
+++ b/package.json
@@ -21,31 +21,40 @@
"scripts": {
"build": "tsc && prettier --check 'dist/**/*' --write",
"test": "jest --coverage",
- "lint": "prettier --check 'src/**/*'",
- "docs": "typedoc --out docs/$npm_package_version && scripts/gen-docs-index.sh $npm_package_version",
+ "lint": "eslint --ext=.ts,.tsx --max-warnings=0 src/ && prettier --check 'src/**/*'",
+ "docs": "scripts/gen-docs.sh $npm_package_version",
"version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/index.js",
- "dist/index.d.ts"
+ "dist/index.d.ts",
+ "eslint-plugin/!(*/__tests__|yarn*)"
],
"dependencies": {
"react": "^16"
},
"devDependencies": {
+ "@react-hook-utilities/eslint-plugin": "./eslint-plugin",
"@testing-library/react-hooks": "^2.0.1",
"@types/jest": "^24.0.18",
"@types/react": "^16.9.2",
+ "@typescript-eslint/eslint-plugin": "^2.3.1",
+ "@typescript-eslint/parser": "^2.3.1",
+ "babel-eslint": "^10.0.3",
"commitlint": "^8.2.0",
"conventional-changelog-cli": "^2.0.25",
+ "eslint": "^6.5.0",
+ "eslint-plugin-react": "^7.14.3",
+ "eslint-plugin-react-hooks": "^2.1.1",
"husky": "^3.0.5",
"jest": "^24.9.0",
"prettier": "^1.19.1",
"react-test-renderer": "^16.9.0",
"ts-jest": "^24.1.0",
"typedoc": "^0.16.9",
- "typescript": "^3.7"
+ "typescript": "^3.7",
+ "typedoc-plugin-markdown-pages": "^0.3.0"
}
}
diff --git a/scripts/gen-docs-index.sh b/scripts/gen-docs-index.sh
deleted file mode 100755
index 6c643e2..0000000
--- a/scripts/gen-docs-index.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-
-PKG_VERSION=$1
-
-echo " ;" > docs/index.html
\ No newline at end of file
diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh
new file mode 100755
index 0000000..7786187
--- /dev/null
+++ b/scripts/gen-docs.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+PKG_VERSION=$1
+
+mkdir pageDocs && ln -sf ../eslint-plugin/README.md pageDocs/EslintPlugin.md
+
+yarn run typedoc --out "docs/${PKG_VERSION}"
+
+echo " ;" > docs/index.html
+
+rm -rf pageDocs
\ No newline at end of file
diff --git a/src/__tests__/useDidUnmount.ts b/src/__tests__/useDidUnmount.ts
index e96ae7c..65b8634 100644
--- a/src/__tests__/useDidUnmount.ts
+++ b/src/__tests__/useDidUnmount.ts
@@ -3,23 +3,23 @@ import { renderHook } from '@testing-library/react-hooks';
import { useDidUnmount } from '..';
let cleanup: jest.Mock;
-let hook: any;
+let useHook: any;
beforeEach(() => {
cleanup = jest.fn();
- hook = ({ func = cleanup, deps }: any = {}) => {
+ useHook = ({ func = cleanup, deps }: any = {}) => {
useDidUnmount(func, deps);
};
});
it('does not call the effect if the component is mounted', () => {
- renderHook(hook);
+ renderHook(useHook);
expect(cleanup).not.toHaveBeenCalled();
});
it('calls the effect when the component gets unmounted', () => {
- const { unmount } = renderHook(hook);
+ const { unmount } = renderHook(useHook);
expect(cleanup).not.toHaveBeenCalled();
unmount();
@@ -28,7 +28,7 @@ it('calls the effect when the component gets unmounted', () => {
});
it('calls the effect only once', () => {
- const { unmount } = renderHook(hook);
+ const { unmount } = renderHook(useHook);
expect(cleanup).not.toHaveBeenCalled();
unmount();
@@ -50,7 +50,7 @@ it('runs an async funciton', async () => {
finished = true;
});
- const { unmount } = renderHook(hook);
+ const { unmount } = renderHook(useHook);
unmount();
@@ -64,7 +64,7 @@ it('runs an async funciton', async () => {
it('calls the effect with updated dependencies', () => {
const secondCleanup = jest.fn();
- const { unmount, rerender } = renderHook(hook, {
+ const { unmount, rerender } = renderHook(useHook, {
initialProps: { func: cleanup, deps: [cleanup] },
});
@@ -78,7 +78,7 @@ it('calls the effect with updated dependencies', () => {
it('does not update the effect when the dependencies have not been updated', () => {
const secondCleanup = jest.fn();
- const { unmount, rerender } = renderHook(hook, {
+ const { unmount, rerender } = renderHook(useHook, {
initialProps: { func: cleanup, deps: [cleanup] },
});
diff --git a/src/__tests__/useEffectUpdate.ts b/src/__tests__/useEffectUpdate.ts
index c00df4a..6304dc5 100644
--- a/src/__tests__/useEffectUpdate.ts
+++ b/src/__tests__/useEffectUpdate.ts
@@ -4,12 +4,15 @@ import { useEffectUpdate } from '..';
const deps = [1, 2, 3];
-const hook = async ({ dependencies, cleanup }: any) =>
+const hook = async ({ dependencies = [], cleanup }: any) =>
new Promise(resolve => {
- useEffectUpdate(oldState => {
- resolve(oldState);
- return cleanup;
- }, dependencies);
+ useEffectUpdate(
+ oldState => {
+ resolve(oldState);
+ return cleanup;
+ },
+ dependencies, // eslint-disable-line @react-hook-utilities/exhaustive-deps
+ );
});
it('receives empty array as old state', () => {
diff --git a/src/__tests__/useLazyRef.ts b/src/__tests__/useLazyRef.ts
index d793201..b30eea4 100644
--- a/src/__tests__/useLazyRef.ts
+++ b/src/__tests__/useLazyRef.ts
@@ -3,18 +3,18 @@ import { renderHook } from '@testing-library/react-hooks';
import { useLazyRef } from '..';
const callbackFn = jest.fn();
-const mockHook = () => useLazyRef(callbackFn);
+const useMockHook = () => useLazyRef(callbackFn);
beforeEach(callbackFn.mockRestore);
it('calls factory on first call', () => {
- renderHook(mockHook);
+ renderHook(useMockHook);
expect(callbackFn.mock.calls.length).toEqual(1);
});
it('does not call factory when value is unset', () => {
- const { result, rerender } = renderHook(mockHook);
+ const { result, rerender } = renderHook(useMockHook);
expect(callbackFn.mock.calls.length).toEqual(1);
// act
@@ -28,7 +28,7 @@ describe('change in current value', () => {
const assertChangeCurrentValue = () => {
// initial value
callbackFn.mockReturnValue(0);
- const { result } = renderHook(mockHook);
+ const { result } = renderHook(useMockHook);
expect(result.current.current).toEqual(0);
diff --git a/src/__tests__/usePromisedState.ts b/src/__tests__/usePromisedState.ts
index eaf7abf..1dca33a 100644
--- a/src/__tests__/usePromisedState.ts
+++ b/src/__tests__/usePromisedState.ts
@@ -3,7 +3,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
import { usePromisedState } from '..';
describe('non-nullable, non-undefined state type', () => {
- const hookHelper = () => usePromisedState();
+ const useHookHelper = () => usePromisedState();
it("does not resolve promise if set isn't called", () => {
let resolved = false;
@@ -11,7 +11,7 @@ describe('non-nullable, non-undefined state type', () => {
result: {
current: [promise],
},
- } = renderHook(hookHelper);
+ } = renderHook(useHookHelper);
promise.then(() => {
resolved = true;
@@ -27,7 +27,7 @@ describe('non-nullable, non-undefined state type', () => {
result: {
current: [promise, set],
},
- } = renderHook(hookHelper);
+ } = renderHook(useHookHelper);
promise.then(() => {
resolved = true;
@@ -48,7 +48,7 @@ describe('non-nullable, non-undefined state type', () => {
it('resolves a new promise when the state is set', async () => {
let resolved = false;
- const { result } = renderHook(hookHelper);
+ const { result } = renderHook(useHookHelper);
result.current[0].then(() => {
resolved = true;
@@ -69,7 +69,7 @@ describe('non-nullable, non-undefined state type', () => {
});
describe('nullable state type', () => {
- const hookHelper = () => usePromisedState();
+ const useHookHelper = () => usePromisedState();
it("does not resolve promise if set isn't called", () => {
let resolved = false;
@@ -77,7 +77,7 @@ describe('nullable state type', () => {
result: {
current: [promise],
},
- } = renderHook(hookHelper);
+ } = renderHook(useHookHelper);
promise.then(() => {
resolved = true;
@@ -93,7 +93,7 @@ describe('nullable state type', () => {
result: {
current: [promise, set],
},
- } = renderHook(hookHelper);
+ } = renderHook(useHookHelper);
promise.then(() => {
resolved = true;
@@ -108,7 +108,7 @@ describe('nullable state type', () => {
});
it('waits on a new state after setting undefined', async () => {
- const { result } = renderHook(hookHelper);
+ const { result } = renderHook(useHookHelper);
act(() => {
result.current[1](0);
@@ -126,7 +126,7 @@ describe('nullable state type', () => {
it('resolves a new promise when the state is set', async () => {
let resolved = false;
- const { result } = renderHook(hookHelper);
+ const { result } = renderHook(useHookHelper);
result.current[0].then(() => {
resolved = true;
diff --git a/src/__tests__/useWorkerState.ts b/src/__tests__/useWorkerState.ts
index 946d493..f3ede35 100644
--- a/src/__tests__/useWorkerState.ts
+++ b/src/__tests__/useWorkerState.ts
@@ -210,7 +210,7 @@ describe('callback', () => {
});
it('updates the scope of the callback', async () => {
- const hook = () => {
+ const useHook = () => {
const [state, setState] = useState(0);
const worker = useWorkerState(async () => {
callbackFn(state);
@@ -218,7 +218,7 @@ describe('callback', () => {
return { state, setState, ...worker };
};
- const { result, waitForNextUpdate } = renderHook(hook);
+ const { result, waitForNextUpdate } = renderHook(useHook);
expect(callbackFn.mock.calls[0][0]).toBe(0);
diff --git a/src/index.tsx b/src/index.tsx
index da9ea48..e807b53 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -4,6 +4,7 @@ import {
useCallback,
useState,
useRef,
+ useMemo,
MutableRefObject,
} from 'react';
@@ -55,11 +56,11 @@ export interface ValuablePromise extends Promise {
*/
export const useAsyncEffect = (
effect: () => Promise,
- dependencies?: readonly any[],
+ dependencies: readonly any[] = [],
) => {
useEffect(() => {
effect();
- }, dependencies);
+ }, [...dependencies, effect]); // eslint-disable-line react-hooks/exhaustive-deps
};
/**
@@ -73,11 +74,11 @@ export const useAsyncEffect = (
*/
export const useAsyncLayoutEffect = (
effect: () => Promise,
- dependencies?: readonly any[],
+ dependencies: readonly any[] = [],
) => {
useLayoutEffect(() => {
effect();
- }, dependencies);
+ }, [...dependencies, effect]); // eslint-disable-line react-hooks/exhaustive-deps
};
/**
@@ -114,19 +115,20 @@ export const useWorker = (
setState(s => ({ ...s, isLoading }));
}, []);
- const callback = useCallback(async (...args: TArgs): Promise<
- TRet | undefined
- > => {
- try {
- setIsLoading(true);
- const result = await worker(...args);
- setState({ isLoading: false, error: undefined });
+ const callback = useCallback(
+ async (...args: TArgs): Promise => {
+ try {
+ setIsLoading(true);
+ const result = await worker(...args);
+ setState({ isLoading: false, error: undefined });
- return result;
- } catch (error) {
- setState({ isLoading: false, error });
- }
- }, dependencies);
+ return result;
+ } catch (error) {
+ setState({ isLoading: false, error });
+ }
+ },
+ [...dependencies, worker], // eslint-disable-line react-hooks/exhaustive-deps
+ );
return { callback, error, isLoading, setError, setIsLoading };
};
@@ -280,7 +282,7 @@ export function useEffectUpdate(
} finally {
oldState.current = dependencies;
}
- }, dependencies);
+ }, [...dependencies, effect]); // eslint-disable-line react-hooks/exhaustive-deps
}
/**
@@ -448,7 +450,7 @@ export function useConditionalEffect(
*/
export function useConditionalEffect(
evalCondition: (oldState: Dependencies) => boolean,
- effect: () => (() => void) | void,
+ effect: () => void | (() => void),
dependencies: Dependencies,
): void;
export function useConditionalEffect(
@@ -458,7 +460,7 @@ export function useConditionalEffect(
): void {
useEffectUpdate(
oldState => (evalCondition(oldState) ? effect() : undefined),
- dependencies,
+ dependencies, // eslint-disable-line @react-hook-utilities/exhaustive-deps
);
}
@@ -476,7 +478,7 @@ export const useDidMount = (
if (typeof result === 'function') {
return result;
}
- }, []);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
};
/**
@@ -508,7 +510,7 @@ export const useDidUnmount = (
effect();
}
},
- dependencies || [],
+ dependencies || [], // eslint-disable-line react-hooks/exhaustive-deps
);
};
@@ -574,7 +576,7 @@ export function useWorkerState(
async () => {
setData(await worker());
},
- dependencies,
+ dependencies, // eslint-disable-line @react-hook-utilities/exhaustive-deps
);
// start loading immediately
@@ -612,28 +614,34 @@ export const usePromisedState = (): [
ValuablePromise,
(_: T) => void,
] => {
- let resolve: (v: T) => void;
- const createPromise = (value?: T) => {
- const result: ValuablePromise = new Promise(r => {
- resolve = r;
- if (value) {
- r(value);
- }
- });
- result.value = value;
- return result;
- };
+ let resolve = useRef<(v: T) => void>();
+ const createPromise = useMemo(
+ () => (value?: T) => {
+ const result: ValuablePromise = new Promise(r => {
+ resolve.current = r;
+ if (value) {
+ r(value);
+ }
+ });
+ result.value = value;
+ return result;
+ },
+ [],
+ );
const [state, setState] = useState(createPromise);
return [
state,
- useCallback((newValue: T) => {
- if (!!newValue) {
- resolve(newValue);
- state.value = newValue;
- }
-
- setState(createPromise(newValue));
- }, []),
+ useCallback(
+ (newValue: T) => {
+ if (!!newValue) {
+ resolve.current!(newValue);
+ state.value = newValue;
+ }
+
+ setState(createPromise(newValue));
+ },
+ [createPromise, state.value],
+ ), // eslint-disable-line @react-hook-utilities/exhaustive-deps
];
};
diff --git a/tsconfig.json b/tsconfig.json
index cdbdbeb..fce9ac4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,6 +12,8 @@
},
"typedocOptions": {
"exclude": "src/__tests__/**/*",
- "out": "docs"
+ "out": "docs",
+ "mdPagesSourcePath": "pageDocs",
+ "theme": "markdown-pages"
}
}
diff --git a/yarn.lock b/yarn.lock
index 3ce89dd..6a26720 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -40,6 +40,16 @@
source-map "^0.5.0"
trim-right "^1.0.1"
+"@babel/generator@^7.6.2":
+ version "7.7.4"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369"
+ integrity sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==
+ dependencies:
+ "@babel/types" "^7.7.4"
+ jsesc "^2.5.1"
+ lodash "^4.17.13"
+ source-map "^0.5.0"
+
"@babel/helper-function-name@^7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
@@ -86,7 +96,12 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0":
+"@babel/parser@^7.0.0", "@babel/parser@^7.6.0", "@babel/parser@^7.6.2":
+ version "7.6.2"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1"
+ integrity sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.4.3":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b"
integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==
@@ -114,7 +129,22 @@
"@babel/parser" "^7.6.0"
"@babel/types" "^7.6.0"
-"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.6.0":
+"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0":
+ version "7.6.2"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.2.tgz#b0e2bfd401d339ce0e6c05690206d1e11502ce2c"
+ integrity sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ==
+ dependencies:
+ "@babel/code-frame" "^7.5.5"
+ "@babel/generator" "^7.6.2"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.4.4"
+ "@babel/parser" "^7.6.2"
+ "@babel/types" "^7.6.0"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.13"
+
+"@babel/traverse@^7.4.3", "@babel/traverse@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.0.tgz#389391d510f79be7ce2ddd6717be66d3fed4b516"
integrity sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==
@@ -138,6 +168,15 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
+"@babel/types@^7.7.4":
+ version "7.7.4"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193"
+ integrity sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.13"
+ to-fast-properties "^2.0.0"
+
"@cnakazawa/watch@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@@ -428,6 +467,9 @@
mkdirp "^0.5.1"
rimraf "^2.5.2"
+"@react-hook-utilities/eslint-plugin@./eslint-plugin":
+ version "0.1.0"
+
"@testing-library/react-hooks@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-2.0.1.tgz#1c3ec40882d0830df3078ddae0056fdf7366c81d"
@@ -470,6 +512,11 @@
dependencies:
"@babel/types" "^7.3.0"
+"@types/eslint-visitor-keys@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
+ integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -502,6 +549,11 @@
dependencies:
"@types/jest-diff" "*"
+"@types/json-schema@^7.0.3":
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
+ integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
+
"@types/minimatch@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -559,6 +611,49 @@
dependencies:
"@types/yargs-parser" "*"
+"@typescript-eslint/eslint-plugin@^2.3.1":
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz#c4cb103275e555e8a7e9b3d14c5951eb6d431e70"
+ integrity sha512-rT51fNLW0u3fnDGnAHVC5nu+Das+y2CpW10yqvf6/j5xbuUV3FxA3mBaIbM24CXODXjbgUznNb4Kg9XZOUxKAw==
+ dependencies:
+ "@typescript-eslint/experimental-utils" "2.10.0"
+ eslint-utils "^1.4.3"
+ functional-red-black-tree "^1.0.1"
+ regexpp "^3.0.0"
+ tsutils "^3.17.1"
+
+"@typescript-eslint/experimental-utils@2.10.0":
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz#8db1656cdfd3d9dcbdbf360b8274dea76f0b2c2c"
+ integrity sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ==
+ dependencies:
+ "@types/json-schema" "^7.0.3"
+ "@typescript-eslint/typescript-estree" "2.10.0"
+ eslint-scope "^5.0.0"
+
+"@typescript-eslint/parser@^2.3.1":
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.10.0.tgz#24b2e48384ab6d5a6121e4c4faf8892c79657ad3"
+ integrity sha512-wQNiBokcP5ZsTuB+i4BlmVWq6o+oAhd8en2eSm/EE9m7BgZUIfEeYFd6z3S+T7bgNuloeiHA1/cevvbBDLr98g==
+ dependencies:
+ "@types/eslint-visitor-keys" "^1.0.0"
+ "@typescript-eslint/experimental-utils" "2.10.0"
+ "@typescript-eslint/typescript-estree" "2.10.0"
+ eslint-visitor-keys "^1.1.0"
+
+"@typescript-eslint/typescript-estree@2.10.0":
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz#89cdabd5e8c774e9d590588cb42fb9afd14dcbd9"
+ integrity sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g==
+ dependencies:
+ debug "^4.1.1"
+ eslint-visitor-keys "^1.1.0"
+ glob "^7.1.6"
+ is-glob "^4.0.1"
+ lodash.unescape "4.0.1"
+ semver "^6.3.0"
+ tsutils "^3.17.1"
+
JSONStream@^1.0.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@@ -585,6 +680,11 @@ acorn-globals@^4.1.0:
acorn "^6.0.1"
acorn-walk "^6.0.1"
+acorn-jsx@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384"
+ integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==
+
acorn-walk@^6.0.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
@@ -600,12 +700,17 @@ acorn@^6.0.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e"
integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==
+acorn@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
+ integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==
+
add-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=
-ajv@^6.5.5:
+ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5:
version "6.10.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==
@@ -620,6 +725,13 @@ ansi-escapes@^3.0.0:
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+ansi-escapes@^4.2.1:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d"
+ integrity sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==
+ dependencies:
+ type-fest "^0.8.1"
+
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@@ -635,6 +747,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+ansi-regex@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
+ integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
+
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -700,6 +817,14 @@ array-ify@^1.0.0:
resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece"
integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=
+array-includes@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
+ integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.7.0"
+
array-unique@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
@@ -757,6 +882,18 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+babel-eslint@^10.0.3:
+ version "10.0.3"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
+ integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/parser" "^7.0.0"
+ "@babel/traverse" "^7.0.0"
+ "@babel/types" "^7.0.0"
+ eslint-visitor-keys "^1.0.0"
+ resolve "^1.12.0"
+
babel-jest@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54"
@@ -982,7 +1119,7 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
-chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2:
+chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -991,6 +1128,11 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
+chardet@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+ integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+
chownr@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6"
@@ -1011,6 +1153,18 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
+cli-cursor@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
+ integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
+ dependencies:
+ restore-cursor "^3.1.0"
+
+cli-width@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+ integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
+
cliui@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
@@ -1303,7 +1457,7 @@ cosmiconfig@^5.2.0, cosmiconfig@^5.2.1:
js-yaml "^3.13.1"
parse-json "^4.0.0"
-cross-spawn@^6.0.0:
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@@ -1380,7 +1534,7 @@ debug@^3.2.6:
dependencies:
ms "^2.1.1"
-debug@^4.1.0, debug@^4.1.1:
+debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
@@ -1469,6 +1623,20 @@ diff-sequences@^24.9.0:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
+doctrine@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+ integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
+ dependencies:
+ esutils "^2.0.2"
+
+doctrine@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+ integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+ dependencies:
+ esutils "^2.0.2"
+
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
@@ -1496,6 +1664,11 @@ emoji-regex@^7.0.1:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
end-of-stream@^1.1.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
@@ -1510,6 +1683,22 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
+es-abstract@^1.12.0, es-abstract@^1.15.0, es-abstract@^1.7.0:
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.2.tgz#4e874331645e9925edef141e74fc4bd144669d34"
+ integrity sha512-jYo/J8XU2emLXl3OLwfwtuFfuF2w6DYPs+xy9ZfVyPkDcrauu6LYrw/q2TyCtrbc/KUdCiC5e9UajRhgNkVopA==
+ dependencies:
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-inspect "^1.7.0"
+ object-keys "^1.1.1"
+ string.prototype.trimleft "^2.1.0"
+ string.prototype.trimright "^2.1.0"
+
es-abstract@^1.5.1:
version "1.14.2"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.14.2.tgz#7ce108fad83068c8783c3cdf62e504e084d8c497"
@@ -1535,6 +1724,15 @@ es-to-primitive@^1.2.0:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -1552,6 +1750,104 @@ escodegen@^1.9.1:
optionalDependencies:
source-map "~0.6.1"
+eslint-plugin-eslint-plugin@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.1.0.tgz#a7a00f15a886957d855feacaafee264f039e62d5"
+ integrity sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg==
+
+eslint-plugin-react-hooks@^2.1.1:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz#53e073961f1f5ccf8dd19558036c1fac8c29d99a"
+ integrity sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw==
+
+eslint-plugin-react@^7.14.3:
+ version "7.17.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz#a31b3e134b76046abe3cd278e7482bd35a1d12d7"
+ integrity sha512-ODB7yg6lxhBVMeiH1c7E95FLD4E/TwmFjltiU+ethv7KPdCwgiFuOZg9zNRHyufStTDLl/dEFqI2Q1VPmCd78A==
+ dependencies:
+ array-includes "^3.0.3"
+ doctrine "^2.1.0"
+ eslint-plugin-eslint-plugin "^2.1.0"
+ has "^1.0.3"
+ jsx-ast-utils "^2.2.3"
+ object.entries "^1.1.0"
+ object.fromentries "^2.0.1"
+ object.values "^1.1.0"
+ prop-types "^15.7.2"
+ resolve "^1.13.1"
+
+eslint-scope@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9"
+ integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-utils@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
+ integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+ dependencies:
+ eslint-visitor-keys "^1.1.0"
+
+eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
+ integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
+
+eslint@^6.5.0:
+ version "6.7.2"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.2.tgz#c17707ca4ad7b2d8af986a33feba71e18a9fecd1"
+ integrity sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ ajv "^6.10.0"
+ chalk "^2.1.0"
+ cross-spawn "^6.0.5"
+ debug "^4.0.1"
+ doctrine "^3.0.0"
+ eslint-scope "^5.0.0"
+ eslint-utils "^1.4.3"
+ eslint-visitor-keys "^1.1.0"
+ espree "^6.1.2"
+ esquery "^1.0.1"
+ esutils "^2.0.2"
+ file-entry-cache "^5.0.1"
+ functional-red-black-tree "^1.0.1"
+ glob-parent "^5.0.0"
+ globals "^12.1.0"
+ ignore "^4.0.6"
+ import-fresh "^3.0.0"
+ imurmurhash "^0.1.4"
+ inquirer "^7.0.0"
+ is-glob "^4.0.0"
+ js-yaml "^3.13.1"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.14"
+ minimatch "^3.0.4"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.3"
+ progress "^2.0.0"
+ regexpp "^2.0.1"
+ semver "^6.1.2"
+ strip-ansi "^5.2.0"
+ strip-json-comments "^3.0.1"
+ table "^5.2.3"
+ text-table "^0.2.0"
+ v8-compile-cache "^2.0.3"
+
+espree@^6.1.2:
+ version "6.1.2"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d"
+ integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==
+ dependencies:
+ acorn "^7.1.0"
+ acorn-jsx "^5.1.0"
+ eslint-visitor-keys "^1.1.0"
+
esprima@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
@@ -1562,7 +1858,21 @@ esprima@^4.0.0:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-estraverse@^4.2.0:
+esquery@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
+ integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
+ integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
+ dependencies:
+ estraverse "^4.1.0"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
@@ -1640,6 +1950,15 @@ extend@~3.0.2:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+external-editor@^3.0.3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
+ integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
+ dependencies:
+ chardet "^0.7.0"
+ iconv-lite "^0.4.24"
+ tmp "^0.0.33"
+
extglob@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
@@ -1674,7 +1993,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
-fast-levenshtein@~2.0.4:
+fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
@@ -1686,6 +2005,20 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"
+figures@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec"
+ integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
+ integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
+ dependencies:
+ flat-cache "^2.0.1"
+
fill-range@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
@@ -1726,6 +2059,20 @@ find-up@^4.0.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
+flat-cache@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
+ integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
+ dependencies:
+ flatted "^2.0.0"
+ rimraf "2.6.3"
+ write "1.0.3"
+
+flatted@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
+ integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
+
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -1786,6 +2133,11 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+functional-red-black-tree@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+ integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -1890,6 +2242,13 @@ gitconfiglocal@^1.0.0:
dependencies:
ini "^1.3.2"
+glob-parent@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2"
+ integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==
+ dependencies:
+ is-glob "^4.0.1"
+
glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
version "7.1.4"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
@@ -1902,6 +2261,18 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
+glob@^7.1.6:
+ version "7.1.6"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+ integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
global-dirs@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
@@ -1914,6 +2285,13 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+globals@^12.1.0:
+ version "12.3.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13"
+ integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==
+ dependencies:
+ type-fest "^0.8.1"
+
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
@@ -1980,6 +2358,11 @@ has-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
+has-symbols@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+ integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -2066,7 +2449,7 @@ husky@^3.0.5:
run-node "^1.0.0"
slash "^3.0.0"
-iconv-lite@0.4.24, iconv-lite@^0.4.4:
+iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -2080,6 +2463,11 @@ ignore-walk@^3.0.1:
dependencies:
minimatch "^3.0.4"
+ignore@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+ integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
@@ -2139,6 +2527,25 @@ ini@^1.3.2, ini@^1.3.4, ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+inquirer@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a"
+ integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==
+ dependencies:
+ ansi-escapes "^4.2.1"
+ chalk "^2.4.2"
+ cli-cursor "^3.1.0"
+ cli-width "^2.0.0"
+ external-editor "^3.0.3"
+ figures "^3.0.0"
+ lodash "^4.17.15"
+ mute-stream "0.0.8"
+ run-async "^2.2.0"
+ rxjs "^6.4.0"
+ string-width "^4.1.0"
+ strip-ansi "^5.1.0"
+ through "^2.3.6"
+
interpret@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
@@ -2241,6 +2648,11 @@ is-extendable@^1.0.1:
dependencies:
is-plain-object "^2.0.4"
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
is-finite@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
@@ -2260,11 +2672,23 @@ is-fullwidth-code-point@^2.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
is-generator-fn@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+is-glob@^4.0.0, is-glob@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+ integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+ dependencies:
+ is-extglob "^2.1.1"
+
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -2289,6 +2713,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+ integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
+
is-regex@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
@@ -2843,6 +3272,11 @@ json-schema@0.2.3:
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+ integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -2877,6 +3311,14 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
+jsx-ast-utils@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f"
+ integrity sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==
+ dependencies:
+ array-includes "^3.0.3"
+ object.assign "^4.1.0"
+
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -2916,7 +3358,7 @@ leven@^3.1.0:
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
-levn@~0.3.0:
+levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
@@ -3008,12 +3450,17 @@ lodash.templatesettings@^4.0.0:
dependencies:
lodash._reinterpolate "^3.0.0"
+lodash.unescape@4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
+ integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
+
lodash@4.17.14:
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
-lodash@^4.14.14, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.2.1:
+lodash@^4.14.14, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.2.1:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -3167,6 +3614,11 @@ mime-types@^2.1.12, mime-types@~2.1.19:
dependencies:
mime-db "1.40.0"
+mimic-fn@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+ integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
minimatch@^3.0.0, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -3242,6 +3694,11 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+mute-stream@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
+ integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+
nan@^2.12.1:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
@@ -3414,7 +3871,12 @@ object-inspect@^1.6.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==
-object-keys@^1.0.12, object-keys@^1.1.1:
+object-inspect@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+ integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -3426,6 +3888,36 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.entries@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
+ integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+object.fromentries@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704"
+ integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.15.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
object.getownpropertydescriptors@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
@@ -3441,6 +3933,16 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
+object.values@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+ integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -3448,6 +3950,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
+onetime@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5"
+ integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==
+ dependencies:
+ mimic-fn "^2.1.0"
+
opencollective-postinstall@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89"
@@ -3473,12 +3982,24 @@ optionator@^0.8.1:
type-check "~0.3.2"
wordwrap "~1.0.0"
+optionator@^0.8.3:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+ integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.6"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ word-wrap "~1.2.3"
+
os-homedir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
-os-tmpdir@^1.0.0:
+os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@@ -3743,7 +4264,7 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-progress@^2.0.3:
+progress@^2.0.0, progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@@ -3756,7 +4277,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.3"
-prop-types@^15.6.2:
+prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -3969,6 +4490,16 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
+regexpp@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
+ integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
+
+regexpp@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e"
+ integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==
+
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -4089,18 +4620,40 @@ resolve@1.1.7:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
-resolve@1.x, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2:
+resolve@1.x, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2:
version "1.12.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
dependencies:
path-parse "^1.0.6"
+resolve@^1.13.1:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16"
+ integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==
+ dependencies:
+ path-parse "^1.0.6"
+
+restore-cursor@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
+ integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+ dependencies:
+ onetime "^5.1.0"
+ signal-exit "^3.0.2"
+
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+rimraf@2.6.3:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+ integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+ dependencies:
+ glob "^7.1.3"
+
rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@@ -4113,11 +4666,25 @@ rsvp@^4.8.4:
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
+ dependencies:
+ is-promise "^2.1.0"
+
run-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e"
integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==
+rxjs@^6.4.0:
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a"
+ integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==
+ dependencies:
+ tslib "^1.9.0"
+
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
@@ -4183,7 +4750,7 @@ semver@6.2.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db"
integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==
-semver@^6.0.0, semver@^6.2.0:
+semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@@ -4249,6 +4816,15 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+slice-ansi@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
+ integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+ dependencies:
+ ansi-styles "^3.2.0"
+ astral-regex "^1.0.0"
+ is-fullwidth-code-point "^2.0.0"
+
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -4432,7 +5008,16 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
-string.prototype.trimleft@^2.0.0:
+string-width@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
+ integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.0"
+
+string.prototype.trimleft@^2.0.0, string.prototype.trimleft@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
@@ -4440,7 +5025,7 @@ string.prototype.trimleft@^2.0.0:
define-properties "^1.1.3"
function-bind "^1.1.1"
-string.prototype.trimright@^2.0.0:
+string.prototype.trimright@^2.0.0, string.prototype.trimright@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
@@ -4483,6 +5068,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
+strip-ansi@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
+ integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
+ dependencies:
+ ansi-regex "^5.0.0"
+
strip-bom@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -4512,6 +5104,11 @@ strip-indent@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
+strip-json-comments@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
+ integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==
+
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -4536,6 +5133,16 @@ symbol-tree@^3.2.2:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+table@^5.2.3:
+ version "5.4.6"
+ resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
+ integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+ dependencies:
+ ajv "^6.10.2"
+ lodash "^4.17.14"
+ slice-ansi "^2.1.0"
+ string-width "^3.0.0"
+
tar@^4:
version "4.4.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.11.tgz#7ac09801445a3cf74445ed27499136b5240ffb73"
@@ -4582,6 +5189,11 @@ text-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.0.0.tgz#43eabd1b495482fae4a2bf65e5f56c29f69220f6"
integrity sha512-F91ZqLgvi1E0PdvmxMgp+gcf6q8fMH7mhdwWfzXnl1k+GbpQDmi8l7DzLC5JTASKbwpY3TfxajAUzAXcv2NmsQ==
+text-table@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+ integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+
throat@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
@@ -4602,11 +5214,18 @@ through2@^3.0.0:
dependencies:
readable-stream "2 || 3"
-through@2, "through@>=2.2.7 <3":
+through@2, "through@>=2.2.7 <3", through@^2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+ dependencies:
+ os-tmpdir "~1.0.2"
+
tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
@@ -4701,6 +5320,18 @@ ts-jest@^24.1.0:
semver "^5.5"
yargs-parser "10.x"
+tslib@^1.8.1, tslib@^1.9.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
+ integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
+
+tsutils@^3.17.1:
+ version "3.17.1"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
+ integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==
+ dependencies:
+ tslib "^1.8.1"
+
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -4725,6 +5356,11 @@ type-fest@^0.6.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+type-fest@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+ integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
typedoc-default-themes@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.7.2.tgz#1e9896f920b58e6da0bba9d7e643738d02405a5a"
@@ -4735,6 +5371,11 @@ typedoc-default-themes@^0.7.2:
lunr "^2.3.8"
underscore "^1.9.1"
+typedoc-plugin-markdown-pages@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown-pages/-/typedoc-plugin-markdown-pages-0.3.0.tgz#17f0f9ff3aa312e9ed142eb15ea216ea95d5fb5f"
+ integrity sha512-QVWs6eL0BEOiRgJm6wRRgE8kESFv85HlefLv14ZbMM2VQ1HqErVeT9ukbhkYMznsD/juAsX3Xc5M+Ju7/ZDq4Q==
+
typedoc@^0.16.9:
version "0.16.9"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.16.9.tgz#d6f46f4dea7d3362029927a92981efdf896f435b"
@@ -4833,6 +5474,11 @@ uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
+v8-compile-cache@^2.0.3:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
+ integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
+
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -4918,6 +5564,11 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
+word-wrap@~1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+ integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
@@ -4951,6 +5602,13 @@ write-file-atomic@2.4.1:
imurmurhash "^0.1.4"
signal-exit "^3.0.2"
+write@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
+ integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
+ dependencies:
+ mkdirp "^0.5.1"
+
ws@^5.2.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"