UNPKG

eslint-plugin-react-19-upgrade

Version:

An ESLint plugin to identify and fix breaking changes when upgrading React 18 to React 19

1,224 lines (1,176 loc) 45.6 kB
const assert = require("assert"); const { RuleTester } = require("eslint"); const babelParser = require("@babel/eslint-parser"); const plugin = require("../index.js"); const ruleNoDefaultProps = require("../rules/no-default-props"); const ruleNoPropTypes = require("../rules/no-prop-types"); const ruleNoLegacyContext = require("../rules/no-legacy-context"); const ruleNoStringRefs = require("../rules/no-string-refs"); const ruleNoFactories = require("../rules/no-factories"); const ruleNoLegacyReactDom = require("../rules/no-legacy-react-dom"); const ruleNoLegacyReactDomServer = require("../rules/no-legacy-react-dom-server"); const ruleNoLegacyTestUtilsAct = require("../rules/no-legacy-test-utils-act"); const ruleNoLegacyTestUtils = require("../rules/no-legacy-test-utils"); const ruleNoLegacyReactIs = require("../rules/no-legacy-react-is"); const ruleNoShallowRenderer = require("../rules/no-shallow-renderer"); const ruleNoImplicitRefCallbackReturn = require("../rules/no-implicit-ref-callback-return"); // --- minimal describe/it harness (mirrors prior style, prints a tree) --- const stats = { passed: 0, failed: 0 }; let currentSuite = null; function describe(text, method) { const parent = currentSuite; const suite = { text, depth: parent ? parent.depth + 1 : 0 }; currentSuite = suite; const indent = " ".repeat(suite.depth); console.log(`${indent}${text}`); try { method(); } finally { currentSuite = parent; } } function it(text, method) { const indent = " ".repeat((currentSuite ? currentSuite.depth : 0) + 1); const label = text.length > 80 ? `${text.slice(0, 77)}...` : text; try { method(); stats.passed += 1; console.log(`${indent}\u2713 ${label}`); } catch (err) { stats.failed += 1; console.log(`${indent}\u2717 ${label}`); console.log(`${indent} ${err.message.split("\n").join(`\n${indent} `)}`); } } // RuleTester wires up to the same harness so its runs appear in the tree RuleTester.describe = describe; RuleTester.it = it; RuleTester.itOnly = it; const ruleTester = new RuleTester({ files: ["**/*.js", "**/*.mjs"], languageOptions: { parser: babelParser, parserOptions: { requireConfigFile: false, babelOptions: { babelrc: false, configFile: false, parserOpts: { plugins: ["jsx", "classProperties"] }, }, ecmaFeatures: { jsx: true }, }, }, }); // ----------------------------------------------------------------------- // 1. Plugin API surface // ----------------------------------------------------------------------- describe("plugin api surface", () => { it("exports a rules object", () => { assert.ok(plugin && typeof plugin === "object", "plugin is an object"); assert.ok(plugin.rules && typeof plugin.rules === "object", "plugin.rules exists"); }); it("registers all twelve canonical rules", () => { for (const name of [ "no-default-props", "no-prop-types", "no-legacy-context", "no-string-refs", "no-factories", "no-legacy-react-dom", "no-legacy-react-dom-server", "no-legacy-test-utils-act", "no-legacy-test-utils", "no-legacy-react-is", "no-shallow-renderer", "no-implicit-ref-callback-return", ]) { assert.ok(plugin.rules[name], `rule ${name} is registered`); } }); it("exposes no-defaultprops / no-proptypes aliases pointing to the same rule", () => { assert.strictEqual(plugin.rules["no-defaultprops"], plugin.rules["no-default-props"]); assert.strictEqual(plugin.rules["no-proptypes"], plugin.rules["no-prop-types"]); }); it("every rule has meta.type, meta.docs.url, meta.messages, and schema", () => { for (const [name, rule] of Object.entries(plugin.rules)) { assert.ok(rule.meta, `${name}.meta`); assert.ok( ["problem", "suggestion", "layout"].includes(rule.meta.type), `${name}.meta.type is valid`, ); assert.ok(rule.meta.docs && typeof rule.meta.docs.url === "string", `${name}.docs.url`); assert.ok(rule.meta.messages && Object.keys(rule.meta.messages).length > 0, `${name}.messages`); assert.ok(Array.isArray(rule.meta.schema), `${name}.schema is array`); assert.strictEqual(typeof rule.create, "function", `${name}.create is function`); } }); it("every message template is a non-empty string", () => { for (const [name, rule] of Object.entries(plugin.rules)) { for (const [id, tpl] of Object.entries(rule.meta.messages)) { assert.strictEqual(typeof tpl, "string", `${name}.${id}`); assert.ok(tpl.length > 0, `${name}.${id} not empty`); } } }); it("rule.create returns an object of AST visitors", () => { const fakeContext = { report: () => {}, getSourceCode: () => ({ getScope: () => ({ variables: [], upper: null }) }), sourceCode: { getScope: () => ({ variables: [], upper: null }), getText: () => "" }, getFilename: () => "<input>", filename: "<input>", getScope: () => ({ variables: [], upper: null }), }; for (const [name, rule] of Object.entries(plugin.rules)) { const visitors = rule.create(fakeContext); assert.ok(visitors && typeof visitors === "object", `${name}.create returns object`); for (const fn of Object.values(visitors)) { assert.strictEqual(typeof fn, "function", `${name} visitor is function`); } } }); }); // ----------------------------------------------------------------------- // 2. no-default-props // ----------------------------------------------------------------------- ruleTester.run("no-default-props", ruleNoDefaultProps, { valid: [ // --- baseline shapes that should not fire --- `const Component = ({ name }) => <div>{name}</div>;`, `const Component = ({ name = 'x' }) => <div>{name}</div>;`, `function Component({ name }) { return <div>{name}</div>; }`, // class components — defaultProps is still legal on classes in React 19 ` import { PureComponent } from 'react'; class ClassComponent extends PureComponent { render() { const { propKey } = this.props; return propKey; } } ClassComponent.defaultProps = { propKey: 'propValue' }; `, ` class C extends React.Component { static defaultProps = { a: 1 }; render() { return null; } } `, // assignment shapes the rule deliberately ignores `foo.bar.defaultProps = { a: 1 };`, // object not an Identifier `Component.propTypes = { a: 1 };`, // different property name `Component.defaultProps = otherDefaults;`, // right is not ObjectExpression `Component["defaultProps"] = { a: 1 };`, // computed member ], invalid: [ // --- simple arrow FC, single key, destructured + shorthand --- { code: `const Component = ({ name }) => <div>{name}</div>; Component.defaultProps = { name: 'Test' };`, errors: [{ messageId: "defaultPropsDisallowed", data: { name: "Component" } }], output: `const Component = ({ name = 'Test' }) => <div>{name}</div>; `, }, // function declaration { code: `function Component({name}) { return <div>{name}</div>; } Component.defaultProps = { name: 'Test' };`, errors: 1, output: `function Component({ name = 'Test' }) { return <div>{name}</div>; } `, }, // non-destructured (props) — reports, but no fix { code: `const Component = (props) => <div>{props.name}</div>; Component.defaultProps = { name: 'Test' };`, errors: 1, output: null, }, // rest spread preserved { code: `const Component = ({ name, ...rest }) => <div>{name}</div>; Component.defaultProps = { name: 'Test' };`, errors: 1, output: `const Component = ({ name = 'Test', ...rest }) => <div>{name}</div>; `, }, // renamed destructure { code: `const Component = ({ name: displayName }) => <div>{displayName}</div>; Component.defaultProps = { name: 'Test' };`, errors: 1, output: `const Component = ({ name: displayName = 'Test' }) => <div>{displayName}</div>; `, }, // aliased via const { code: `const Component = ({ name }) => <div>{name}</div>; const Component2 = Component; Component2.defaultProps = { name: 'Test' };`, errors: [{ messageId: "defaultPropsDisallowed", data: { name: "Component2" } }], output: `const Component = ({ name = 'Test' }) => <div>{name}</div>; const Component2 = Component; `, }, // aliased to an imported component — reports, but no fix (can't reach definition) { code: `import Component from './Component'; const Component2 = Component; Component2.defaultProps = { name: 'Test' };`, errors: 1, }, // wrapped by styled() — not a function component, reports without fix { code: "import Component from './Component'; const Component2 = styled(Component)``; Component2.defaultProps = { name: 'Test' };", errors: 1, }, // forwardRef wrapping — same: call expression, not a function/class node we can edit { code: `const C = React.forwardRef((props, ref) => <div />); C.defaultProps = { a: 1 };`, errors: 1, output: null, }, // --- multi-key behavior --- { code: `const C = ({ a, b }) => <div>{a}{b}</div>; C.defaultProps = { a: 1, b: 2 };`, errors: 1, output: `const C = ({ a = 1, b = 2 }) => <div>{a}{b}</div>; `, }, // key in defaults but not in destructure — appended to destructure { code: `const C = ({ a }) => <div />; C.defaultProps = { a: 1, b: 2 };`, errors: 1, output: `const C = ({ a = 1, b = 2 }) => <div />; `, }, // key in destructure but not in defaults — left untouched { code: `const C = ({ a, b }) => <div />; C.defaultProps = { a: 1 };`, errors: 1, output: `const C = ({ a = 1, b }) => <div />; `, }, // --- preservation of existing defaults --- { code: `const C = ({ a = 'keep' }) => <div />; C.defaultProps = { a: 'ignored' };`, errors: 1, output: `const C = ({ a = 'keep' }) => <div />; `, }, // --- complex default expressions --- { code: `const C = ({ onClick }) => <button onClick={onClick} />; C.defaultProps = { onClick: () => {} };`, errors: 1, output: `const C = ({ onClick = () => {} }) => <button onClick={onClick} />; `, }, { code: `const DEFAULT = 'hi'; const C = ({ name }) => <div>{name}</div>; C.defaultProps = { name: DEFAULT };`, errors: 1, output: `const DEFAULT = 'hi'; const C = ({ name = DEFAULT }) => <div>{name}</div>; `, }, // --- non-fixable due to unsafe key shape --- { code: "const C = ({ a }) => <div />; C.defaultProps = { a: 1, 'data-x': 2 };", errors: 1, output: null, }, // --- empty defaults — reports, fix collapses the assignment --- { code: `const C = ({ a }) => <div />; C.defaultProps = {};`, errors: 1, output: `const C = ({ a }) => <div />; `, }, ], }); // ----------------------------------------------------------------------- // 3. no-prop-types // ----------------------------------------------------------------------- ruleTester.run("no-prop-types", ruleNoPropTypes, { valid: [ `const Component = ({ name }) => <div>{name}</div>;`, `import Something from 'not-prop-types';`, `const other = require('not-prop-types');`, // different module `foo.propTypes = { a: 1 };`, // lowercase identifier guarded `this.propTypes = { a: 1 };`, // ThisExpression, not Identifier `class C { propTypes = { a: 1 }; }`, // instance field, not static ], invalid: [ { code: ` import PropTypes from 'prop-types'; const Component = ({ name }) => <div>{name}</div>; Component.propTypes = { name: PropTypes.string }; `, errors: [ { messageId: "propTypesImportDisallowed" }, { messageId: "propTypesDisallowed", data: { name: "Component" } }, ], }, { code: ` import PT from 'prop-types'; const Component = ({ name }) => <div>{name}</div>; Component.propTypes = { name: PT.string }; `, errors: [ { messageId: "propTypesImportDisallowed" }, { messageId: "propTypesDisallowed" }, ], }, { code: `import { bool, string } from 'prop-types';`, errors: [ { messageId: "propTypesImportDisallowed" }, { messageId: "propTypesImportDisallowed" }, ], }, { code: `import * as PT from 'prop-types';`, errors: [{ messageId: "propTypesImportDisallowed" }], }, // propTypes assignment without any import — still flagged { code: `Component.propTypes = { a: 1 };`, errors: [{ messageId: "propTypesDisallowed" }], }, // CommonJS require of prop-types { code: `const PropTypes = require('prop-types');`, errors: [{ messageId: "propTypesImportDisallowed" }], }, // static propTypes class field on a class declaration { code: `class Component { static propTypes = { a: 1 }; }`, errors: [{ messageId: "propTypesDisallowed", data: { name: "Component" } }], }, // static propTypes on a class expression (uses binding name) { code: `const Component = class { static propTypes = { a: 1 }; };`, errors: [{ messageId: "propTypesDisallowed", data: { name: "Component" } }], }, // multiple components in one file { code: ` import PropTypes from 'prop-types'; const A = () => null; const B = () => null; A.propTypes = { x: PropTypes.string }; B.propTypes = { y: PropTypes.number }; `, errors: [ { messageId: "propTypesImportDisallowed" }, { messageId: "propTypesDisallowed", data: { name: "A" } }, { messageId: "propTypesDisallowed", data: { name: "B" } }, ], }, ], }); // ----------------------------------------------------------------------- // 4. no-legacy-context // ----------------------------------------------------------------------- ruleTester.run("no-legacy-context", ruleNoLegacyContext, { valid: [ ` class MyComponent extends React.Component { render() { return <div>{this.props.children}</div>; } } `, // modern context API ` const MyContext = React.createContext(); class MyComponent extends React.Component { static contextType = MyContext; render() { return <div>{this.context}</div>; } } `, // similarly-named, unrelated members ` class C extends React.Component { static myContextTypes = { a: 1 }; getContext() { return {}; } render() { return null; } } `, ], invalid: [ // the full triple from the original suite { code: ` import PropTypes from 'prop-types'; class Parent extends React.Component { static childContextTypes = { foo: PropTypes.string.isRequired }; getChildContext() { return { foo: 'bar' }; } render() { return <Child />; } } class Child extends React.Component { static contextTypes = { foo: PropTypes.string.isRequired }; render() { return <div>{this.context.foo}</div>; } } `, errors: [ { message: "'childContextTypes' uses a legacy context API that is no longer supported in React 19. Use 'contextType' instead." }, { message: "'getChildContext' uses a legacy context API that is no longer supported in React 19. Use 'React.createContext()' instead." }, { message: "'contextTypes' uses a legacy context API that is no longer supported in React 19. Use 'contextType' instead." }, ], }, // contextTypes alone { code: ` class C extends React.Component { static contextTypes = { a: 1 }; render() { return null; } } `, errors: 1, }, // childContextTypes alone { code: ` class C extends React.Component { static childContextTypes = { a: 1 }; render() { return null; } } `, errors: 1, }, // getChildContext alone { code: ` class C extends React.Component { getChildContext() { return {}; } render() { return null; } } `, errors: 1, }, // instance (non-static) class field — rule still catches these { code: ` class C extends React.Component { contextTypes = { a: 1 }; render() { return null; } } `, errors: 1, }, // class expression with legacy context — also caught { code: ` const C = class extends React.Component { static contextTypes = { a: 1 }; getChildContext() { return {}; } render() { return null; } }; `, errors: 2, }, ], }); // ----------------------------------------------------------------------- // 5. no-string-refs // ----------------------------------------------------------------------- ruleTester.run("no-string-refs", ruleNoStringRefs, { valid: [ // callback ref { code: `<input ref={(input) => this.input = input} />` }, // object ref from useRef { code: `const r = useRef(null); return <input ref={r} />;` }, // null ref { code: `<input ref={null} />` }, // numeric in expression container { code: `<input ref={42} />` }, // no ref attribute at all { code: `<input type="text" />` }, // template literal (not a Literal node) — bypasses the rule by design { code: "<input ref={`dyn`} />" }, ], invalid: [ { code: `<input ref='input' />`, errors: [{ messageId: "noStringRefs", type: "JSXAttribute" }], }, // double-quoted { code: `<input ref="input" />`, errors: [{ messageId: "noStringRefs" }], }, // empty string literal in attribute position { code: `<input ref="" />`, errors: [{ messageId: "noStringRefs" }], }, // string literal wrapped in a JSXExpressionContainer { code: `<input ref={"input"} />`, errors: [{ messageId: "noStringRefs" }], }, { code: `<input ref={""} />`, errors: [{ messageId: "noStringRefs" }], }, // nested JSX { code: `<div><input ref="inner" /></div>`, errors: [{ messageId: "noStringRefs" }], }, // inside a map { code: `items.map(i => <input key={i} ref="x" />);`, errors: [{ messageId: "noStringRefs" }], }, // multiple string refs in one file — each reported { code: `<div><input ref="a" /><input ref="b" /></div>`, errors: [ { messageId: "noStringRefs" }, { messageId: "noStringRefs" }, ], }, ], }); // ----------------------------------------------------------------------- // 6. no-factories // ----------------------------------------------------------------------- ruleTester.run("no-factories", ruleNoFactories, { valid: [ `function FactoryComponent() { return <div />; }`, `const button = <button />;`, // lowercase function returning an object with render — not a React factory ` function notReact() { return { render: function() { return value; } }; } `, // lowercase const assigned a factory-like object — not flagged ` const helper = function() { return { render: function() { return null; } }; }; `, // uppercase but no render method — not the module-factory pattern ` function Config() { return { foo: 1, bar: 2 }; } `, // createFactory imported from a different package — not flagged `import { createFactory } from 'other'; const d = createFactory('div');`, // destructure from a require() of a different package `const { createFactory } = require('other'); const d = createFactory('div');`, ], invalid: [ // classic module-pattern factory { code: `function FactoryComponent() { return { render() { return <div />; } } }`, errors: [{ messageId: "noModulePattern" }], }, // uppercase arrow / function expression assigned to const { code: `const Foo = function() { return { render: function() { return null; } }; };`, errors: [{ messageId: "noModulePattern" }], }, // named import of createFactory from react { code: `import { createFactory } from 'react'; const divFactory = createFactory('div');`, errors: [{ messageId: "noCreateFactory" }], }, // aliased React.createFactory { code: `const createFactory = React.createFactory; const divFactory = createFactory('div');`, errors: [{ messageId: "noCreateFactory" }], }, // destructured from React (identifier, not via require) { code: `const { createFactory } = React; const d = createFactory('div');`, errors: [{ messageId: "noCreateFactory" }], }, // renamed destructure — tracked via property key matching { code: `const { createFactory: cf } = React; const d = cf('div');`, errors: [{ messageId: "noCreateFactory" }], }, // direct React.createFactory call without any alias { code: `const d = React.createFactory('div');`, errors: [{ messageId: "noCreateFactory" }], }, // destructure from require('react') { code: `const { createFactory } = require('react'); const d = createFactory('div');`, errors: [{ messageId: "noCreateFactory" }], }, // renamed destructure from require('react') { code: `const { createFactory: cf } = require('react'); const d = cf('div');`, errors: [{ messageId: "noCreateFactory" }], }, // direct require('react').createFactory(...) call { code: `const d = require('react').createFactory('div');`, errors: [{ messageId: "noCreateFactory" }], }, ], }); // ----------------------------------------------------------------------- // 7. no-legacy-react-dom // ----------------------------------------------------------------------- ruleTester.run("no-legacy-react-dom", ruleNoLegacyReactDom, { valid: [ // surviving react-dom APIs `import ReactDOM from 'react-dom'; ReactDOM.flushSync(() => {});`, `import { createPortal } from 'react-dom'; createPortal(child, container);`, // different module `const other = require('not-react-dom'); other.render(x, y);`, // locally-defined render, no react-dom import `const render = () => {}; render();`, // the correct R19 replacement code `import { createRoot } from 'react-dom/client'; createRoot(el).render(<App />);`, `import { hydrateRoot } from 'react-dom/client'; hydrateRoot(el, <App />);`, // react-dom/client is a different module `import ReactDOMClient from 'react-dom/client'; ReactDOMClient.createRoot(el);`, // shadowed local is not the imported namespace `import ReactDOM from 'react-dom'; function useLocal(ReactDOM) { ReactDOM.render(<App />, root); }`, ], invalid: [ { code: `import ReactDOM from 'react-dom'; ReactDOM.render(<App />, root);`, errors: [{ messageId: "noRender" }], }, { code: `import { render } from 'react-dom'; render(<App />, root);`, errors: [{ messageId: "noRender" }], }, // removed import should fail even if unused { code: `import { render } from 'react-dom';`, errors: [{ messageId: "noRender" }], }, // aliased named import { code: `import { render as r } from 'react-dom'; r(<App />, root);`, errors: [{ messageId: "noRender" }], }, // removed member reference should fail even if not called { code: `import ReactDOM from 'react-dom'; const legacyRender = ReactDOM.render;`, errors: [{ messageId: "noRender" }], }, { code: `import ReactDOM from 'react-dom'; ReactDOM.hydrate(<App />, root);`, errors: [{ messageId: "noHydrate" }], }, { code: `import { hydrate } from 'react-dom'; hydrate(<App />, root);`, errors: [{ messageId: "noHydrate" }], }, { code: `import ReactDOM from 'react-dom'; ReactDOM.unmountComponentAtNode(root);`, errors: [{ messageId: "noUnmountComponentAtNode" }], }, { code: `const { unmountComponentAtNode } = require('react-dom'); unmountComponentAtNode(root);`, errors: [{ messageId: "noUnmountComponentAtNode" }], }, // destructured require should fail even if unused { code: `const { render } = require('react-dom');`, errors: [{ messageId: "noRender" }], }, { code: `import ReactDOM from 'react-dom'; ReactDOM.findDOMNode(instance);`, errors: [{ messageId: "noFindDOMNode" }], }, { code: `const { findDOMNode } = require('react-dom'); findDOMNode(instance);`, errors: [{ messageId: "noFindDOMNode" }], }, // renamed destructure from require { code: `const { render: r } = require('react-dom'); r(<App />, root);`, errors: [{ messageId: "noRender" }], }, // namespace import { code: `import * as RD from 'react-dom'; RD.findDOMNode(i);`, errors: [{ messageId: "noFindDOMNode" }], }, // inline require { code: `require('react-dom').render(<App />, root);`, errors: [{ messageId: "noRender" }], }, // CommonJS namespace var { code: `const ReactDOM = require('react-dom'); ReactDOM.hydrate(<App />, root);`, errors: [{ messageId: "noHydrate" }], }, // removed unstable_* APIs { code: `import ReactDOM from 'react-dom'; ReactDOM.unstable_flushControlled(fn);`, errors: [{ messageId: "noUnstableFlushControlled" }], }, // removed unstable import should fail even if unused { code: `import { unstable_createEventHandle } from 'react-dom';`, errors: [{ messageId: "noUnstableCreateEventHandle" }], }, // aliased named import { code: `import { unstable_flushControlled as fc } from 'react-dom'; fc(fn);`, errors: [{ messageId: "noUnstableFlushControlled" }], }, // destructure from require { code: `const { unstable_renderSubtreeIntoContainer } = require('react-dom');`, errors: [{ messageId: "noUnstableRenderSubtreeIntoContainer" }], }, // namespace import { code: `import * as RD from 'react-dom'; RD.renderSubtreeIntoContainer(parent, el, container);`, errors: [{ messageId: "noRenderSubtreeIntoContainer" }], }, // inline require { code: `require('react-dom').unstable_runWithPriority(priority, fn);`, errors: [{ messageId: "noUnstableRunWithPriority" }], }, ], }); // ----------------------------------------------------------------------- // 8. no-legacy-react-dom-server // ----------------------------------------------------------------------- ruleTester.run("no-legacy-react-dom-server", ruleNoLegacyReactDomServer, { valid: [ // surviving exports `import { renderToString } from 'react-dom/server'; renderToString(<App />);`, `import { renderToStaticMarkup } from 'react-dom/server'; renderToStaticMarkup(<App />);`, `import { renderToPipeableStream } from 'react-dom/server';`, `import { renderToReadableStream } from 'react-dom/server';`, `import Server from 'react-dom/server'; Server.renderToString(<App />);`, // different module `import { renderToNodeStream } from 'not-react-dom-server';`, `const { renderToNodeStream } = require('something-else');`, // shadowed local is not the imported namespace `import Server from 'react-dom/server'; function useLocal(Server) { Server.renderToNodeStream(<App />); }`, ], invalid: [ { code: `import { renderToNodeStream } from 'react-dom/server'; renderToNodeStream(<App />);`, errors: [{ messageId: "noRenderToNodeStream" }], }, { code: `import { renderToStaticNodeStream } from 'react-dom/server';`, errors: [{ messageId: "noRenderToStaticNodeStream" }], }, // mixed import: allowlist proves surviving export is ignored { code: `import { renderToString, renderToNodeStream } from 'react-dom/server';`, errors: [{ messageId: "noRenderToNodeStream" }], }, // namespace access { code: `import Server from 'react-dom/server'; Server.renderToNodeStream(<App />);`, errors: [{ messageId: "noRenderToNodeStream" }], }, // removed member reference should fail even if not called { code: `import Server from 'react-dom/server'; const legacy = Server.renderToNodeStream;`, errors: [{ messageId: "noRenderToNodeStream" }], }, { code: `import Server from 'react-dom/server'; Server.renderToStaticNodeStream(<App />);`, errors: [{ messageId: "noRenderToStaticNodeStream" }], }, // namespace * as { code: `import * as Server from 'react-dom/server'; Server.renderToNodeStream(<App />);`, errors: [{ messageId: "noRenderToNodeStream" }], }, // destructure from require { code: `const { renderToNodeStream } = require('react-dom/server');`, errors: [{ messageId: "noRenderToNodeStream" }], }, // CommonJS namespace var { code: `const Server = require('react-dom/server'); Server.renderToStaticNodeStream(<App />);`, errors: [{ messageId: "noRenderToStaticNodeStream" }], }, // inline require { code: `require('react-dom/server').renderToNodeStream(<App />);`, errors: [{ messageId: "noRenderToNodeStream" }], }, ], }); // ----------------------------------------------------------------------- // 9. no-legacy-test-utils-act // ----------------------------------------------------------------------- ruleTester.run("no-legacy-test-utils-act", ruleNoLegacyTestUtilsAct, { valid: [ // correct R19 import `import { act } from 'react'; act(() => {});`, // unrelated test-utils export still legal (narrow rule; see no-legacy-test-utils) `import { Simulate } from 'react-dom/test-utils'; Simulate.click(node);`, `const TestUtils = require('react-dom/test-utils'); TestUtils.Simulate.click(node);`, // different module `import TestUtils from 'other/test-utils'; TestUtils.act(() => {});`, // local identifier coincidentally named act, no test-utils binding `const act = (fn) => fn(); act(() => {});`, // shadowed local is not the imported namespace `import TestUtils from 'react-dom/test-utils'; function useLocal(TestUtils) { TestUtils.act(() => {}); }`, ], invalid: [ // simple named import — fixable { code: `import { act } from 'react-dom/test-utils'; act(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], output: `import { act } from 'react'; act(() => {});`, }, // aliased named import — alias preserved { code: `import { act as a } from 'react-dom/test-utils'; a(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], output: `import { act as a } from 'react'; a(() => {});`, }, // mixed — split into two imports { code: `import { act, Simulate } from 'react-dom/test-utils';`, errors: [{ messageId: "noTestUtilsAct" }], output: `import { act } from 'react';\nimport { Simulate } from 'react-dom/test-utils';`, }, // default + named — default stays, act moves { code: `import TestUtils, { act } from 'react-dom/test-utils'; TestUtils.Simulate.click(node);`, errors: [{ messageId: "noTestUtilsAct" }], output: `import { act } from 'react';\nimport TestUtils from 'react-dom/test-utils'; TestUtils.Simulate.click(node);`, }, // namespace + act via member access — reported, not fixed { code: `import TestUtils from 'react-dom/test-utils'; TestUtils.act(() => {});`, errors: [ { messageId: "noTestUtilsAct" }, ], }, { code: `import * as TestUtils from 'react-dom/test-utils'; TestUtils.act(() => {});`, errors: [ { messageId: "noTestUtilsAct" }, ], }, // CommonJS destructure — reported, not fixed { code: `const { act } = require('react-dom/test-utils'); act(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], }, // CommonJS namespace var + member access { code: `const TestUtils = require('react-dom/test-utils'); TestUtils.act(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], }, // inline require { code: `require('react-dom/test-utils').act(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], }, ], }); // ----------------------------------------------------------------------- // 10. no-legacy-test-utils // ----------------------------------------------------------------------- ruleTester.run("no-legacy-test-utils", ruleNoLegacyTestUtils, { valid: [ // correct R19 import `import { act } from 'react'; act(() => {});`, // the modern replacement for the removed utilities `import { render } from '@testing-library/react'; render(<App />);`, // different module `import TestUtils from 'other/test-utils'; TestUtils.act(() => {});`, `const TestUtils = require('other/test-utils'); TestUtils.Simulate.click(node);`, // local identifier coincidentally named act, no test-utils binding `const act = (fn) => fn(); act(() => {});`, ], invalid: [ // simple named import — fixable { code: `import { act } from 'react-dom/test-utils'; act(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], output: `import { act } from 'react'; act(() => {});`, }, // aliased named import — alias preserved { code: `import { act as a } from 'react-dom/test-utils'; a(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], output: `import { act as a } from 'react'; a(() => {});`, }, // mixed — act split out; the removed utility flagged and left for a later pass { code: `import { act, Simulate } from 'react-dom/test-utils';`, errors: [ { messageId: "noTestUtilsAct" }, { messageId: "noTestUtilsRemoved", data: { name: "Simulate" } }, ], output: `import { act } from 'react';\nimport { Simulate } from 'react-dom/test-utils';`, }, // default + named — default flagged as removed module, act moves { code: `import TestUtils, { act } from 'react-dom/test-utils'; TestUtils.Simulate.click(node);`, errors: [ { messageId: "noTestUtilsModule" }, { messageId: "noTestUtilsAct" }, ], output: `import { act } from 'react';\nimport TestUtils from 'react-dom/test-utils'; TestUtils.Simulate.click(node);`, }, // removed utility as named import { code: `import { Simulate } from 'react-dom/test-utils'; Simulate.click(node);`, errors: [{ messageId: "noTestUtilsRemoved", data: { name: "Simulate" } }], }, // removed utility, aliased { code: `import { renderIntoDocument as rid } from 'react-dom/test-utils';`, errors: [ { messageId: "noTestUtilsRemoved", data: { name: "renderIntoDocument" } }, ], }, // default / namespace imports — the whole module is gone { code: `import TestUtils from 'react-dom/test-utils'; TestUtils.act(() => {});`, errors: [{ messageId: "noTestUtilsModule" }], }, { code: `import * as TestUtils from 'react-dom/test-utils'; TestUtils.act(() => {});`, errors: [{ messageId: "noTestUtilsModule" }], }, // shadowing doesn't rescue the import itself { code: `import TestUtils from 'react-dom/test-utils'; function useLocal(TestUtils) { TestUtils.act(() => {}); }`, errors: [{ messageId: "noTestUtilsModule" }], }, // side-effect import { code: `import 'react-dom/test-utils';`, errors: [{ messageId: "noTestUtilsModule" }], }, // CommonJS destructure — reported, not fixed { code: `const { act } = require('react-dom/test-utils'); act(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], }, // CommonJS destructure of removed utilities { code: `const { Simulate, mockComponent } = require('react-dom/test-utils');`, errors: [ { messageId: "noTestUtilsRemoved", data: { name: "Simulate" } }, { messageId: "noTestUtilsRemoved", data: { name: "mockComponent" } }, ], }, // CommonJS namespace var { code: `const TestUtils = require('react-dom/test-utils'); TestUtils.act(() => {});`, errors: [{ messageId: "noTestUtilsModule" }], }, // inline require { code: `require('react-dom/test-utils').act(() => {});`, errors: [{ messageId: "noTestUtilsAct" }], }, { code: `require('react-dom/test-utils').renderIntoDocument(<App />);`, errors: [ { messageId: "noTestUtilsRemoved", data: { name: "renderIntoDocument" } }, ], }, ], }); // ----------------------------------------------------------------------- // 11. no-legacy-react-is // ----------------------------------------------------------------------- ruleTester.run("no-legacy-react-is", ruleNoLegacyReactIs, { valid: [ // surviving react-is APIs `import { isElement } from 'react-is'; isElement(x);`, `import * as ReactIs from 'react-is'; ReactIs.isFragment(x);`, `const { isValidElementType } = require('react-is'); isValidElementType(x);`, // same names from a different module `import { isAsyncMode } from 'not-react-is'; isAsyncMode(x);`, // local identifier coincidence, no react-is binding `const isConcurrentMode = () => false; isConcurrentMode(x);`, // shadowed local is not the imported namespace `import ReactIs from 'react-is'; function useLocal(ReactIs) { ReactIs.isAsyncMode(x); }`, ], invalid: [ { code: `import { isConcurrentMode } from 'react-is'; isConcurrentMode(x);`, errors: [{ messageId: "noIsConcurrentMode" }], }, // removed import should fail even if unused { code: `import { isAsyncMode } from 'react-is';`, errors: [{ messageId: "noIsAsyncMode" }], }, // aliased named import { code: `import { isAsyncMode as iam } from 'react-is'; iam(x);`, errors: [{ messageId: "noIsAsyncMode" }], }, // default import member access { code: `import ReactIs from 'react-is'; ReactIs.isConcurrentMode(x);`, errors: [{ messageId: "noIsConcurrentMode" }], }, // namespace import member access { code: `import * as ReactIs from 'react-is'; ReactIs.isAsyncMode(x);`, errors: [{ messageId: "noIsAsyncMode" }], }, // removed member reference should fail even if not called { code: `import ReactIs from 'react-is'; const check = ReactIs.isConcurrentMode;`, errors: [{ messageId: "noIsConcurrentMode" }], }, // destructure from require { code: `const { isConcurrentMode } = require('react-is'); isConcurrentMode(x);`, errors: [{ messageId: "noIsConcurrentMode" }], }, // renamed destructure from require { code: `const { isAsyncMode: a } = require('react-is'); a(x);`, errors: [{ messageId: "noIsAsyncMode" }], }, // CommonJS namespace var { code: `const ReactIs = require('react-is'); ReactIs.isAsyncMode(x);`, errors: [{ messageId: "noIsAsyncMode" }], }, // inline require { code: `require('react-is').isConcurrentMode(x);`, errors: [{ messageId: "noIsConcurrentMode" }], }, ], }); // ----------------------------------------------------------------------- // 12. no-shallow-renderer // ----------------------------------------------------------------------- ruleTester.run("no-shallow-renderer", ruleNoShallowRenderer, { valid: [ // the correct R19 replacement `import ShallowRenderer from 'react-shallow-renderer';`, `const ShallowRenderer = require('react-shallow-renderer');`, // the base package is a separate (deprecated, not removed) concern `import TestRenderer from 'react-test-renderer';`, // near-miss source string must not match `import Shallow from 'react-test-renderer/shallow-thing';`, ], invalid: [ { code: `import ShallowRenderer from 'react-test-renderer/shallow';`, errors: [{ messageId: "noShallowRenderer" }], output: `import ShallowRenderer from 'react-shallow-renderer';`, }, // namespace import preserved { code: `import * as SR from 'react-test-renderer/shallow';`, errors: [{ messageId: "noShallowRenderer" }], output: `import * as SR from 'react-shallow-renderer';`, }, // double quotes preserved { code: `import SR from "react-test-renderer/shallow";`, errors: [{ messageId: "noShallowRenderer" }], output: `import SR from "react-shallow-renderer";`, }, // side-effect import { code: `import 'react-test-renderer/shallow';`, errors: [{ messageId: "noShallowRenderer" }], output: `import 'react-shallow-renderer';`, }, // CommonJS require { code: `const ShallowRenderer = require('react-test-renderer/shallow');`, errors: [{ messageId: "noShallowRenderer" }], output: `const ShallowRenderer = require('react-shallow-renderer');`, }, // destructured require { code: `const { createRenderer } = require('react-test-renderer/shallow');`, errors: [{ messageId: "noShallowRenderer" }], output: `const { createRenderer } = require('react-shallow-renderer');`, }, // inline require { code: `new (require('react-test-renderer/shallow'))();`, errors: [{ messageId: "noShallowRenderer" }], output: `new (require('react-shallow-renderer'))();`, }, ], }); // ----------------------------------------------------------------------- // 13. no-implicit-ref-callback-return // ----------------------------------------------------------------------- ruleTester.run( "no-implicit-ref-callback-return", ruleNoImplicitRefCallbackReturn, { valid: [ // block body `const view = <div ref={el => { this.el = el; }} />;`, // empty block body `const view = <div ref={el => {}} />;`, // explicit undefined `const view = <div ref={el => undefined} />;`, // void operator `const view = <div ref={el => void (this.el = el)} />;`, // ref object `const view = <div ref={myRef} />;`, // implicit return is fine on non-ref attributes `const view = <div onClick={e => (count = e)} />;`, // function expressions are out of scope `const view = <div ref={function (el) { self.el = el; }} />;`, // explicit returns in block bodies are out of scope (intentional cleanup) `const view = <div ref={el => { attach(el); return () => detach(el); }} />;`, ], invalid: [ // parenthesized assignment — parens removed by the fix { code: `const view = <div ref={el => (this.el = el)} />;`, errors: [{ messageId: "noImplicitRefCallbackReturn" }], output: `const view = <div ref={el => { this.el = el; }} />;`, }, // bare assignment { code: `const view = <div ref={el => this.el = el} />;`, errors: [{ messageId: "noImplicitRefCallbackReturn" }], output: `const view = <div ref={el => { this.el = el; }} />;`, }, // call expression { code: `const view = <li ref={el => list.push(el)} />;`, errors: [{ messageId: "noImplicitRefCallbackReturn" }], output: `const view = <li ref={el => { list.push(el); }} />;`, }, // conditional expression { code: `const view = <div ref={el => el ? attach(el) : detach()} />;`, errors: [{ messageId: "noImplicitRefCallbackReturn" }], output: `const view = <div ref={el => { el ? attach(el) : detach(); }} />;`, }, // sequence expression { code: `const view = <div ref={el => (map.set(id, el), el.focus())} />;`, errors: [{ messageId: "noImplicitRefCallbackReturn" }], output: `const view = <div ref={el => { map.set(id, el), el.focus(); }} />;`, }, // only undefined/void are exempt — null still flagged { code: `const view = <div ref={el => null} />;`, errors: [{ messageId: "noImplicitRefCallbackReturn" }], output: `const view = <div ref={el => { null; }} />;`, }, // comments adjacent to the body suppress the fix { code: `const view = <div ref={el => /* keep */ (this.el = el)} />;`, errors: [{ messageId: "noImplicitRefCallbackReturn" }], output: null, }, // multiple refs fixed in one pass { code: `const view = <div><span ref={el => (a = el)} /><em ref={el => (b = el)} /></div>;`, errors: [ { messageId: "noImplicitRefCallbackReturn" }, { messageId: "noImplicitRefCallbackReturn" }, ], output: `const view = <div><span ref={el => { a = el; }} /><em ref={el => { b = el; }} /></div>;`, }, ], }, ); // ----------------------------------------------------------------------- // summary // ----------------------------------------------------------------------- console.log(`\n${stats.passed} passed, ${stats.failed} failed`); if (stats.failed > 0) { process.exitCode = 1; }