diff --git a/.changeset/fix-use-memo-result.md b/.changeset/fix-use-memo-result.md new file mode 100644 index 0000000..9845601 --- /dev/null +++ b/.changeset/fix-use-memo-result.md @@ -0,0 +1,9 @@ +--- +"react-19-remove-memoization": patch +--- + +Replace `useMemo(factory, deps)` with the factory's result instead of the factory function, so memoized values +stay values. Only unwrap parameterless factories; keep return-type annotations and `useMemo` typing; invoke +member factories unbound via `(0, obj.fn)()`; convert nested memoization inside-out; and remove unused import +bindings by local alias so duplicate `useMemo as a` / `useMemo as b` specifiers stay independent. Preserve +`import type` and semicolon style on React imports, and leave imports that lose no specifier untouched. diff --git a/.gitignore b/.gitignore index 9ab5e4e..4f0f42c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +.pnpm-store/ dist/ *.log .DS_Store diff --git a/codemods/remove-memoization/README.md b/codemods/remove-memoization/README.md index 243937b..a769f1f 100644 --- a/codemods/remove-memoization/README.md +++ b/codemods/remove-memoization/README.md @@ -4,6 +4,21 @@ Remove manual React memoization calls: `useCallback(...)`, `useMemo(...)`, and ` This codemod is intended for React Compiler adoption and mirrors `react/19/remove-memoization` from `codemod/commons`. +`useCallback(fn, deps)` and `memo(Component)` are replaced by their first argument. `useMemo(factory, deps)` +is replaced by the factory's *result*: + +- a parameterless expression-bodied arrow becomes its body (`useMemo(() => x, deps)` → `x`) +- a parameterless block that only returns an expression does the same (`useMemo(() => { return x; }, deps)` → `x`) +- arrows with parameters, `async` factories, and other bodies are invoked in place (`((x = f()) => x)()`, `(async () => …)()`) +- return-type annotations stay on an IIFE; `useMemo(…)` without a return annotation keeps `T` via `as T` +- member factories are invoked unbound (`useMemo(obj.fn, [])` → `(0, obj.fn)()`) to match React’s call +- nested memoization is converted inside-out (`useMemo(useMemo(() => () => 42))` → `42`) + +Only React imports that lose a specifier are rewritten; `import type` and the file's semicolon style are preserved. +Unused memoization bindings are removed by local alias, so duplicate aliases of the same export +(`useMemo as a` vs `useMemo as b`) are handled independently. A default or namespace React import is removed only +when the removed memoization calls were its last uses. + > This is not a universally safe codemod. React Compiler is not a 1:1 replacement for every manual `useMemo`, `useCallback`, or `memo` occurrence, so some memoization may need to be kept to preserve semantics. ## Usage diff --git a/codemods/remove-memoization/scripts/codemod.ts b/codemods/remove-memoization/scripts/codemod.ts index b19ad81..3adb36c 100644 --- a/codemods/remove-memoization/scripts/codemod.ts +++ b/codemods/remove-memoization/scripts/codemod.ts @@ -122,24 +122,271 @@ function memoizationCallInfo( return MEMOIZATION_IMPORTS.has(importedName) ? { call, importedName } : null; } -function firstMemoizationArgument( - call: SgNode, +// Parents where an arbitrary expression can stand in for the call without +// extra parentheses. Anywhere else (`a + useMemo(...)`, `!useMemo(...)`) the +// factory result is wrapped so operator precedence is preserved. +const BARE_EXPRESSION_PARENTS = new Set([ + "variable_declarator", + "assignment_expression", + "return_statement", + "arguments", + "array", + "pair", + "jsx_expression", + "parenthesized_expression", +]); + +function hasAnonymousChild(node: SgNode, text: string): boolean { + return node.children().some((child) => !child.isNamed() && child.text() === text); +} + +function unwrapParentheses(node: SgNode): SgNode { + let current = node; + while (current.kind() === "parenthesized_expression") { + const inner = namedChildren(current)[0]; + if (!inner) { + return current; + } + current = inner; + } + return current; +} + +// The expression returned by a block that is nothing but `return ;`. +// Any other statement or comment in the block means it is kept as an IIFE. +function soleReturnExpression(block: SgNode): SgNode | null { + const statements = block.children().filter((child) => child.isNamed()); + const statement = statements[0]; + if (statements.length !== 1 || statement?.kind() !== "return_statement") { + return null; + } + + return namedChildren(statement)[0] ?? null; +} + +function arrowFormalParameters(factory: SgNode): SgNode | null { + const params = factory.field("parameters"); + if (params?.kind() === "formal_parameters") { + return params; + } + + return namedChildren(factory).find((child) => child.kind() === "formal_parameters") ?? null; +} + +// Parameterless factories are `() => ...`. Any binding, default, rest, or +// destructuring parameter keeps its own scope and must stay as an invocation. +function isParameterlessArrow(factory: SgNode): boolean { + if (factory.field("parameter")) { + return false; + } + + const formal = arrowFormalParameters(factory); + if (formal) { + return namedChildren(formal).length === 0; + } + + // `x => x` has no formal_parameters node; the binding is a named child that + // is not the body (and not a type annotation / type parameters). + const body = factory.field("body"); + return !namedChildren(factory).some((child) => { + if (body && child.id() === body.id()) { + return false; + } + const kind = child.kind(); + return kind !== "type_parameters" && kind !== "type_annotation" && kind !== "returning_type_annotation"; + }); +} + +function arrowReturnTypeAnnotation(factory: SgNode): SgNode | null { + return ( + factory.field("return_type") ?? + namedChildren(factory).find( + (child) => child.kind() === "type_annotation" || child.kind() === "returning_type_annotation", + ) ?? + null + ); +} + +function callTypeArgumentsText(call: SgNode): string | null { + const typeArgs = call.field("type_arguments"); + if (!typeArgs) { + return null; + } + + // type_arguments text is `` or ``; strip the brackets. + const text = typeArgs.text(); + if (text.startsWith("<") && text.endsWith(">")) { + return text.slice(1, -1).trim(); + } + + return text.trim() || null; +} + +function wrapExpressionText(call: SgNode, expression: SgNode): string { + const bareParent = BARE_EXPRESSION_PARENTS.has(call.parent()?.kind() ?? ""); + const text = expression.text(); + return bareParent && expression.kind() !== "sequence_expression" ? text : `(${text})`; +} + +function wrapRawExpressionText(call: SgNode, text: string, forceParen = false): string { + const bareParent = BARE_EXPRESSION_PARENTS.has(call.parent()?.kind() ?? ""); + return !forceParen && bareParent ? text : `(${text})`; +} + +function invokeFactoryText(call: SgNode, factory: SgNode): string { + if (factory.kind() === "identifier") { + return `${factory.text()}()`; + } + + // Member references must stay unbound: React calls the factory as a bare + // function, so `(obj.compute)()` would incorrectly set `this` to `obj`. + if (factory.kind() === "member_expression" || factory.kind() === "subscript_expression") { + return `(0, ${factory.text()})()`; + } + + return `(${factory.text()})()`; +} + +// Extract the expression a useMemo factory evaluates to, when that is safe. +// Returns null when the factory must be invoked instead (params, async, +// return type annotation, non-trivial body, etc.). +function extractableUseMemoResult(factory: SgNode): SgNode | null { + const unwrapped = unwrapParentheses(factory); + + if (unwrapped.kind() !== "arrow_function" || hasAnonymousChild(unwrapped, "async")) { + return null; + } + + if (!isParameterlessArrow(unwrapped) || arrowReturnTypeAnnotation(unwrapped)) { + return null; + } + + const body = unwrapped.field("body"); + if (!body) { + return null; + } + + if (body.kind() !== "statement_block") { + return unwrapParentheses(body); + } + + return soleReturnExpression(body); +} + +function applyTypeAssertion(call: SgNode, expressionText: string): string { + const typeArgs = callTypeArgumentsText(call); + if (!typeArgs) { + return expressionText; + } + + // `as` binds looser than most operators; parenthesize the asserted value and + // wrap again when the call sits in an operator position. + const asserted = `(${expressionText}) as ${typeArgs}`; + const bareParent = BARE_EXPRESSION_PARENTS.has(call.parent()?.kind() ?? ""); + return bareParent ? asserted : `(${asserted})`; +} + +// `useMemo(factory, deps)` evaluates to the factory's *result*, so the call is +// replaced by that result, not by the factory itself. Expression-bodied arrows +// and blocks that only return an expression become that expression; +// everything else is invoked in place. +function memoFactoryResultText(call: SgNode, memoFactory: SgNode): string { + const factory = unwrapParentheses(memoFactory); + const extracted = extractableUseMemoResult(factory); + if (extracted) { + // Preserve useMemo contextual typing when the factory itself has no + // return annotation (those keep the annotated IIFE path below). + if (callTypeArgumentsText(call)) { + return applyTypeAssertion(call, extracted.text()); + } + return wrapExpressionText(call, extracted); + } + + // Annotated arrows keep their return type by staying as invocations. + // Type arguments on the call are redundant once the annotation remains. + if ( + factory.kind() === "arrow_function" && + !hasAnonymousChild(factory, "async") && + arrowReturnTypeAnnotation(factory) && + isParameterlessArrow(factory) + ) { + return `(${factory.text()})()`; + } + + const invoked = invokeFactoryText(call, factory); + return callTypeArgumentsText(call) && !arrowReturnTypeAnnotation(factory) + ? applyTypeAssertion(call, invoked) + : invoked; +} + +function memoizationResultText( + match: MemoizationCall, selectedCallIds: Set, -): SgNode | null { - const firstArg = callArguments(call)[0] ?? null; + namedImports: Map, + reactObjectNames: Set, +): string | null { + const firstArg = callArguments(match.call)[0] ?? null; if (!firstArg) { return null; } - if (selectedCallIds.has(firstArg.id())) { - return firstMemoizationArgument(firstArg, selectedCallIds); + // Nested memoization: convert the inner call first, then apply this layer's + // semantics to that result (useMemo invokes it; useCallback/memo keep it). + if (selectedCallIds.has(firstArg.id()) && firstArg.kind() === "call_expression") { + const inner = memoizationCallInfo(firstArg, namedImports, reactObjectNames); + if (!inner) { + return null; + } + + const innerResult = memoizationResultText(inner, selectedCallIds, namedImports, reactObjectNames); + if (innerResult === null) { + return null; + } + + if (match.importedName !== "useMemo") { + return innerResult; + } + + // Prefer AST-based unwrapping when the inner result is still an extractable + // factory node (common for useCallback/memo wrappers). Otherwise invoke the + // inner result text as the outer factory. + if (inner.importedName !== "useMemo") { + const innerFactory = callArguments(inner.call)[0]; + if (innerFactory) { + return memoFactoryResultText(match.call, innerFactory); + } + } + + // Inner was useMemo: its result text is the outer factory value. If that + // text is itself a parameterless expression-bodied arrow we can unwrap + // once more via a lightweight parse of the common `() => expr` shape by + // looking at the inner factory's extractable result and treating *that* + // as the outer factory. + const innerFactory = callArguments(inner.call)[0]; + if (innerFactory) { + const innerExtracted = extractableUseMemoResult(unwrapParentheses(innerFactory)); + if (innerExtracted) { + return memoFactoryResultText(match.call, innerExtracted); + } + } + + return wrapRawExpressionText(match.call, `(${innerResult})()`, true); + } + + if (match.importedName === "useMemo") { + return memoFactoryResultText(match.call, firstArg); } - return firstArg; + return firstArg.text(); } -function replacementText(call: SgNode, selectedCallIds: Set): string | null { - return firstMemoizationArgument(call, selectedCallIds)?.text() ?? null; +function replacementText( + match: MemoizationCall, + selectedCallIds: Set, + namedImports: Map, + reactObjectNames: Set, +): string | null { + return memoizationResultText(match, selectedCallIds, namedImports, reactObjectNames); } function keptRanges(call: SgNode, selectedCallIds: Set): Range[] { @@ -261,11 +508,56 @@ function consumeFollowingBlankLine(edit: Edit, programText: string): Edit { return edit; } +function specifierLocalName(specifier: SgNode): string { + return specifier.field("alias")?.text() ?? specifier.field("name")?.text() ?? ""; +} + +// Removes several named specifiers from one statement that keeps at least one. +// Specifiers are matched by local binding name (alias or exported name) so two +// aliases of the same export can be removed independently. `removeImport` +// takes a separator with each specifier, so removing neighbours +// (`useCallback, useMemo`) one by one would produce overlapping edits. Each +// run of removed specifiers is instead cut up to the next kept one, or back +// to the previous kept one when the run is last, preserving formatting. +function removeSpecifierEdits(statement: SgNode, unusedLocalNames: string[]): Edit[] { + const unused = new Set(unusedLocalNames); + const specifiers = statement.findAll({ rule: { kind: "import_specifier" } }); + const removed = specifiers.map((specifier) => unused.has(specifierLocalName(specifier))); + const edits: Edit[] = []; + + for (let index = 0; index < specifiers.length; index++) { + if (!removed[index] || removed[index - 1]) { + continue; + } + + let runEnd = index; + while (removed[runEnd + 1]) { + runEnd++; + } + + const next = specifiers[runEnd + 1]; + const previous = specifiers[index - 1]; + if (next) { + edits.push({ startPos: nodeRange(specifiers[index]!).start, endPos: nodeRange(next).start, insertedText: "" }); + } else if (previous) { + edits.push({ startPos: nodeRange(previous).end, endPos: nodeRange(specifiers[runEnd]!).end, insertedText: "" }); + } + } + + return edits; +} + function cleanupUnusedImports( rootNode: SgNode, usedImportLocals: Set, + referencedImportLocals: Set, edits: Edit[], ): void { + // A default or namespace import is only removed when this codemod is what + // made it unused; one that was already unused is left in place. + const keepObjectImport = (alias: string) => + usedImportLocals.has(alias) || !referencedImportLocals.has(alias); + const programText = rootNode.text(); const handledStatements = new Set(); @@ -280,7 +572,10 @@ function cleanupUnusedImports( continue; } - const unusedInStatement: string[] = []; + // Track unused *local* bindings, not exported names, so `useMemo as a` + // can be removed while `useMemo as b` remains. + const unusedLocalsInStatement: string[] = []; + const unusedExportNames = new Set(); for (const memoName of MEMOIZATION_NAMES) { for (const sibling of getAllImports(rootNode, { type: "named", @@ -291,22 +586,24 @@ function cleanupUnusedImports( continue; } if (importStatementForBinding(sibling.node)?.id() === statement.id()) { - unusedInStatement.push(memoName); + unusedLocalsInStatement.push(sibling.alias); + unusedExportNames.add(memoName); } } } - const removingAllNamed = unusedInStatement.length >= namedSpecifierCount(statement); + const removingAllNamed = unusedLocalsInStatement.length >= namedSpecifierCount(statement); const binding = defaultOrNamespaceLocal(statement); - const keepBinding = binding ? usedImportLocals.has(binding.alias) : false; + const keepBinding = binding ? keepObjectImport(binding.alias) : false; - if (removingAllNamed && !keepBinding) { - // Drop the whole statement in one edit (avoids overlapping default+named removals). + if (removingAllNamed) { + // removeImport drops the whole statement, or only the `, { ... }` clause + // when a default/namespace binding must stay. const removeEdit = removeImport(rootNode, { type: "named", - specifiers: unusedInStatement, + specifiers: [...unusedExportNames], from: "react", - }) ?? (binding + }) ?? (!keepBinding && binding ? removeImport(rootNode, { type: binding.isNamespace ? "namespace" : "default", from: "react", @@ -320,23 +617,19 @@ function cleanupUnusedImports( continue; } - const removeEdit = removeImport(rootNode, { - type: "named", - specifiers: removingAllNamed ? unusedInStatement : [name], - from: "react", - }); - - if (removeEdit) { - edits.push(consumeFollowingBlankLine(removeEdit, programText)); - if (removingAllNamed) { - handledStatements.add(statement.id()); - } + // Partial removal: match by local binding so duplicate export aliases + // (`useMemo as a` vs `useMemo as b`) can be removed independently. + // removeImport matches the first export name and would delete the wrong one. + if (unusedLocalsInStatement.length > 0) { + edits.push(...removeSpecifierEdits(statement, unusedLocalsInStatement)); + handledStatements.add(statement.id()); + continue; } } } for (const imp of getAllImports(rootNode, { type: "default", from: "react" })) { - if (usedImportLocals.has(imp.alias)) { + if (keepObjectImport(imp.alias)) { continue; } @@ -398,7 +691,7 @@ const transform: Transform = async (root) => { const metricCounts = new Map(); for (const match of selectedMatches) { - const replacement = replacementText(match.call, selectedCallIds); + const replacement = replacementText(match, selectedCallIds, namedImports, reactObjectNames); if (!replacement) { continue; } @@ -423,7 +716,9 @@ const transform: Transform = async (root) => { selectedCallIds, ); - cleanupUnusedImports(rootNode, usedImportLocals, edits); + const referencedImportLocals = collectUsedImportLocals(rootNode, importedLocalNames, [], selectedCallIds); + + cleanupUnusedImports(rootNode, usedImportLocals, referencedImportLocals, edits); for (const [pattern, count] of metricCounts) { metric.increment({ diff --git a/codemods/remove-memoization/tests/aliased-named-imports/expected.tsx b/codemods/remove-memoization/tests/aliased-named-imports/expected.tsx index 4074508..852861a 100644 --- a/codemods/remove-memoization/tests/aliased-named-imports/expected.tsx +++ b/codemods/remove-memoization/tests/aliased-named-imports/expected.tsx @@ -1,3 +1,3 @@ -const value = () => computeValue(); +const value = computeValue(); const handler = () => value; const MemoizedWidget = Widget; diff --git a/codemods/remove-memoization/tests/default-import-kept-named-removed/expected.tsx b/codemods/remove-memoization/tests/default-import-kept-named-removed/expected.tsx new file mode 100644 index 0000000..00b6e06 --- /dev/null +++ b/codemods/remove-memoization/tests/default-import-kept-named-removed/expected.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +const value = 1; +const handler = () => 2; +export const element = ; diff --git a/codemods/remove-memoization/tests/default-import-kept-named-removed/input.tsx b/codemods/remove-memoization/tests/default-import-kept-named-removed/input.tsx new file mode 100644 index 0000000..c799aac --- /dev/null +++ b/codemods/remove-memoization/tests/default-import-kept-named-removed/input.tsx @@ -0,0 +1,5 @@ +import React, { useMemo, useCallback } from 'react'; + +const value = useMemo(() => 1, []); +const handler = useCallback(() => 2, []); +export const element = ; diff --git a/codemods/remove-memoization/tests/default-import-kept-named-removed/metrics.json b/codemods/remove-memoization/tests/default-import-kept-named-removed/metrics.json new file mode 100644 index 0000000..bbc5557 --- /dev/null +++ b/codemods/remove-memoization/tests/default-import-kept-named-removed/metrics.json @@ -0,0 +1,18 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/default-import-kept-named-removed/input.tsx", + "pattern": "useCallback" + }, + "count": 1 + }, + { + "cardinality": { + "file": "tests/default-import-kept-named-removed/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/default-import-removed/expected.tsx b/codemods/remove-memoization/tests/default-import-removed/expected.tsx new file mode 100644 index 0000000..10dc773 --- /dev/null +++ b/codemods/remove-memoization/tests/default-import-removed/expected.tsx @@ -0,0 +1 @@ +const value = computeValue(); diff --git a/codemods/remove-memoization/tests/default-import-removed/input.tsx b/codemods/remove-memoization/tests/default-import-removed/input.tsx new file mode 100644 index 0000000..a87c144 --- /dev/null +++ b/codemods/remove-memoization/tests/default-import-removed/input.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +const value = React.useMemo(() => computeValue(), [computeValue]); diff --git a/codemods/remove-memoization/tests/default-import-removed/metrics.json b/codemods/remove-memoization/tests/default-import-removed/metrics.json new file mode 100644 index 0000000..7d9d92a --- /dev/null +++ b/codemods/remove-memoization/tests/default-import-removed/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/default-import-removed/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/expected.tsx b/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/expected.tsx new file mode 100644 index 0000000..aaf0237 --- /dev/null +++ b/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/expected.tsx @@ -0,0 +1,6 @@ +import { useMemo as b, useState } from 'react'; + +const value = computeValue(); +const handler = () => value; +const stillUsed = b; +const [state] = useState(0); diff --git a/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/input.tsx b/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/input.tsx new file mode 100644 index 0000000..3c0b76f --- /dev/null +++ b/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/input.tsx @@ -0,0 +1,6 @@ +import { useMemo as a, useMemo as b, useCallback, useState } from 'react'; + +const value = a(() => computeValue(), [computeValue]); +const handler = useCallback(() => value, [value]); +const stillUsed = b; +const [state] = useState(0); diff --git a/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/metrics.json b/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/metrics.json new file mode 100644 index 0000000..95ec9a3 --- /dev/null +++ b/codemods/remove-memoization/tests/duplicate-usememo-alias-preserved/metrics.json @@ -0,0 +1,18 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/duplicate-usememo-alias-preserved/input.tsx", + "pattern": "useCallback" + }, + "count": 1 + }, + { + "cardinality": { + "file": "tests/duplicate-usememo-alias-preserved/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/expected.tsx b/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/expected.tsx new file mode 100644 index 0000000..a041357 --- /dev/null +++ b/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/expected.tsx @@ -0,0 +1,16 @@ +import { + createContext, + useState, + useContext, +} from 'react'; +import { useRef } from 'react'; + +const Context = createContext(null); +const value = 1; +const handler = () => 2; +const Memoized = Widget; +const cached = 3; +const stable = () => 4; +const [state] = useState(0); +const theme = useContext(Context); +const ref = useRef(null); diff --git a/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/input.tsx b/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/input.tsx new file mode 100644 index 0000000..82cc751 --- /dev/null +++ b/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/input.tsx @@ -0,0 +1,19 @@ +import { + useCallback, + createContext, + useMemo, + memo, + useState, + useContext, +} from 'react'; +import { useMemo as useCached, useCallback as useStable, useRef } from 'react'; + +const Context = createContext(null); +const value = useMemo(() => 1, []); +const handler = useCallback(() => 2, []); +const Memoized = memo(Widget); +const cached = useCached(() => 3, []); +const stable = useStable(() => 4, []); +const [state] = useState(0); +const theme = useContext(Context); +const ref = useRef(null); diff --git a/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/metrics.json b/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/metrics.json new file mode 100644 index 0000000..076523e --- /dev/null +++ b/codemods/remove-memoization/tests/multiple-memo-specifiers-removed/metrics.json @@ -0,0 +1,25 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/multiple-memo-specifiers-removed/input.tsx", + "pattern": "memo" + }, + "count": 1 + }, + { + "cardinality": { + "file": "tests/multiple-memo-specifiers-removed/input.tsx", + "pattern": "useCallback" + }, + "count": 2 + }, + { + "cardinality": { + "file": "tests/multiple-memo-specifiers-removed/input.tsx", + "pattern": "useMemo" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/multiple-react-imports/expected.tsx b/codemods/remove-memoization/tests/multiple-react-imports/expected.tsx index 1b1b8a0..d717f5a 100644 --- a/codemods/remove-memoization/tests/multiple-react-imports/expected.tsx +++ b/codemods/remove-memoization/tests/multiple-react-imports/expected.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -const value = () => computeValue(); +const value = computeValue(); const handler = () => value; const [state] = useState(0); diff --git a/codemods/remove-memoization/tests/no-semicolon-keep-sibling/expected.tsx b/codemods/remove-memoization/tests/no-semicolon-keep-sibling/expected.tsx index 10d1606..81683ab 100644 --- a/codemods/remove-memoization/tests/no-semicolon-keep-sibling/expected.tsx +++ b/codemods/remove-memoization/tests/no-semicolon-keep-sibling/expected.tsx @@ -1,3 +1,3 @@ import { useState } from "react" -const v = () => 1 +const v = 1 const [s] = useState(0) diff --git a/codemods/remove-memoization/tests/react-member-calls/expected.tsx b/codemods/remove-memoization/tests/react-member-calls/expected.tsx index 1521295..d6ac7ae 100644 --- a/codemods/remove-memoization/tests/react-member-calls/expected.tsx +++ b/codemods/remove-memoization/tests/react-member-calls/expected.tsx @@ -3,10 +3,10 @@ import React from 'react'; function Component() { const state = React.useState(); - const example1 = () => { + const example1 = (() => { const diff = today.diff(selectedDate, "days"); return diff > 3 || diff < -3; - }; + })(); const example2 = () => { const diff = today.diff(selectedDate, "days"); diff --git a/codemods/remove-memoization/tests/type-only-import-preserved/expected.tsx b/codemods/remove-memoization/tests/type-only-import-preserved/expected.tsx new file mode 100644 index 0000000..2d0991e --- /dev/null +++ b/codemods/remove-memoization/tests/type-only-import-preserved/expected.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from 'react' +import { + createContext, + useState, +} from 'react' + +const Context = createContext({ open: false }) + +export const Panel = ({ children }: { children: ReactNode }) => { + const [open] = useState(false) + const value = { open } + return {children} +} diff --git a/codemods/remove-memoization/tests/type-only-import-preserved/input.tsx b/codemods/remove-memoization/tests/type-only-import-preserved/input.tsx new file mode 100644 index 0000000..4a47a71 --- /dev/null +++ b/codemods/remove-memoization/tests/type-only-import-preserved/input.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from 'react' +import { + createContext, + useMemo, + useState, +} from 'react' + +const Context = createContext({ open: false }) + +export const Panel = ({ children }: { children: ReactNode }) => { + const [open] = useState(false) + const value = useMemo(() => ({ open }), [open]) + return {children} +} diff --git a/codemods/remove-memoization/tests/type-only-import-preserved/metrics.json b/codemods/remove-memoization/tests/type-only-import-preserved/metrics.json new file mode 100644 index 0000000..86b60c0 --- /dev/null +++ b/codemods/remove-memoization/tests/type-only-import-preserved/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/type-only-import-preserved/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/type-only-import-removed/expected.tsx b/codemods/remove-memoization/tests/type-only-import-removed/expected.tsx new file mode 100644 index 0000000..c1f2f0b --- /dev/null +++ b/codemods/remove-memoization/tests/type-only-import-removed/expected.tsx @@ -0,0 +1,7 @@ +import type { ComponentProps, ReactNode } from 'react'; +import { type FC } from 'react'; + +const Label: FC<{ children: ReactNode }> = ({ children }) => { + const style = { fontWeight: 'bold' }; + return {children}; +}; diff --git a/codemods/remove-memoization/tests/type-only-import-removed/input.tsx b/codemods/remove-memoization/tests/type-only-import-removed/input.tsx new file mode 100644 index 0000000..a2b7c11 --- /dev/null +++ b/codemods/remove-memoization/tests/type-only-import-removed/input.tsx @@ -0,0 +1,7 @@ +import type { ComponentProps, ReactNode } from 'react'; +import { useMemo, type FC } from 'react'; + +const Label: FC<{ children: ReactNode }> = ({ children }) => { + const style = useMemo(() => ({ fontWeight: 'bold' }), []); + return {children}; +}; diff --git a/codemods/remove-memoization/tests/type-only-import-removed/metrics.json b/codemods/remove-memoization/tests/type-only-import-removed/metrics.json new file mode 100644 index 0000000..c05f776 --- /dev/null +++ b/codemods/remove-memoization/tests/type-only-import-removed/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/type-only-import-removed/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/typescript-react-member-calls/expected.tsx b/codemods/remove-memoization/tests/typescript-react-member-calls/expected.tsx index 684d6f3..336e85d 100644 --- a/codemods/remove-memoization/tests/typescript-react-member-calls/expected.tsx +++ b/codemods/remove-memoization/tests/typescript-react-member-calls/expected.tsx @@ -3,10 +3,10 @@ import React from 'react'; function Component({ url }: { url: string }) { const state = React.useState(); - const example1 = () => { + const example1 = (() => { const diff = today.diff(selectedDate, "days"); return diff > 3 || diff < -3; - }; + })(); const example2 = () => { const diff = today.diff(selectedDate, "days"); diff --git a/codemods/remove-memoization/tests/typescript-use-memo/expected.tsx b/codemods/remove-memoization/tests/typescript-use-memo/expected.tsx index 5c8db40..b5c0801 100644 --- a/codemods/remove-memoization/tests/typescript-use-memo/expected.tsx +++ b/codemods/remove-memoization/tests/typescript-use-memo/expected.tsx @@ -1,6 +1,6 @@ function Component({ url }: { url: string }) { - const selectedDateMin3DaysDifference = () => { + const selectedDateMin3DaysDifference = (() => { const diff = today.diff(selectedDate, "days"); return diff > 3 || diff < -3; - }; + })(); } diff --git a/codemods/remove-memoization/tests/unused-default-import-preserved/expected.tsx b/codemods/remove-memoization/tests/unused-default-import-preserved/expected.tsx new file mode 100644 index 0000000..e89efb0 --- /dev/null +++ b/codemods/remove-memoization/tests/unused-default-import-preserved/expected.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import { useState } from 'react'; + +const [state] = useState(0); +const value = state; diff --git a/codemods/remove-memoization/tests/unused-default-import-preserved/input.tsx b/codemods/remove-memoization/tests/unused-default-import-preserved/input.tsx new file mode 100644 index 0000000..22574f3 --- /dev/null +++ b/codemods/remove-memoization/tests/unused-default-import-preserved/input.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import { useMemo, useState } from 'react'; + +const [state] = useState(0); +const value = useMemo(() => state, [state]); diff --git a/codemods/remove-memoization/tests/unused-default-import-preserved/metrics.json b/codemods/remove-memoization/tests/unused-default-import-preserved/metrics.json new file mode 100644 index 0000000..4311762 --- /dev/null +++ b/codemods/remove-memoization/tests/unused-default-import-preserved/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/unused-default-import-preserved/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-bare-contexts/expected.tsx b/codemods/remove-memoization/tests/use-memo-bare-contexts/expected.tsx new file mode 100644 index 0000000..350b0a5 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-bare-contexts/expected.tsx @@ -0,0 +1,11 @@ +let total; +total = a + b; + +function sum() { + return a + b; +} + +report(a + b); +const list = [a + b]; +const object = { sum: a + b }; +const grouped = (a + b); diff --git a/codemods/remove-memoization/tests/use-memo-bare-contexts/input.tsx b/codemods/remove-memoization/tests/use-memo-bare-contexts/input.tsx new file mode 100644 index 0000000..777d11f --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-bare-contexts/input.tsx @@ -0,0 +1,13 @@ +import { useMemo } from 'react'; + +let total; +total = useMemo(() => a + b, [a, b]); + +function sum() { + return useMemo(() => a + b, [a, b]); +} + +report(useMemo(() => a + b, [a, b])); +const list = [useMemo(() => a + b, [a, b])]; +const object = { sum: useMemo(() => a + b, [a, b]) }; +const grouped = (useMemo(() => a + b, [a, b])); diff --git a/codemods/remove-memoization/tests/use-memo-bare-contexts/metrics.json b/codemods/remove-memoization/tests/use-memo-bare-contexts/metrics.json new file mode 100644 index 0000000..fedb49e --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-bare-contexts/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-bare-contexts/input.tsx", + "pattern": "useMemo" + }, + "count": 6 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/expected.tsx b/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/expected.tsx new file mode 100644 index 0000000..a442c39 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/expected.tsx @@ -0,0 +1,19 @@ +const prepared = (() => { + const value = computeValue(); + return value; +})(); + +const commented = (() => { + // cached because computeValue is slow + return computeValue(); +})(); + +const sideEffect = (() => { + computeValue(); +})(); + +const empty = (() => {})(); + +const fromFunction = (function () { + return computeValue(); +})(); diff --git a/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/input.tsx b/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/input.tsx new file mode 100644 index 0000000..661d2e1 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/input.tsx @@ -0,0 +1,21 @@ +import { useMemo } from 'react'; + +const prepared = useMemo(() => { + const value = computeValue(); + return value; +}, [computeValue]); + +const commented = useMemo(() => { + // cached because computeValue is slow + return computeValue(); +}, [computeValue]); + +const sideEffect = useMemo(() => { + computeValue(); +}, [computeValue]); + +const empty = useMemo(() => {}, []); + +const fromFunction = useMemo(function () { + return computeValue(); +}, [computeValue]); diff --git a/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/metrics.json b/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/metrics.json new file mode 100644 index 0000000..c872518 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-block-kept-as-iife/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-block-kept-as-iife/input.tsx", + "pattern": "useMemo" + }, + "count": 5 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-block-return/expected.tsx b/codemods/remove-memoization/tests/use-memo-block-return/expected.tsx new file mode 100644 index 0000000..10dc773 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-block-return/expected.tsx @@ -0,0 +1 @@ +const value = computeValue(); diff --git a/codemods/remove-memoization/tests/use-memo-block-return/input.tsx b/codemods/remove-memoization/tests/use-memo-block-return/input.tsx new file mode 100644 index 0000000..8b8778d --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-block-return/input.tsx @@ -0,0 +1,5 @@ +import { useMemo } from 'react'; + +const value = useMemo(() => { + return computeValue(); +}, [computeValue]); diff --git a/codemods/remove-memoization/tests/use-memo-block-return/metrics.json b/codemods/remove-memoization/tests/use-memo-block-return/metrics.json new file mode 100644 index 0000000..1fb7f3d --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-block-return/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-block-return/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-expression-body/expected.tsx b/codemods/remove-memoization/tests/use-memo-expression-body/expected.tsx new file mode 100644 index 0000000..0569f7f --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-expression-body/expected.tsx @@ -0,0 +1,15 @@ +import { createContext } from 'react'; + +const Context = createContext(null); + +function Component({ items, onSelect }) { + const count = items.length; + const context = { items, count }; + const label = count === 1 ? 'item' : 'items'; + const handler = () => onSelect(items); + const total = 1 + (count * 2); + const value = compute(); + const lazy = (async () => fetchValue())(); + + return {label}; +} diff --git a/codemods/remove-memoization/tests/use-memo-expression-body/input.tsx b/codemods/remove-memoization/tests/use-memo-expression-body/input.tsx new file mode 100644 index 0000000..a09ed1d --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-expression-body/input.tsx @@ -0,0 +1,15 @@ +import { createContext, useCallback, useMemo } from 'react'; + +const Context = createContext(null); + +function Component({ items, onSelect }) { + const count = useMemo(() => items.length, [items]); + const context = useMemo(() => ({ items, count }), [items, count]); + const label = useMemo(() => (count === 1 ? 'item' : 'items'), [count]); + const handler = useCallback(() => onSelect(items), [onSelect, items]); + const total = 1 + useMemo(() => count * 2, [count]); + const value = useMemo(compute, [compute]); + const lazy = useMemo(async () => fetchValue(), []); + + return ({ handler }), [handler])}>{label}; +} diff --git a/codemods/remove-memoization/tests/use-memo-expression-body/metrics.json b/codemods/remove-memoization/tests/use-memo-expression-body/metrics.json new file mode 100644 index 0000000..268c7d1 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-expression-body/metrics.json @@ -0,0 +1,18 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-expression-body/input.tsx", + "pattern": "useCallback" + }, + "count": 1 + }, + { + "cardinality": { + "file": "tests/use-memo-expression-body/input.tsx", + "pattern": "useMemo" + }, + "count": 7 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-nested-calls/expected.tsx b/codemods/remove-memoization/tests/use-memo-nested-calls/expected.tsx new file mode 100644 index 0000000..7f80933 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-nested-calls/expected.tsx @@ -0,0 +1,3 @@ +const nestedMemos = 42; +const memoOverCallback = 42; +const callbackOverMemo = () => 42; diff --git a/codemods/remove-memoization/tests/use-memo-nested-calls/input.tsx b/codemods/remove-memoization/tests/use-memo-nested-calls/input.tsx new file mode 100644 index 0000000..f659638 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-nested-calls/input.tsx @@ -0,0 +1,5 @@ +import { useCallback, useMemo } from 'react'; + +const nestedMemos = useMemo(useMemo(() => () => 42, []), []); +const memoOverCallback = useMemo(useCallback(() => 42, []), []); +const callbackOverMemo = useCallback(useMemo(() => () => 42, []), []); diff --git a/codemods/remove-memoization/tests/use-memo-nested-calls/metrics.json b/codemods/remove-memoization/tests/use-memo-nested-calls/metrics.json new file mode 100644 index 0000000..eec01d3 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-nested-calls/metrics.json @@ -0,0 +1,18 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-nested-calls/input.tsx", + "pattern": "useCallback" + }, + "count": 1 + }, + { + "cardinality": { + "file": "tests/use-memo-nested-calls/input.tsx", + "pattern": "useMemo" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-parameterized-factory/expected.tsx b/codemods/remove-memoization/tests/use-memo-parameterized-factory/expected.tsx new file mode 100644 index 0000000..83d9500 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-parameterized-factory/expected.tsx @@ -0,0 +1,4 @@ +const withDefault = ((x = computeValue()) => x)(); +const withParam = ((x) => x)(); +const withRest = ((...args) => args.length)(); +const withDestructure = (({ value } = defaults) => value)(); diff --git a/codemods/remove-memoization/tests/use-memo-parameterized-factory/input.tsx b/codemods/remove-memoization/tests/use-memo-parameterized-factory/input.tsx new file mode 100644 index 0000000..1f122c5 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-parameterized-factory/input.tsx @@ -0,0 +1,6 @@ +import { useMemo } from 'react'; + +const withDefault = useMemo((x = computeValue()) => x, []); +const withParam = useMemo((x) => x, []); +const withRest = useMemo((...args) => args.length, []); +const withDestructure = useMemo(({ value } = defaults) => value, [defaults]); diff --git a/codemods/remove-memoization/tests/use-memo-parameterized-factory/metrics.json b/codemods/remove-memoization/tests/use-memo-parameterized-factory/metrics.json new file mode 100644 index 0000000..5f1693c --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-parameterized-factory/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-parameterized-factory/input.tsx", + "pattern": "useMemo" + }, + "count": 4 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-parenthesized-factory/expected.tsx b/codemods/remove-memoization/tests/use-memo-parenthesized-factory/expected.tsx new file mode 100644 index 0000000..10dc773 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-parenthesized-factory/expected.tsx @@ -0,0 +1 @@ +const value = computeValue(); diff --git a/codemods/remove-memoization/tests/use-memo-parenthesized-factory/input.tsx b/codemods/remove-memoization/tests/use-memo-parenthesized-factory/input.tsx new file mode 100644 index 0000000..f7c8d0c --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-parenthesized-factory/input.tsx @@ -0,0 +1,3 @@ +import { useMemo } from 'react'; + +const value = useMemo((() => computeValue()), [computeValue]); diff --git a/codemods/remove-memoization/tests/use-memo-parenthesized-factory/metrics.json b/codemods/remove-memoization/tests/use-memo-parenthesized-factory/metrics.json new file mode 100644 index 0000000..42377e8 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-parenthesized-factory/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-parenthesized-factory/input.tsx", + "pattern": "useMemo" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-preserve-types/expected.tsx b/codemods/remove-memoization/tests/use-memo-preserve-types/expected.tsx new file mode 100644 index 0000000..3290aa8 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-preserve-types/expected.tsx @@ -0,0 +1,5 @@ +type Config = { mode: 'on' | 'off' }; + +const fromReturnType = ((): Config => ({ mode: 'on' }))(); +const fromGeneric = ({ mode: 'on' }) as Config; +const fromBoth = ((): Config => ({ mode: 'off' }))(); diff --git a/codemods/remove-memoization/tests/use-memo-preserve-types/input.tsx b/codemods/remove-memoization/tests/use-memo-preserve-types/input.tsx new file mode 100644 index 0000000..b689a81 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-preserve-types/input.tsx @@ -0,0 +1,7 @@ +import { useMemo } from 'react'; + +type Config = { mode: 'on' | 'off' }; + +const fromReturnType = useMemo((): Config => ({ mode: 'on' }), []); +const fromGeneric = useMemo(() => ({ mode: 'on' }), []); +const fromBoth = useMemo((): Config => ({ mode: 'off' }), []); diff --git a/codemods/remove-memoization/tests/use-memo-preserve-types/metrics.json b/codemods/remove-memoization/tests/use-memo-preserve-types/metrics.json new file mode 100644 index 0000000..736f339 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-preserve-types/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-preserve-types/input.tsx", + "pattern": "useMemo" + }, + "count": 3 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-sequence-expression/expected.tsx b/codemods/remove-memoization/tests/use-memo-sequence-expression/expected.tsx new file mode 100644 index 0000000..19e0c4c --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-sequence-expression/expected.tsx @@ -0,0 +1,2 @@ +const fromBody = (track(), computeValue()); +const fromReturn = (track(), computeValue()); diff --git a/codemods/remove-memoization/tests/use-memo-sequence-expression/input.tsx b/codemods/remove-memoization/tests/use-memo-sequence-expression/input.tsx new file mode 100644 index 0000000..9e25118 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-sequence-expression/input.tsx @@ -0,0 +1,6 @@ +import { useMemo } from 'react'; + +const fromBody = useMemo(() => (track(), computeValue()), [computeValue]); +const fromReturn = useMemo(() => { + return track(), computeValue(); +}, [computeValue]); diff --git a/codemods/remove-memoization/tests/use-memo-sequence-expression/metrics.json b/codemods/remove-memoization/tests/use-memo-sequence-expression/metrics.json new file mode 100644 index 0000000..6c982ee --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-sequence-expression/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-sequence-expression/input.tsx", + "pattern": "useMemo" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo-unbound-member/expected.tsx b/codemods/remove-memoization/tests/use-memo-unbound-member/expected.tsx new file mode 100644 index 0000000..9e0d75e --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-unbound-member/expected.tsx @@ -0,0 +1,3 @@ +const fromMember = (0, obj.compute)(); +const fromNestedMember = (0, helpers.obj.compute)(); +const fromParenMember = (0, obj.compute)(); diff --git a/codemods/remove-memoization/tests/use-memo-unbound-member/input.tsx b/codemods/remove-memoization/tests/use-memo-unbound-member/input.tsx new file mode 100644 index 0000000..c6ff0c4 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-unbound-member/input.tsx @@ -0,0 +1,5 @@ +import { useMemo } from 'react'; + +const fromMember = useMemo(obj.compute, []); +const fromNestedMember = useMemo(helpers.obj.compute, []); +const fromParenMember = useMemo((obj.compute), []); diff --git a/codemods/remove-memoization/tests/use-memo-unbound-member/metrics.json b/codemods/remove-memoization/tests/use-memo-unbound-member/metrics.json new file mode 100644 index 0000000..6a5f294 --- /dev/null +++ b/codemods/remove-memoization/tests/use-memo-unbound-member/metrics.json @@ -0,0 +1,11 @@ +{ + "remove-memoization-replacements": [ + { + "cardinality": { + "file": "tests/use-memo-unbound-member/input.tsx", + "pattern": "useMemo" + }, + "count": 3 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-memoization/tests/use-memo/expected.tsx b/codemods/remove-memoization/tests/use-memo/expected.tsx index bafc971..58abe84 100644 --- a/codemods/remove-memoization/tests/use-memo/expected.tsx +++ b/codemods/remove-memoization/tests/use-memo/expected.tsx @@ -1,6 +1,6 @@ function Component() { - const selectedDateMin3DaysDifference = () => { + const selectedDateMin3DaysDifference = (() => { const diff = today.diff(selectedDate, "days"); return diff > 3 || diff < -3; - }; + })(); }