mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Add ESLint rule for React Hooks
This commit is contained in:
parent
acb4899637
commit
ddbfe2ed50
48
packages/eslint-plugin-react-hooks/README.md
Normal file
48
packages/eslint-plugin-react-hooks/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# `eslint-plugin-react-hooks`
|
||||
|
||||
This ESLint plugin enforces the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html).
|
||||
|
||||
It is a part of the [Hooks proposal](https://reactjs.org/docs/hooks-intro.html) for React.
|
||||
|
||||
## Experimental Status
|
||||
|
||||
This is an experimental release and is intended to be used for testing the Hooks proposal with React 16.7 alpha. The exact heuristics it uses may be adjusted.
|
||||
|
||||
The [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) documentation contains a link to the technical RFC. Please leave a comment on the RFC if you have concerns or ideas about how this plugin should work.
|
||||
|
||||
## Installation
|
||||
|
||||
**Note: If you're using Create React App, please wait for a corresponding experimental release of `react-scripts` that includes this rule instead of adding it directly.**
|
||||
|
||||
Assuming you already have ESLint installed, run:
|
||||
|
||||
```sh
|
||||
# npm
|
||||
npm install eslint-plugin-react-hooks@next --save-dev
|
||||
|
||||
# yarn
|
||||
yarn add eslint-plugin-react-hooks@next --dev
|
||||
```
|
||||
|
||||
Then add it to your ESLint configuration:
|
||||
|
||||
```js
|
||||
{
|
||||
"plugins": [
|
||||
// ...
|
||||
"react-hooks"
|
||||
],
|
||||
"rules": {
|
||||
// ...
|
||||
"react-hooks/rules-of-hooks": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Valid and Invalid Examples
|
||||
|
||||
Please refer to the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) documentation and the [Hooks FAQ](https://reactjs.org/docs/hooks-faq.html#what-exactly-do-the-lint-rules-enforce) to learn more about this rule.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -0,0 +1,632 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ESLintTester = require('eslint').RuleTester;
|
||||
const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks');
|
||||
const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['rules-of-hooks'];
|
||||
|
||||
ESLintTester.setDefaultConfig({
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
const eslintTester = new ESLintTester();
|
||||
eslintTester.run('react-hooks', ReactHooksESLintRule, {
|
||||
valid: [
|
||||
`
|
||||
// Valid because components can use hooks.
|
||||
function ComponentWithHook() {
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because components can use hooks.
|
||||
function createComponentWithHook() {
|
||||
return function ComponentWithHook() {
|
||||
useHook();
|
||||
};
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can use hooks.
|
||||
function useHookWithHook() {
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can use hooks.
|
||||
function createHook() {
|
||||
return function useHookWithHook() {
|
||||
useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because components can call functions.
|
||||
function ComponentWithNormalFunction() {
|
||||
doSomething();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function normalFunctionWithNormalFunction() {
|
||||
doSomething();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function normalFunctionWithConditionalFunction() {
|
||||
if (cond) {
|
||||
doSomething();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function functionThatStartsWithUseButIsntAHook() {
|
||||
if (cond) {
|
||||
userFetch();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid although unconditional return doesn't make sense and would fail other rules.
|
||||
// We could make it invalid but it doesn't matter.
|
||||
function useUnreachable() {
|
||||
return;
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() { useState(); }
|
||||
const whatever = function useHook() { useState(); };
|
||||
const useHook1 = () => { useState(); };
|
||||
let useHook2 = () => useState();
|
||||
useHook2 = () => { useState(); };
|
||||
({useHook: () => { useState(); }});
|
||||
({useHook() { useState(); }});
|
||||
const {useHook = () => { useState(); }} = {};
|
||||
({useHook = () => { useState(); }} = {});
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
useHook1();
|
||||
useHook2();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function createHook() {
|
||||
return function useHook() {
|
||||
useHook1();
|
||||
useHook2();
|
||||
};
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
useState() && a;
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
return useHook1() + useHook2();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
return useHook1(useHook2());
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because classes can call functions.
|
||||
// We don't consider these to be hooks.
|
||||
class C {
|
||||
m() {
|
||||
this.useHook();
|
||||
super.useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid.
|
||||
// We *could* make this invalid if we want, but it creates false positives
|
||||
// (see the FooStore case).
|
||||
class C {
|
||||
m() {
|
||||
This.useHook();
|
||||
Super.useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid although we *could* consider these invalid.
|
||||
// But it doesn't bring much benefit since it's an immediate runtime error anyway.
|
||||
// So might as well allow it.
|
||||
Hook.use();
|
||||
Hook._use();
|
||||
Hook.useState();
|
||||
Hook._useState();
|
||||
Hook.use42();
|
||||
Hook.useHook();
|
||||
Hook.use_hook();
|
||||
`,
|
||||
`
|
||||
// Valid -- this is a regression test.
|
||||
jest.useFakeTimers();
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
})
|
||||
`,
|
||||
`
|
||||
// Valid because that's a false positive we've seen quite a bit.
|
||||
// This is a regression test.
|
||||
class Foo extends Component {
|
||||
render() {
|
||||
if (cond) {
|
||||
FooStore.useFeatureFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid because we found this to be a common pattern
|
||||
// for feature flag checks in existing components.
|
||||
// We *could* make it invalid but that produces quite a few false positives.
|
||||
// Why does it make sense to ignore it? Firstly, because using
|
||||
// hooks in a class would cause a runtime error anyway.
|
||||
// But why don't we care about the same kind of false positive in a functional
|
||||
// component? Because even if it was a false positive, it would be confusing
|
||||
// anyway. So it might make sense to rename a feature flag check in that case.
|
||||
class ClassComponentWithFeatureFlag extends React.Component {
|
||||
render() {
|
||||
if (foo) {
|
||||
useFeatureFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid because we don't check for hooks in classes.
|
||||
// See ClassComponentWithFeatureFlag for rationale.
|
||||
// We *could* make it invalid if we don't regress that false positive.
|
||||
class ClassComponentWithHook extends React.Component {
|
||||
render() {
|
||||
React.useState();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid.
|
||||
// These are variations capturing the current heuristic--
|
||||
// we only allow hooks in PascalCase, useFoo functions,
|
||||
// or classes (due to common false positives and because they error anyway).
|
||||
// We *could* make some of these invalid.
|
||||
// They probably don't matter much.
|
||||
(class {useHook = () => { useState(); }});
|
||||
(class {useHook() { useState(); }});
|
||||
(class {h = () => { useState(); }});
|
||||
(class {i() { useState(); }});
|
||||
`,
|
||||
`
|
||||
// Currently valid although we *could* consider these invalid.
|
||||
// It doesn't make a lot of difference because it would crash early.
|
||||
use();
|
||||
_use();
|
||||
useState();
|
||||
_useState();
|
||||
use42();
|
||||
useHook();
|
||||
use_hook();
|
||||
React.useState();
|
||||
`,
|
||||
`
|
||||
// Regression test for the popular "history" library
|
||||
const {createHistory, useBasename} = require('history-2.1.2');
|
||||
const browserHistory = useBasename(createHistory)({
|
||||
basename: '/',
|
||||
});
|
||||
`,
|
||||
`
|
||||
// Regression test for some internal code.
|
||||
// This shows how the "callback rule" is more relaxed,
|
||||
// and doesn't kick in unless we're confident we're in
|
||||
// a component or a hook.
|
||||
function makeListener(instance) {
|
||||
each(pixelsWithInferredEvents, pixel => {
|
||||
if (useExtendedSelector(pixel.id) && extendedButton) {
|
||||
foo();
|
||||
}
|
||||
});
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Regression test for incorrectly flagged valid code.
|
||||
function RegressionTest() {
|
||||
const foo = cond ? a : b;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function createComponent() {
|
||||
return function ComponentWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function createHook() {
|
||||
return function useHookWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithTernaryHook() {
|
||||
cond ? useTernaryHook() : null;
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useTernaryHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function ComponentWithHookInsideCallback() {
|
||||
useEffect(() => {
|
||||
useHookInsideCallback();
|
||||
});
|
||||
}
|
||||
`,
|
||||
errors: [genericError('useHookInsideCallback')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function createComponent() {
|
||||
return function ComponentWithHookInsideCallback() {
|
||||
useEffect(() => {
|
||||
useHookInsideCallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [genericError('useHookInsideCallback')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function ComponentWithHookInsideCallback() {
|
||||
function handleClick() {
|
||||
useState();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'handleClick')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function createComponent() {
|
||||
return function ComponentWithHookInsideCallback() {
|
||||
function handleClick() {
|
||||
useState();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'handleClick')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithHookInsideLoop() {
|
||||
while (cond) {
|
||||
useHookInsideLoop();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [loopError('useHookInsideLoop')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function renderItem() {
|
||||
useState();
|
||||
}
|
||||
|
||||
function List(props) {
|
||||
return props.items.map(renderItem);
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'renderItem')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Currently invalid because it violates the convention and removes the "taint"
|
||||
// from a hook. We *could* make it valid to avoid some false positives but let's
|
||||
// ensure that we don't break the "renderItem" and "normalFunctionWithConditionalHook"
|
||||
// cases which must remain invalid.
|
||||
function normalFunctionWithHook() {
|
||||
useHookInsideNormalFunction();
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
functionError('useHookInsideNormalFunction', 'normalFunctionWithHook'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function normalFunctionWithConditionalHook() {
|
||||
if (cond) {
|
||||
useHookInsideNormalFunction();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
functionError(
|
||||
'useHookInsideNormalFunction',
|
||||
'normalFunctionWithConditionalHook'
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookInLoops() {
|
||||
while (a) {
|
||||
useHook1();
|
||||
if (b) return;
|
||||
useHook2();
|
||||
}
|
||||
while (c) {
|
||||
useHook3();
|
||||
if (d) return;
|
||||
useHook4();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
loopError('useHook1'),
|
||||
loopError('useHook2'),
|
||||
loopError('useHook3'),
|
||||
loopError('useHook4'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookInLoops() {
|
||||
while (a) {
|
||||
useHook1();
|
||||
if (b) continue;
|
||||
useHook2();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
loopError('useHook1'),
|
||||
|
||||
// NOTE: Small imprecision in error reporting due to caching means we
|
||||
// have a conditional error here instead of a loop error. However,
|
||||
// we will always get an error so this is acceptable.
|
||||
conditionalError('useHook2', true),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useLabeledBlock() {
|
||||
label: {
|
||||
if (a) break label;
|
||||
useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Currently invalid.
|
||||
// These are variations capturing the current heuristic--
|
||||
// we only allow hooks in PascalCase or useFoo functions.
|
||||
// We *could* make some of these valid. But before doing it,
|
||||
// consider specific cases documented above that contain reasoning.
|
||||
function a() { useState(); }
|
||||
const whatever = function b() { useState(); };
|
||||
const c = () => { useState(); };
|
||||
let d = () => useState();
|
||||
e = () => { useState(); };
|
||||
({f: () => { useState(); }});
|
||||
({g() { useState(); }});
|
||||
const {j = () => { useState(); }} = {};
|
||||
({k = () => { useState(); }} = {});
|
||||
`,
|
||||
errors: [
|
||||
functionError('useState', 'a'),
|
||||
functionError('useState', 'b'),
|
||||
functionError('useState', 'c'),
|
||||
functionError('useState', 'd'),
|
||||
functionError('useState', 'e'),
|
||||
functionError('useState', 'f'),
|
||||
functionError('useState', 'g'),
|
||||
functionError('useState', 'j'),
|
||||
functionError('useState', 'k'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (a) return;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (a) return;
|
||||
if (b) {
|
||||
console.log('true');
|
||||
} else {
|
||||
console.log('false');
|
||||
}
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (b) {
|
||||
console.log('true');
|
||||
} else {
|
||||
console.log('false');
|
||||
}
|
||||
if (a) return;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
a && useHook1();
|
||||
b && useHook2();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useHook1'), conditionalError('useHook2')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
try {
|
||||
f();
|
||||
useState();
|
||||
} catch {}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
// NOTE: This is an error since `f()` could possibly throw.
|
||||
conditionalError('useState'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function conditionalError(hook, hasPreviousFinalizer = false) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" is called conditionally. React Hooks must be ` +
|
||||
'called in the exact same order in every component render.' +
|
||||
(hasPreviousFinalizer
|
||||
? ' Did you accidentally call a React Hook after an early return?'
|
||||
: ''),
|
||||
};
|
||||
}
|
||||
|
||||
function loopError(hook) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" may be executed more than once. Possibly ` +
|
||||
'because it is called in a loop. React Hooks must be called in the ' +
|
||||
'exact same order in every component render.',
|
||||
};
|
||||
}
|
||||
|
||||
function functionError(hook, fn) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" is called in function "${fn}" which is neither ` +
|
||||
'a React function component or a custom React Hook function.',
|
||||
};
|
||||
}
|
||||
|
||||
function genericError(hook) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" cannot be called inside a callback. React Hooks ` +
|
||||
'must be called in a React function component or a custom React ' +
|
||||
'Hook function.',
|
||||
};
|
||||
}
|
||||
10
packages/eslint-plugin-react-hooks/index.js
Normal file
10
packages/eslint-plugin-react-hooks/index.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./src/index');
|
||||
9
packages/eslint-plugin-react-hooks/npm/index.js
Normal file
9
packages/eslint-plugin-react-hooks/npm/index.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
// TODO: this doesn't make sense for an ESLint rule.
|
||||
// We need to fix our build process to not create bundles for "raw" packages like this.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/eslint-plugin-react-hooks.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');
|
||||
}
|
||||
29
packages/eslint-plugin-react-hooks/package.json
Normal file
29
packages/eslint-plugin-react-hooks/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"description": "ESLint rules for React Hooks",
|
||||
"version": "0.0.0",
|
||||
"repository": "facebook/react",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"index.js",
|
||||
"cjs"
|
||||
],
|
||||
"keywords": [
|
||||
"eslint",
|
||||
"eslint-plugin",
|
||||
"eslintplugin",
|
||||
"react"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/facebook/react/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"homepage": "https://reactjs.org/",
|
||||
"peerDependencies": {
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
}
|
||||
545
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
Normal file
545
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
||||
* character to exclude identifiers like "user".
|
||||
*/
|
||||
|
||||
function isHookName(s) {
|
||||
return /^use[A-Z0-9].*$/.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* We consider hooks to be a hook name identifier or a member expression
|
||||
* containing a hook name.
|
||||
*/
|
||||
|
||||
function isHook(node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return isHookName(node.name);
|
||||
} else if (
|
||||
node.type === 'MemberExpression' &&
|
||||
!node.computed &&
|
||||
isHook(node.property)
|
||||
) {
|
||||
// Only consider React.useFoo() to be namespace hooks for now to avoid false positives.
|
||||
// We can expand this check later.
|
||||
const obj = node.object;
|
||||
return obj.type === 'Identifier' && obj.name === 'React';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a React component name. React component names must
|
||||
* always start with a non-lowercase letter. So `MyComponent` or `_MyComponent`
|
||||
* are valid component names for instance.
|
||||
*/
|
||||
|
||||
function isComponentName(node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return !/^[a-z]/.test(node.name);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isInsideComponentOrHook(node) {
|
||||
while (node) {
|
||||
const functionName = getFunctionName(node);
|
||||
if (functionName) {
|
||||
if (isComponentName(functionName) || isHook(functionName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
const codePathReactHooksMapStack = [];
|
||||
const codePathSegmentStack = [];
|
||||
return {
|
||||
// Maintain code segment path stack as we traverse.
|
||||
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
|
||||
onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
|
||||
|
||||
// Maintain code path stack as we traverse.
|
||||
onCodePathStart: () => codePathReactHooksMapStack.push(new Map()),
|
||||
|
||||
// Process our code path.
|
||||
//
|
||||
// Everything is ok if all React Hooks are both reachable from the initial
|
||||
// segment and reachable from every final segment.
|
||||
onCodePathEnd(codePath, codePathNode) {
|
||||
const reactHooksMap = codePathReactHooksMapStack.pop();
|
||||
if (reactHooksMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// All of the segments which are cyclic are recorded in this set.
|
||||
const cyclic = new Set();
|
||||
|
||||
/**
|
||||
* Count the number of code paths from the start of the function to this
|
||||
* segment. For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* if (condition) {
|
||||
* // Segment 1
|
||||
* } else {
|
||||
* // Segment 2
|
||||
* }
|
||||
* // Segment 3
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Segments 1 and 2 have one path to the beginning of `MyComponent` and
|
||||
* segment 3 has two paths to the beginning of `MyComponent` since we
|
||||
* could have either taken the path of segment 1 or segment 2.
|
||||
*
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsFromStart(segment) {
|
||||
const {cache} = countPathsFromStart;
|
||||
let paths = cache.get(segment.id);
|
||||
|
||||
// If `paths` is null then we've found a cycle! Add it to `cyclic` and
|
||||
// any other segments which are a part of this cycle.
|
||||
if (paths === null) {
|
||||
if (cyclic.has(segment.id)) {
|
||||
return 0;
|
||||
} else {
|
||||
cyclic.add(segment.id);
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
countPathsFromStart(prevSegment);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// We have a cached `paths`. Return it.
|
||||
if (paths !== undefined) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Compute `paths` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (segment.prevSegments.length === 0) {
|
||||
paths = 1;
|
||||
} else {
|
||||
paths = 0;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
paths += countPathsFromStart(prevSegment);
|
||||
}
|
||||
}
|
||||
cache.set(segment.id, paths);
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of code paths from this segment to the end of the
|
||||
* function. For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* // Segment 1
|
||||
* if (condition) {
|
||||
* // Segment 2
|
||||
* } else {
|
||||
* // Segment 3
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Segments 2 and 3 have one path to the end of `MyComponent` and
|
||||
* segment 1 has two paths to the end of `MyComponent` since we could
|
||||
* either take the path of segment 1 or segment 2.
|
||||
*
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsToEnd(segment) {
|
||||
const {cache} = countPathsToEnd;
|
||||
let paths = cache.get(segment.id);
|
||||
|
||||
// If `paths` is null then we've found a cycle! Add it to `cyclic` and
|
||||
// any other segments which are a part of this cycle.
|
||||
if (paths === null) {
|
||||
if (cyclic.has(segment.id)) {
|
||||
return 0;
|
||||
} else {
|
||||
cyclic.add(segment.id);
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
countPathsToEnd(nextSegment);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// We have a cached `paths`. Return it.
|
||||
if (paths !== undefined) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Compute `paths` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (segment.nextSegments.length === 0) {
|
||||
paths = 1;
|
||||
} else {
|
||||
paths = 0;
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
paths += countPathsToEnd(nextSegment);
|
||||
}
|
||||
}
|
||||
cache.set(segment.id, paths);
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the shortest path length to the start of a code path.
|
||||
* For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* if (condition) {
|
||||
* // Segment 1
|
||||
* }
|
||||
* // Segment 2
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* There is only one path from segment 1 to the code path start. Its
|
||||
* length is one so that is the shortest path.
|
||||
*
|
||||
* There are two paths from segment 2 to the code path start. One
|
||||
* through segment 1 with a length of two and another directly to the
|
||||
* start with a length of one. The shortest path has a length of one
|
||||
* so we would return that.
|
||||
*/
|
||||
|
||||
function shortestPathLengthToStart(segment) {
|
||||
const {cache} = shortestPathLengthToStart;
|
||||
let length = cache.get(segment.id);
|
||||
|
||||
// If `length` is null then we found a cycle! Return infinity since
|
||||
// the shortest path is definitely not the one where we looped.
|
||||
if (length === null) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
// We have a cached `length`. Return it.
|
||||
if (length !== undefined) {
|
||||
return length;
|
||||
}
|
||||
|
||||
// Compute `length` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (segment.prevSegments.length === 0) {
|
||||
length = 1;
|
||||
} else {
|
||||
length = Infinity;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
const prevLength = shortestPathLengthToStart(prevSegment);
|
||||
if (prevLength < length) {
|
||||
length = prevLength;
|
||||
}
|
||||
}
|
||||
length += 1;
|
||||
}
|
||||
cache.set(segment.id, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
countPathsFromStart.cache = new Map();
|
||||
countPathsToEnd.cache = new Map();
|
||||
shortestPathLengthToStart.cache = new Map();
|
||||
|
||||
// Count all code paths to the end of our component/hook. Also primes
|
||||
// the `countPathsToEnd` cache.
|
||||
const allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);
|
||||
|
||||
// Gets the function name for our code path. If the function name is
|
||||
// `undefined` then we know either that we have an anonymous function
|
||||
// expression or our code path is not in a function. In both cases we
|
||||
// will want to error since neither are React functional components or
|
||||
// hook functions.
|
||||
const codePathFunctionName = getFunctionName(codePathNode);
|
||||
|
||||
// This is a valid code path for React hooks if we are direcly in a React
|
||||
// functional component or we are in a hook function.
|
||||
const isSomewhereInsideComponentOrHook = isInsideComponentOrHook(
|
||||
codePathNode,
|
||||
);
|
||||
const isDirectlyInsideComponentOrHook = codePathFunctionName
|
||||
? isComponentName(codePathFunctionName) ||
|
||||
isHook(codePathFunctionName)
|
||||
: false;
|
||||
|
||||
// Compute the earliest finalizer level using information from the
|
||||
// cache. We expect all reachable final segments to have a cache entry
|
||||
// after calling `visitSegment()`.
|
||||
let shortestFinalPathLength = Infinity;
|
||||
for (const finalSegment of codePath.finalSegments) {
|
||||
if (!finalSegment.reachable) {
|
||||
continue;
|
||||
}
|
||||
const length = shortestPathLengthToStart(finalSegment);
|
||||
if (length < shortestFinalPathLength) {
|
||||
shortestFinalPathLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all React Hooks pass our lint invariants. Log warnings
|
||||
// if not.
|
||||
for (const [segment, reactHooks] of reactHooksMap) {
|
||||
// NOTE: We could report here that the hook is not reachable, but
|
||||
// that would be redundant with more general "no unreachable"
|
||||
// lint rules.
|
||||
if (!segment.reachable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there are any final segments with a shorter path to start then
|
||||
// we possibly have an early return.
|
||||
//
|
||||
// If our segment is a final segment itself then siblings could
|
||||
// possibly be early returns.
|
||||
const possiblyHasEarlyReturn =
|
||||
segment.nextSegments.length === 0
|
||||
? shortestFinalPathLength <= shortestPathLengthToStart(segment)
|
||||
: shortestFinalPathLength < shortestPathLengthToStart(segment);
|
||||
|
||||
// Count all the paths from the start of our code path to the end of
|
||||
// our code path that go _through_ this segment. The critical piece
|
||||
// of this is _through_. If we just call `countPathsToEnd(segment)`
|
||||
// then we neglect that we may have gone through multiple paths to get
|
||||
// to this point! Consider:
|
||||
//
|
||||
// ```js
|
||||
// function MyComponent() {
|
||||
// if (a) {
|
||||
// // Segment 1
|
||||
// } else {
|
||||
// // Segment 2
|
||||
// }
|
||||
// // Segment 3
|
||||
// if (b) {
|
||||
// // Segment 4
|
||||
// } else {
|
||||
// // Segment 5
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// In this component we have four code paths:
|
||||
//
|
||||
// 1. `a = true; b = true`
|
||||
// 2. `a = true; b = false`
|
||||
// 3. `a = false; b = true`
|
||||
// 4. `a = false; b = false`
|
||||
//
|
||||
// From segment 3 there are two code paths to the end through segment
|
||||
// 4 and segment 5. However, we took two paths to get here through
|
||||
// segment 1 and segment 2.
|
||||
//
|
||||
// If we multiply the paths from start (two) by the paths to end (two)
|
||||
// for segment 3 we get four. Which is our desired count.
|
||||
const pathsFromStartToEnd =
|
||||
countPathsFromStart(segment) * countPathsToEnd(segment);
|
||||
|
||||
// Is this hook a part of a cyclic segment?
|
||||
const cycled = cyclic.has(segment.id);
|
||||
|
||||
for (const hook of reactHooks) {
|
||||
// Report an error if a hook may be called more then once.
|
||||
if (cycled) {
|
||||
context.report(
|
||||
hook,
|
||||
`React Hook "${context.getSource(hook)}" may be executed ` +
|
||||
'more than once. Possibly because it is called in a loop. ' +
|
||||
'React Hooks must be called in the exact same order in ' +
|
||||
'every component render.',
|
||||
);
|
||||
}
|
||||
|
||||
// If this is not a valid code path for React hooks then we need to
|
||||
// log a warning for every hook in this code path.
|
||||
//
|
||||
// Pick a special message depending on the scope this hook was
|
||||
// called in.
|
||||
if (isDirectlyInsideComponentOrHook) {
|
||||
// Report an error if a hook does not reach all finalizing code
|
||||
// path segments.
|
||||
//
|
||||
// Special case when we think there might be an early return.
|
||||
if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
|
||||
context.report(
|
||||
hook,
|
||||
`React Hook "${context.getSource(hook)}" is called ` +
|
||||
'conditionally. React Hooks must be called in the exact ' +
|
||||
'same order in every component render.' +
|
||||
(possiblyHasEarlyReturn
|
||||
? ' Did you accidentally call a React Hook after an' +
|
||||
' early return?'
|
||||
: ''),
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
codePathNode.parent &&
|
||||
(codePathNode.parent.type === 'MethodDefinition' ||
|
||||
codePathNode.parent.type === 'ClassProperty') &&
|
||||
codePathNode.parent.value === codePathNode
|
||||
) {
|
||||
// Ignore class methods for now because they produce too many
|
||||
// false positives due to feature flag checks. We're less
|
||||
// sensitive to them in classes because hooks would produce
|
||||
// runtime errors in classes anyway, and because a use*()
|
||||
// call in a class, if it works, is unambigously *not* a hook.
|
||||
} else if (codePathFunctionName) {
|
||||
// Custom message if we found an invalid function name.
|
||||
context.report(
|
||||
hook,
|
||||
`React Hook "${context.getSource(hook)}" is called in ` +
|
||||
`function "${context.getSource(codePathFunctionName)}" ` +
|
||||
'which is neither a React function component or a custom ' +
|
||||
'React Hook function.',
|
||||
);
|
||||
} else if (codePathNode.type === 'Program') {
|
||||
// For now, ignore if it's in top level scope.
|
||||
// We could warn here but there are false positives related
|
||||
// configuring libraries like `history`.
|
||||
} else {
|
||||
// Assume in all other cases the user called a hook in some
|
||||
// random function callback. This should usually be true for
|
||||
// anonymous function expressions. Hopefully this is clarifying
|
||||
// enough in the common case that the incorrect message in
|
||||
// uncommon cases doesn't matter.
|
||||
if (isSomewhereInsideComponentOrHook) {
|
||||
context.report(
|
||||
hook,
|
||||
`React Hook "${context.getSource(hook)}" cannot be called ` +
|
||||
'inside a callback. React Hooks must be called in a ' +
|
||||
'React function component or a custom React Hook function.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Missed opportunity...We could visit all `Identifier`s instead of all
|
||||
// `CallExpression`s and check that _every use_ of a hook name is valid.
|
||||
// But that gets complicated and enters type-system territory, so we're
|
||||
// only being strict about hook calls for now.
|
||||
CallExpression(node) {
|
||||
if (isHook(node.callee)) {
|
||||
// Add the hook node to a map keyed by the code path segment. We will
|
||||
// do full code path analysis at the end of our code path.
|
||||
const reactHooksMap = last(codePathReactHooksMapStack);
|
||||
const codePathSegment = last(codePathSegmentStack);
|
||||
let reactHooks = reactHooksMap.get(codePathSegment);
|
||||
if (!reactHooks) {
|
||||
reactHooks = [];
|
||||
reactHooksMap.set(codePathSegment, reactHooks);
|
||||
}
|
||||
reactHooks.push(node.callee);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets tbe static name of a function AST node. For function declarations it is
|
||||
* easy. For anonymous function expressions it is much harder. If you search for
|
||||
* `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
|
||||
* where JS gives anonymous function expressions names. We roughly detect the
|
||||
* same AST nodes with some exceptions to better fit our usecase.
|
||||
*/
|
||||
|
||||
function getFunctionName(node) {
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
(node.type === 'FunctionExpression' && node.id)
|
||||
) {
|
||||
// function useHook() {}
|
||||
// const whatever = function useHook() {};
|
||||
//
|
||||
// Function declaration or function expression names win over any
|
||||
// assignment statements or other renames.
|
||||
return node.id;
|
||||
} else if (
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (
|
||||
node.parent.type === 'VariableDeclarator' &&
|
||||
node.parent.init === node
|
||||
) {
|
||||
// const useHook = () => {};
|
||||
return node.parent.id;
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentExpression' &&
|
||||
node.parent.right === node &&
|
||||
node.parent.operator === '='
|
||||
) {
|
||||
// useHook = () => {};
|
||||
return node.parent.left;
|
||||
} else if (
|
||||
node.parent.type === 'Property' &&
|
||||
node.parent.value === node &&
|
||||
!node.parent.computed
|
||||
) {
|
||||
// {useHook: () => {}}
|
||||
// {useHook() {}}
|
||||
return node.parent.key;
|
||||
|
||||
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
|
||||
// here to be pedantic. However, hooks in a class are an anti-pattern. So
|
||||
// we don't allow it to error early.
|
||||
//
|
||||
// class {useHook = () => {}}
|
||||
// class {useHook() {}}
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentPattern' &&
|
||||
node.parent.right === node &&
|
||||
!node.parent.computed
|
||||
) {
|
||||
// const {useHook = () => {}} = {};
|
||||
// ({useHook = () => {}} = {});
|
||||
//
|
||||
// Kinda clowny, but we'd said we'd follow spec convention for
|
||||
// `IsAnonymousFunctionDefinition()` usage.
|
||||
return node.parent.left;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for peeking the last item in a stack.
|
||||
*/
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
14
packages/eslint-plugin-react-hooks/src/index.js
Normal file
14
packages/eslint-plugin-react-hooks/src/index.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import RuleOfHooks from './RulesOfHooks';
|
||||
|
||||
export const rules = {
|
||||
'rules-of-hooks': RuleOfHooks,
|
||||
};
|
||||
|
|
@ -410,6 +410,21 @@ const bundles = [
|
|||
externals: ['jest-diff'],
|
||||
},
|
||||
|
||||
/******* ESLint Plugin for Hooks (proposal) *******/
|
||||
{
|
||||
label: 'eslint-plugin-react-hooks',
|
||||
// TODO: it's awkward to create a bundle for this
|
||||
// but if we don't, the package won't get copied.
|
||||
// We also can't create just DEV bundle because
|
||||
// it contains a NODE_ENV check inside.
|
||||
// We should probably tweak our build process
|
||||
// to allow "raw" packages that don't get bundled.
|
||||
bundleTypes: [NODE_DEV, NODE_PROD],
|
||||
moduleType: ISOMORPHIC,
|
||||
entry: 'eslint-plugin-react-hooks',
|
||||
externals: [],
|
||||
},
|
||||
|
||||
{
|
||||
label: 'scheduler-tracing',
|
||||
bundleTypes: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user