diff --git a/compiler/forget/package.json b/compiler/forget/package.json index 6721176bd4..07b345f77f 100644 --- a/compiler/forget/package.json +++ b/compiler/forget/package.json @@ -49,6 +49,7 @@ "@babel/traverse": "^7.19.1", "@testing-library/react": "^13.4.0", "@tsconfig/node16-strictest": "^1.0.3", + "@types/glob": "^8.1.0", "@types/invariant": "^2.2.35", "@types/jest": "^29.0.3", "@types/node": "^18.7.18", diff --git a/compiler/forget/scripts/build-react-hooks-fixures.js b/compiler/forget/scripts/build-react-hooks-fixures.js new file mode 100644 index 0000000000..04c238278a --- /dev/null +++ b/compiler/forget/scripts/build-react-hooks-fixures.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 { tests } = require("./eslint-plugin-react-hooks-test-cases"); +const { + runReactForgetBabelPlugin, +} = require("../dist/Babel/RunReactForgetBabelPlugin"); +const fs = require("fs"); +const path = require("path"); +const prettier = require("prettier"); +const prettierConfigPath = require.resolve("../.prettierrc"); +const process = require("process"); + +const FIXTURES_DIR = path.join( + process.cwd(), + "src", + "__tests__", + "fixtures", + "compiler", + "rules-of-hooks" +); + +const fixtures = []; +for (const test of tests.valid) { + fixtures.push({ code: test.code, valid: true }); +} +for (const test of tests.invalid) { + fixtures.push({ code: test.code, valid: false }); +} + +let index = 0; +for (const fixture of fixtures) { + let error = null; + let passes = true; + try { + // Does the fixture pass with hooks validation disabled? if not skip it + runReactForgetBabelPlugin(fixture.code, "rules-of-hooks.js", "typescript", { + environment: { + validateHooksUsage: false, + }, + }); + // Does the fixture pass with hooks validation enabled? + try { + runReactForgetBabelPlugin( + fixture.code, + "rules-of-hooks.js", + "typescript", + { + environment: { + validateHooksUsage: true, + }, + } + ); + } catch (e) { + passes = false; + } + } catch (e) { + error = e; + } + let name = `rules-of-hooks-${index}.js`; + let code = fixture.code; + if (error !== null) { + name = `todo.${name}`; + code = `// @skip\n${code}`; + } else if (fixture.valid === false) { + name = `error.${name}`; + if (passes) { + // oops, passed when we expected an error + name = `todo.${name}`; + code = `// @skip\n${code}`; + } + } else if (!passes) { + // oops, error when it should have passed + name = `todo.${name}`; + code = `// @skip\n${code}`; + } + + const fixturePath = path.join(FIXTURES_DIR, name); + const options = prettier.resolveConfig.sync(fixturePath, { + config: prettierConfigPath, + }); + const formatted = prettier.format(code, options); + fs.writeFileSync(fixturePath, formatted, "utf8"); + index++; +} diff --git a/compiler/forget/scripts/eslint-plugin-react-hooks-test-cases.js b/compiler/forget/scripts/eslint-plugin-react-hooks-test-cases.js new file mode 100644 index 0000000000..d47a1d38b7 --- /dev/null +++ b/compiler/forget/scripts/eslint-plugin-react-hooks-test-cases.js @@ -0,0 +1,985 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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"; + +// NOTE: Extracted from https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js + +/** + * A string template tag that removes padding from the left side of multi-line strings + */ +function normalizeIndent(strings) { + const codeLines = strings[0].split("\n"); + const leftPadding = codeLines[1].match(/\s+/)[0]; + return codeLines.map((line) => line.slice(leftPadding.length)).join("\n"); +} + +module.exports.tests = { + valid: [ + { + code: normalizeIndent` + // Valid because components can use hooks. + function ComponentWithHook() { + useHook(); + } + `, + }, + { + code: normalizeIndent` + // Valid because components can use hooks. + function createComponentWithHook() { + return function ComponentWithHook() { + useHook(); + }; + } + `, + }, + { + code: normalizeIndent` + // Valid because hooks can use hooks. + function useHookWithHook() { + useHook(); + } + `, + }, + { + code: normalizeIndent` + // Valid because hooks can use hooks. + function createHook() { + return function useHookWithHook() { + useHook(); + } + } + `, + }, + { + code: normalizeIndent` + // Valid because components can call functions. + function ComponentWithNormalFunction() { + doSomething(); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions can call functions. + function normalFunctionWithNormalFunction() { + doSomething(); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions can call functions. + function normalFunctionWithConditionalFunction() { + if (cond) { + doSomething(); + } + } + `, + }, + { + code: normalizeIndent` + // Valid because functions can call functions. + function functionThatStartsWithUseButIsntAHook() { + if (cond) { + userFetch(); + } + } + `, + }, + { + code: normalizeIndent` + // 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(); + } + `, + }, + { + code: normalizeIndent` + // 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 {useHook3 = () => { useState(); }} = {}; + ({useHook = () => { useState(); }} = {}); + Namespace.useHook = () => { useState(); }; + `, + }, + { + code: normalizeIndent` + // Valid because hooks can call hooks. + function useHook() { + useHook1(); + useHook2(); + } + `, + }, + { + code: normalizeIndent` + // Valid because hooks can call hooks. + function createHook() { + return function useHook() { + useHook1(); + useHook2(); + }; + } + `, + }, + { + code: normalizeIndent` + // Valid because hooks can call hooks. + function useHook() { + useState() && a; + } + `, + }, + { + code: normalizeIndent` + // Valid because hooks can call hooks. + function useHook() { + return useHook1() + useHook2(); + } + `, + }, + { + code: normalizeIndent` + // Valid because hooks can call hooks. + function useHook() { + return useHook1(useHook2()); + } + `, + }, + { + code: normalizeIndent` + // Valid because hooks can be used in anonymous arrow-function arguments + // to forwardRef. + const FancyButton = React.forwardRef((props, ref) => { + useHook(); + return ; + }); + `, + errors: [], + }, + { + code: normalizeIndent` + // Invalid because it's dangerous and might not warn otherwise. + // This *must* be invalid. + const FancyButton = forwardRef(function(props, ref) { + if (props.fancy) { + useCustomHook(); + } + return ; + }); + `, + errors: [], + }, + { + code: normalizeIndent` + // Invalid because it's dangerous and might not warn otherwise. + // This *must* be invalid. + const MemoizedButton = memo(function(props) { + if (props.fancy) { + useCustomHook(); + } + return ; + }); + `, + errors: [], + }, + { + code: normalizeIndent` + // This is invalid because "use"-prefixed functions used in named + // functions are assumed to be hooks. + React.unknownFunction(function notAComponent(foo, bar) { + useProbablyAHook(bar) + }); + `, + errors: [], + }, + { + code: normalizeIndent` + // Invalid because it's dangerous. + // Normally, this would crash, but not if you use inline requires. + // This *must* be invalid. + // It's expected to have some false positives, but arguably + // they are confusing anyway due to the use*() convention + // already being associated with Hooks. + useState(); + if (foo) { + const foo = React.useCallback(() => {}); + } + useCustomHook(); + `, + errors: [], + }, + { + code: normalizeIndent` + // Technically this is a false positive. + // We *could* make it valid (and it used to be). + // + // However, top-level Hook-like calls can be very dangerous + // in environments with inline requires because they can mask + // the runtime error by accident. + // So we prefer to disallow it despite the false positive. + + const {createHistory, useBasename} = require('history-2.1.2'); + const browserHistory = useBasename(createHistory)({ + basename: '/', + }); + `, + errors: [], + }, + { + code: normalizeIndent` + class ClassComponentWithFeatureFlag extends React.Component { + render() { + if (foo) { + useFeatureFlag(); + } + } + } + `, + errors: [], + }, + { + code: normalizeIndent` + class ClassComponentWithHook extends React.Component { + render() { + React.useState(); + } + } + `, + errors: [], + }, + { + code: normalizeIndent` + (class {useHook = () => { useState(); }}); + `, + errors: [], + }, + { + code: normalizeIndent` + (class {useHook() { useState(); }}); + `, + errors: [], + }, + { + code: normalizeIndent` + (class {h = () => { useState(); }}); + `, + errors: [], + }, + { + code: normalizeIndent` + (class {i() { useState(); }}); + `, + errors: [], + }, + ], +}; diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-after-early-return.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-after-early-return.expect.md new file mode 100644 index 0000000000..f8146c8510 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-after-early-return.expect.md @@ -0,0 +1,21 @@ + +## Input + +```javascript +function Component(props) { + if (props.cond) { + return null; + } + return useHook(); +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/error.invalid-hook-after-early-return.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-after-early-return.js similarity index 100% rename from compiler/forget/src/__tests__/fixtures/compiler/error.invalid-hook-after-early-return.js rename to compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-after-early-return.js diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-for.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-for.expect.md new file mode 100644 index 0000000000..a9acc25430 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-for.expect.md @@ -0,0 +1,26 @@ + +## Input + +```javascript +function Component(props) { + let i = 0; + for (let x = 0; useHook(x) < 10; useHook(i), x++) { + i += useHook(x); + } + return i; +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3) + +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4) + +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/error.invalid-hook-for.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-for.js similarity index 100% rename from compiler/forget/src/__tests__/fixtures/compiler/error.invalid-hook-for.js rename to compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-for.js diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-alternate.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-alternate.expect.md new file mode 100644 index 0000000000..51c2925634 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-alternate.expect.md @@ -0,0 +1,23 @@ + +## Input + +```javascript +function Component(props) { + let x = null; + if (props.cond) { + } else { + x = useHook(); + } + return x; +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/error.invalid-hook-if-alternate.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-alternate.js similarity index 100% rename from compiler/forget/src/__tests__/fixtures/compiler/error.invalid-hook-if-alternate.js rename to compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-alternate.js diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-consequent.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-consequent.expect.md new file mode 100644 index 0000000000..d21db9859d --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-consequent.expect.md @@ -0,0 +1,22 @@ + +## Input + +```javascript +function Component(props) { + let x = null; + if (props.cond) { + x = useHook(); + } + return x; +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/error.invalid-hook-if-consequent.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-consequent.js similarity index 100% rename from compiler/forget/src/__tests__/fixtures/compiler/error.invalid-hook-if-consequent.js rename to compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-if-consequent.js diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-32.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-32.expect.md new file mode 100644 index 0000000000..48f4ab1bd8 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-32.expect.md @@ -0,0 +1,22 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function ComponentWithConditionalHook() { + if (cond) { + useConditionalHook(); + } +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-32.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-32.js new file mode 100644 index 0000000000..455b7fb367 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-32.js @@ -0,0 +1,7 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function ComponentWithConditionalHook() { + if (cond) { + useConditionalHook(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-38.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-38.expect.md new file mode 100644 index 0000000000..0703fedacd --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-38.expect.md @@ -0,0 +1,22 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useHookWithConditionalHook() { + if (cond) { + useConditionalHook(); + } +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-38.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-38.js new file mode 100644 index 0000000000..8b82456138 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-38.js @@ -0,0 +1,7 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useHookWithConditionalHook() { + if (cond) { + useConditionalHook(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-40.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-40.expect.md new file mode 100644 index 0000000000..a79a682a9f --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-40.expect.md @@ -0,0 +1,20 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function ComponentWithTernaryHook() { + cond ? useTernaryHook() : null; +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-40.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-40.js new file mode 100644 index 0000000000..c5754bd624 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-40.js @@ -0,0 +1,5 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function ComponentWithTernaryHook() { + cond ? useTernaryHook() : null; +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-47.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-47.expect.md new file mode 100644 index 0000000000..a8401e92a7 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-47.expect.md @@ -0,0 +1,22 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function ComponentWithHookInsideLoop() { + while (cond) { + useHookInsideLoop(); + } +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-47.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-47.js new file mode 100644 index 0000000000..25177f5938 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-47.js @@ -0,0 +1,7 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function ComponentWithHookInsideLoop() { + while (cond) { + useHookInsideLoop(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-51.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-51.expect.md new file mode 100644 index 0000000000..9b14df8262 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-51.expect.md @@ -0,0 +1,22 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function normalFunctionWithConditionalHook() { + if (cond) { + useHookInsideNormalFunction(); + } +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-51.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-51.js new file mode 100644 index 0000000000..89bfce1eda --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-51.js @@ -0,0 +1,7 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function normalFunctionWithConditionalHook() { + if (cond) { + useHookInsideNormalFunction(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-52.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-52.expect.md new file mode 100644 index 0000000000..c73794ad22 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-52.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +// 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(); + } +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) + +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (7:7) + +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (10:10) + +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (12:12) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-52.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-52.js new file mode 100644 index 0000000000..2fb10722a6 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-52.js @@ -0,0 +1,14 @@ +// 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(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-53.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-53.expect.md new file mode 100644 index 0000000000..33298ad7c6 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-53.expect.md @@ -0,0 +1,26 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useHookInLoops() { + while (a) { + useHook1(); + if (b) continue; + useHook2(); + } +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) + +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (7:7) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-53.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-53.js new file mode 100644 index 0000000000..c00a73a62d --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-53.js @@ -0,0 +1,9 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useHookInLoops() { + while (a) { + useHook1(); + if (b) continue; + useHook2(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-54.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-54.expect.md new file mode 100644 index 0000000000..bf1bf3336d --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-54.expect.md @@ -0,0 +1,23 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useLabeledBlock() { + label: { + if (a) break label; + useHook(); + } +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-54.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-54.js new file mode 100644 index 0000000000..e5f6732143 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-54.js @@ -0,0 +1,8 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useLabeledBlock() { + label: { + if (a) break label; + useHook(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-56.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-56.expect.md new file mode 100644 index 0000000000..c466eb9725 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-56.expect.md @@ -0,0 +1,21 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useHook() { + if (a) return; + useState(); +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-56.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-56.js new file mode 100644 index 0000000000..7e77918794 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-56.js @@ -0,0 +1,6 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useHook() { + if (a) return; + useState(); +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-57.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-57.expect.md new file mode 100644 index 0000000000..6f3a91949e --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-57.expect.md @@ -0,0 +1,26 @@ + +## Input + +```javascript +// 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(); +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (10:10) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-57.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-57.js new file mode 100644 index 0000000000..fb5423937b --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-57.js @@ -0,0 +1,11 @@ +// 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(); +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-58.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-58.expect.md new file mode 100644 index 0000000000..95b2471979 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-58.expect.md @@ -0,0 +1,26 @@ + +## Input + +```javascript +// 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(); +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (10:10) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-58.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-58.js new file mode 100644 index 0000000000..c2fc30ec88 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-58.js @@ -0,0 +1,11 @@ +// 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(); +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-59.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-59.expect.md new file mode 100644 index 0000000000..528da10835 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-59.expect.md @@ -0,0 +1,23 @@ + +## Input + +```javascript +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useHook() { + a && useHook1(); + b && useHook2(); +} + +``` + + +## Error + +``` +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4) + +[ReactForget] InvalidInput: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (5:5) +``` + + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-59.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-59.js new file mode 100644 index 0000000000..09245c8884 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.rules-of-hooks-59.js @@ -0,0 +1,6 @@ +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +function useHook() { + a && useHook1(); + b && useHook2(); +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-0.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-0.expect.md new file mode 100644 index 0000000000..d84e26368f --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-0.expect.md @@ -0,0 +1,21 @@ + +## Input + +```javascript +// Valid because components can use hooks. +function ComponentWithHook() { + useHook(); +} + +``` + +## Code + +```javascript +// Valid because components can use hooks. +function ComponentWithHook() { + useHook(); +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-0.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-0.js new file mode 100644 index 0000000000..d0a47a7001 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-0.js @@ -0,0 +1,4 @@ +// Valid because components can use hooks. +function ComponentWithHook() { + useHook(); +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-1.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-1.expect.md new file mode 100644 index 0000000000..e4d4d1c66d --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-1.expect.md @@ -0,0 +1,33 @@ + +## Input + +```javascript +// Valid because components can use hooks. +function createComponentWithHook() { + return function ComponentWithHook() { + useHook(); + }; +} + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; // Valid because components can use hooks. +function createComponentWithHook() { + const $ = useMemoCache(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = function ComponentWithHook() { + useHook(); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-1.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-1.js new file mode 100644 index 0000000000..9b5bcde4be --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-1.js @@ -0,0 +1,6 @@ +// Valid because components can use hooks. +function createComponentWithHook() { + return function ComponentWithHook() { + useHook(); + }; +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-10.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-10.expect.md new file mode 100644 index 0000000000..50fb6486c9 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-10.expect.md @@ -0,0 +1,23 @@ + +## Input + +```javascript +// Valid because hooks can call hooks. +function useHook() { + useHook1(); + useHook2(); +} + +``` + +## Code + +```javascript +// Valid because hooks can call hooks. +function useHook() { + useHook1(); + useHook2(); +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-10.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-10.js new file mode 100644 index 0000000000..2ab771dc31 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-10.js @@ -0,0 +1,5 @@ +// Valid because hooks can call hooks. +function useHook() { + useHook1(); + useHook2(); +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-11.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-11.expect.md new file mode 100644 index 0000000000..ca7131796f --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-11.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +// Valid because hooks can call hooks. +function createHook() { + return function useHook() { + useHook1(); + useHook2(); + }; +} + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; // Valid because hooks can call hooks. +function createHook() { + const $ = useMemoCache(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = function useHook() { + useHook1(); + useHook2(); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-11.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-11.js new file mode 100644 index 0000000000..e2c9e72adf --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-11.js @@ -0,0 +1,7 @@ +// Valid because hooks can call hooks. +function createHook() { + return function useHook() { + useHook1(); + useHook2(); + }; +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-12.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-12.expect.md new file mode 100644 index 0000000000..6db60d1c8b --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-12.expect.md @@ -0,0 +1,21 @@ + +## Input + +```javascript +// Valid because hooks can call hooks. +function useHook() { + useState() && a; +} + +``` + +## Code + +```javascript +// Valid because hooks can call hooks. +function useHook() { + useState() && a; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-12.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-12.js new file mode 100644 index 0000000000..6936266e58 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-12.js @@ -0,0 +1,4 @@ +// Valid because hooks can call hooks. +function useHook() { + useState() && a; +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-13.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-13.expect.md new file mode 100644 index 0000000000..4bcb4dee57 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-13.expect.md @@ -0,0 +1,21 @@ + +## Input + +```javascript +// Valid because hooks can call hooks. +function useHook() { + return useHook1() + useHook2(); +} + +``` + +## Code + +```javascript +// Valid because hooks can call hooks. +function useHook() { + return useHook1() + useHook2(); +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-13.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-13.js new file mode 100644 index 0000000000..c1356a4b6d --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-13.js @@ -0,0 +1,4 @@ +// Valid because hooks can call hooks. +function useHook() { + return useHook1() + useHook2(); +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-14.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-14.expect.md new file mode 100644 index 0000000000..ae50157e09 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-14.expect.md @@ -0,0 +1,21 @@ + +## Input + +```javascript +// Valid because hooks can call hooks. +function useHook() { + return useHook1(useHook2()); +} + +``` + +## Code + +```javascript +// Valid because hooks can call hooks. +function useHook() { + return useHook1(useHook2()); +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-14.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-14.js new file mode 100644 index 0000000000..1c16d086bf --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-14.js @@ -0,0 +1,4 @@ +// Valid because hooks can call hooks. +function useHook() { + return useHook1(useHook2()); +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-16.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-16.expect.md new file mode 100644 index 0000000000..978a78e0c1 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-16.expect.md @@ -0,0 +1,25 @@ + +## Input + +```javascript +// Valid because hooks can be used in anonymous function arguments to +// forwardRef. +const FancyButton = React.forwardRef(function (props, ref) { + useHook(); + return ; +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-64.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-64.js new file mode 100644 index 0000000000..a257479288 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-64.js @@ -0,0 +1,10 @@ +// @skip + +// Invalid because it's dangerous and might not warn otherwise. +// This *must* be invalid. +const MemoizedButton = memo(function (props) { + if (props.fancy) { + useCustomHook(); + } + return ; +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-65.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-65.js new file mode 100644 index 0000000000..b647cb6968 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-65.js @@ -0,0 +1,7 @@ +// @skip + +// This is invalid because "use"-prefixed functions used in named +// functions are assumed to be hooks. +React.unknownFunction(function notAComponent(foo, bar) { + useProbablyAHook(bar); +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-66.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-66.js new file mode 100644 index 0000000000..e60fc3c5ec --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-66.js @@ -0,0 +1,13 @@ +// @skip + +// Invalid because it's dangerous. +// Normally, this would crash, but not if you use inline requires. +// This *must* be invalid. +// It's expected to have some false positives, but arguably +// they are confusing anyway due to the use*() convention +// already being associated with Hooks. +useState(); +if (foo) { + const foo = React.useCallback(() => {}); +} +useCustomHook(); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-67.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-67.js new file mode 100644 index 0000000000..175e9631a2 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-67.js @@ -0,0 +1,14 @@ +// @skip + +// Technically this is a false positive. +// We *could* make it valid (and it used to be). +// +// However, top-level Hook-like calls can be very dangerous +// in environments with inline requires because they can mask +// the runtime error by accident. +// So we prefer to disallow it despite the false positive. + +const { createHistory, useBasename } = require("history-2.1.2"); +const browserHistory = useBasename(createHistory)({ + basename: "/", +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-68.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-68.js new file mode 100644 index 0000000000..861a9bb241 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-68.js @@ -0,0 +1,9 @@ +// @skip + +class ClassComponentWithFeatureFlag extends React.Component { + render() { + if (foo) { + useFeatureFlag(); + } + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-69.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-69.js new file mode 100644 index 0000000000..59b5f2eeee --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-69.js @@ -0,0 +1,7 @@ +// @skip + +class ClassComponentWithHook extends React.Component { + render() { + React.useState(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-70.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-70.js new file mode 100644 index 0000000000..d0217a5ab2 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-70.js @@ -0,0 +1,7 @@ +// @skip + +(class { + useHook = () => { + useState(); + }; +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-71.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-71.js new file mode 100644 index 0000000000..17ade6f71f --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-71.js @@ -0,0 +1,7 @@ +// @skip + +(class { + useHook() { + useState(); + } +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-72.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-72.js new file mode 100644 index 0000000000..54cd765991 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-72.js @@ -0,0 +1,7 @@ +// @skip + +(class { + h = () => { + useState(); + }; +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-73.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-73.js new file mode 100644 index 0000000000..3500c604ff --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-73.js @@ -0,0 +1,7 @@ +// @skip + +(class { + i() { + useState(); + } +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-15.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-15.js new file mode 100644 index 0000000000..05d81522d1 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-15.js @@ -0,0 +1,8 @@ +// @skip + +// Valid because hooks can be used in anonymous arrow-function arguments +// to forwardRef. +const FancyButton = React.forwardRef((props, ref) => { + useHook(); + return ; +}); diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-7.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-7.js new file mode 100644 index 0000000000..93f6a55ac5 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-7.js @@ -0,0 +1,8 @@ +// @skip + +// Valid because functions can call functions. +function functionThatStartsWithUseButIsntAHook() { + if (cond) { + userFetch(); + } +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-9.js b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-9.js new file mode 100644 index 0000000000..c43ac4caae --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/rules-of-hooks/todo.rules-of-hooks-9.js @@ -0,0 +1,39 @@ +// @skip + +// 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 { + useHook3 = () => { + useState(); + }, +} = {}; +({ + useHook = () => { + useState(); + }, +} = {}); +Namespace.useHook = () => { + useState(); +}; diff --git a/compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts b/compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts index ffaf53ae70..149f4cd65f 100644 --- a/compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts +++ b/compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts @@ -10,6 +10,7 @@ /* global expect,test */ import fs from "fs"; +import glob from "glob"; import path from "path"; import { GatingOptions, PluginOptions } from "../../Babel/PluginOptions"; @@ -55,7 +56,9 @@ export default function generateTestsFromFixtures( ) { let files: Array; try { - files = fs.readdirSync(fixturesPath); + files = glob.sync("**/*.{js,md}", { + cwd: fixturesPath, + }); } catch (e) { if (e.code === "ENOENT") { files = []; @@ -233,11 +236,12 @@ function matchInputOutputFixtures(files: string[], fixturesPath: string) { ); } entry.input = resolvedPath; - const outputFile = path.format({ - dir: fixturesPath, - name: basename, - ext: EXPECT_SUFFIX, - }); + const outputName = `${basename}${EXPECT_SUFFIX}`; + const outputFile = path.join( + fixturesPath, + path.dirname(file), + outputName + ); entry.output = outputFile; } } diff --git a/compiler/forget/yarn.lock b/compiler/forget/yarn.lock index b0e6f1b618..d971d03b4b 100644 --- a/compiler/forget/yarn.lock +++ b/compiler/forget/yarn.lock @@ -81,7 +81,7 @@ source-map "^0.5.0" trim-right "^1.0.1" -"@babel/generator@^7.0.0", "@babel/generator@^7.21.4": +"@babel/generator@^7.0.0": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA== @@ -100,6 +100,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.5.tgz#c0c0e5449504c7b7de8236d99338c3e2a340745f" + integrity sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w== + dependencies: + "@babel/types" "^7.21.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -135,6 +145,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba" + integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== + "@babel/helper-function-name@^7.1.0", "@babel/helper-function-name@^7.19.0": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" @@ -259,6 +274,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" + integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" @@ -307,7 +327,12 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3" integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ== -"@babel/parser@^7.21.4", "@babel/parser@^7.7.4": +"@babel/parser@^7.21.5": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" + integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== + +"@babel/parser@^7.7.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== @@ -582,18 +607,18 @@ lodash "^4.17.10" "@babel/traverse@^7.19.1": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36" - integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" + integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== dependencies: "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-environment-visitor" "^7.18.9" + "@babel/generator" "^7.21.5" + "@babel/helper-environment-visitor" "^7.21.5" "@babel/helper-function-name" "^7.21.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.4" - "@babel/types" "^7.21.4" + "@babel/parser" "^7.21.5" + "@babel/types" "^7.21.5" debug "^4.1.0" globals "^11.1.0" @@ -659,6 +684,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== + dependencies: + "@babel/helper-string-parser" "^7.21.5" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1110,6 +1144,14 @@ dependencies: "@babel/types" "^7.3.0" +"@types/glob@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" + integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w== + dependencies: + "@types/minimatch" "^5.1.2" + "@types/node" "*" + "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -1171,6 +1213,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/minimatch@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + "@types/node@*", "@types/node@^18.7.18": version "18.7.19" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.19.tgz#ad83aa9b7af470fab7e0f562be87e97dc8ffe08e"