diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index cc600d2085e50..ab0e71b83cfc7 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -132,9 +132,9 @@ jobs: mkdir ./compiled/facebook-www/__test_utils__ mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js - # Move eslint-plugin-react-hooks into eslint-plugin-react-hooks + # Copy eslint-plugin-react-hooks mkdir ./compiled/eslint-plugin-react-hooks - mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ + cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ ./compiled/eslint-plugin-react-hooks/index.js # Move unstable_server-external-runtime.js into facebook-www @@ -167,6 +167,12 @@ jobs: rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js + # Copy eslint-plugin-react-hooks + # NOTE: This is different from www, here we include the full package + # including package.json to include dependencies in fbsource. + mkdir "$BASE_FOLDER/tools" + cp -r build/oss-experimental/eslint-plugin-react-hooks "$BASE_FOLDER/tools" + # Move React Native version file mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 8dfdc76978cc2..831d1ca38054e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -103,6 +103,7 @@ import {transformFire} from '../Transform'; import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender'; import {CompilerError} from '..'; import {validateStaticComponents} from '../Validation/ValidateStaticComponents'; +import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -274,6 +275,10 @@ function runWithEnvironment( if (env.config.validateNoImpureFunctionsInRender) { validateNoImpureFunctionsInRender(hir).unwrap(); } + + if (env.config.validateNoFreezingKnownMutableFunctions) { + validateNoFreezingKnownMutableFunctions(hir).unwrap(); + } } inferReactivePlaces(hir); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 276e4f7b404d3..a487b5086c0fe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -367,6 +367,11 @@ const EnvironmentConfigSchema = z.object({ */ validateNoImpureFunctionsInRender: z.boolean().default(false), + /** + * Validate against passing mutable functions to hooks + */ + validateNoFreezingKnownMutableFunctions: z.boolean().default(false), + /* * When enabled, the compiler assumes that hooks follow the Rules of React: * - Hooks may memoize computation based on any of their parameters, thus diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 7ed42cbce132a..b8504494662d6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -25,6 +25,9 @@ import { BuiltInUseRefId, BuiltInUseStateId, BuiltInUseTransitionId, + BuiltInWeakMapId, + BuiltInWeakSetId, + ReanimatedSharedValueId, ShapeRegistry, addFunction, addHook, @@ -491,6 +494,38 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ true, ), ], + [ + 'WeakMap', + addFunction( + DEFAULT_SHAPES, + [], + { + positionalParams: [Effect.ConditionallyMutateIterator], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInWeakMapId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + }, + null, + true, + ), + ], + [ + 'WeakSet', + addFunction( + DEFAULT_SHAPES, + [], + { + positionalParams: [Effect.ConditionallyMutateIterator], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInWeakSetId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + }, + null, + true, + ), + ], // TODO: rest of Global objects ]; @@ -908,7 +943,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType { addHook(registry, { positionalParams: [], restParam: Effect.Freeze, - returnType: {kind: 'Poly'}, + returnType: {kind: 'Object', shapeId: ReanimatedSharedValueId}, returnValueKind: ValueKind.Mutable, noAlias: true, calleeEffect: Effect.Read, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 611b5bd210226..99b8c189ee0fd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1725,6 +1725,18 @@ export function isRefOrRefValue(id: Identifier): boolean { return isUseRefType(id) || isRefValueType(id); } +/* + * Returns true if the type is a Ref or a custom user type that acts like a ref when it + * shouldn't. For now the only other case of this is Reanimated's shared values. + */ +export function isRefOrRefLikeMutableType(type: Type): boolean { + return ( + type.kind === 'Object' && + (type.shapeId === 'BuiltInUseRefId' || + type.shapeId == 'ReanimatedSharedValueId') + ); +} + export function isSetStateType(id: Identifier): boolean { return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState'; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index a599fc2d74760..03f4120149b0e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -203,6 +203,8 @@ export const BuiltInPropsId = 'BuiltInProps'; export const BuiltInArrayId = 'BuiltInArray'; export const BuiltInSetId = 'BuiltInSet'; export const BuiltInMapId = 'BuiltInMap'; +export const BuiltInWeakSetId = 'BuiltInWeakSet'; +export const BuiltInWeakMapId = 'BuiltInWeakMap'; export const BuiltInFunctionId = 'BuiltInFunction'; export const BuiltInJsxId = 'BuiltInJsx'; export const BuiltInObjectId = 'BuiltInObject'; @@ -225,6 +227,9 @@ export const BuiltInStartTransitionId = 'BuiltInStartTransition'; export const BuiltInFireId = 'BuiltInFire'; export const BuiltInFireFunctionId = 'BuiltInFireFunction'; +// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types +export const ReanimatedSharedValueId = 'ReanimatedSharedValueId'; + // ShapeRegistry with default definitions for built-ins. export const BUILTIN_SHAPES: ShapeRegistry = new Map(); @@ -764,6 +769,101 @@ addObject(BUILTIN_SHAPES, BuiltInMapId, [ ], ]); +addObject(BUILTIN_SHAPES, BuiltInWeakSetId, [ + [ + /** + * add(value) + * Parameters + * value: the value of the element to add to the Set object. + * Returns the Set object with added value. + */ + 'add', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInWeakSetId}, + calleeEffect: Effect.Store, + // returnValueKind is technically dependent on the ValueKind of the set itself + returnValueKind: ValueKind.Mutable, + }), + ], + [ + /** + * setInstance.delete(value) + * Returns true if value was already in Set; otherwise false. + */ + 'delete', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Store, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'has', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], +]); + +addObject(BUILTIN_SHAPES, BuiltInWeakMapId, [ + [ + 'delete', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Store, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'get', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + 'has', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + /** + * Params + * key: the key of the element to add to the Map object. The key may be + * any JavaScript type (any primitive value or any type of JavaScript + * object). + * value: the value of the element to add to the Map object. + * Returns the Map object. + */ + 'set', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Capture, Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInWeakMapId}, + calleeEffect: Effect.Store, + returnValueKind: ValueKind.Mutable, + }), + ], +]); + addObject(BUILTIN_SHAPES, BuiltInUseStateId, [ ['0', {kind: 'Poly'}], [ diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InerAliasForUncalledFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InerAliasForUncalledFunctions.ts new file mode 100644 index 0000000000000..1dc5743b73702 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InerAliasForUncalledFunctions.ts @@ -0,0 +1,134 @@ +/** + * 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. + */ + +import { + Effect, + HIRFunction, + Identifier, + isMutableEffect, + isRefOrRefLikeMutableType, + makeInstructionId, +} from '../HIR/HIR'; +import {eachInstructionValueOperand} from '../HIR/visitors'; +import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables'; +import DisjointSet from '../Utils/DisjointSet'; + +/** + * If a function captures a mutable value but never gets called, we don't infer a + * mutable range for that function. This means that we also don't alias the function + * with its mutable captures. + * + * This case is tricky, because we don't generally know for sure what is a mutation + * and what may just be a normal function call. For example: + * + * ``` + * hook useFoo() { + * const x = makeObject(); + * return () => { + * return readObject(x); // could be a mutation! + * } + * } + * ``` + * + * If we pessimistically assume that all such cases are mutations, we'd have to group + * lots of memo scopes together unnecessarily. However, if there is definitely a mutation: + * + * ``` + * hook useFoo(createEntryForKey) { + * const cache = new WeakMap(); + * return (key) => { + * let entry = cache.get(key); + * if (entry == null) { + * entry = createEntryForKey(key); + * cache.set(key, entry); // known mutation! + * } + * return entry; + * } + * } + * ``` + * + * Then we have to ensure that the function and its mutable captures alias together and + * end up in the same scope. However, aliasing together isn't enough if the function + * and operands all have empty mutable ranges (end = start + 1). + * + * This pass finds function expressions and object methods that have an empty mutable range + * and known-mutable operands which also don't have a mutable range, and ensures that the + * function and those operands are aliased together *and* that their ranges are updated to + * end after the function expression. This is sufficient to ensure that a reactive scope is + * created for the alias set. + */ +export function inferAliasForUncalledFunctions( + fn: HIRFunction, + aliases: DisjointSet, +): void { + for (const block of fn.body.blocks.values()) { + instrs: for (const instr of block.instructions) { + const {lvalue, value} = instr; + if ( + value.kind !== 'ObjectMethod' && + value.kind !== 'FunctionExpression' + ) { + continue; + } + /* + * If the function is known to be mutated, we will have + * already aliased any mutable operands with it + */ + const range = lvalue.identifier.mutableRange; + if (range.end > range.start + 1) { + continue; + } + /* + * If the function already has operands with an active mutable range, + * then we don't need to do anything — the function will have already + * been visited and included in some mutable alias set. This case can + * also occur due to visiting the same function in an earlier iteration + * of the outer fixpoint loop. + */ + for (const operand of eachInstructionValueOperand(value)) { + if (isMutable(instr, operand)) { + continue instrs; + } + } + const operands: Set = new Set(); + for (const effect of value.loweredFunc.func.effects ?? []) { + if (effect.kind !== 'ContextMutation') { + continue; + } + /* + * We're looking for known-mutations only, so we look at the effects + * rather than function context + */ + if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) { + for (const operand of effect.places) { + /* + * It's possible that function effect analysis thinks there was a context mutation, + * but then InferReferenceEffects figures out some operands are globals and therefore + * creates a non-mutable effect for those operands. + * We should change InferReferenceEffects to swap the ContextMutation for a global + * mutation in that case, but for now we just filter them out here + */ + if ( + isMutableEffect(operand.effect, operand.loc) && + !isRefOrRefLikeMutableType(operand.identifier.type) + ) { + operands.add(operand.identifier); + } + } + } + } + if (operands.size !== 0) { + operands.add(lvalue.identifier); + aliases.union([...operands]); + // Update mutable ranges, if the ranges are empty then a reactive scope isn't created + for (const operand of operands) { + operand.mutableRange.end = makeInstructionId(instr.id + 1); + } + } + } + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts index a8f33b31d58f3..624c302fbf7ee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts @@ -6,6 +6,7 @@ */ import {HIRFunction, Identifier} from '../HIR/HIR'; +import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions'; import {inferAliases} from './InferAlias'; import {inferAliasForPhis} from './InferAliasForPhis'; import {inferAliasForStores} from './InferAliasForStores'; @@ -76,6 +77,7 @@ export function inferMutableRanges(ir: HIRFunction): void { while (true) { inferMutableRangesForAlias(ir, aliases); inferAliasForPhis(ir, aliases); + inferAliasForUncalledFunctions(ir, aliases); const nextAliases = aliases.canonicalize(); if (areEqualMaps(prevAliases, nextAliases)) { break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index 790a64b407316..33a124dcec6e2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -2327,9 +2327,12 @@ function codegenInstructionValue( * u0080 to u009F: C1 control codes * u00A0 to uFFFF: All non-basic Latin characters * https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes + * + * u010000 to u10FFFF: Astral plane characters + * https://mathiasbynens.be/notes/javascript-unicode */ const STRING_REQUIRES_EXPR_CONTAINER_PATTERN = - /[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}]|"|\\/u; + /[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}\u{010000}-\u{10FFFF}]|"|\\/u; function codegenJsxAttribute( cx: Context, attribute: JsxAttribute, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts new file mode 100644 index 0000000000000..81612a7441728 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts @@ -0,0 +1,130 @@ +/** + * 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. + */ + +import {CompilerError, Effect, ErrorSeverity} from '..'; +import { + FunctionEffect, + HIRFunction, + IdentifierId, + isMutableEffect, + isRefOrRefLikeMutableType, + Place, +} from '../HIR'; +import { + eachInstructionValueOperand, + eachTerminalOperand, +} from '../HIR/visitors'; +import {Result} from '../Utils/Result'; +import {Iterable_some} from '../Utils/utils'; + +/** + * Validates that functions with known mutations (ie due to types) cannot be passed + * where a frozen value is expected. Example: + * + * ``` + * function Component() { + * const cache = new Map(); + * const onClick = () => { + * cache.set(...); + * } + * useHook(onClick); // ERROR: cannot pass a mutable value + * return // ERROR: cannot pass a mutable value + * } + * ``` + * + * Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable + * variables. But unlike other mutables values like an array, the receiver of the function has + * no way to avoid mutation — for example, a function can receive an array and choose not to mutate + * it, but there's no way to know that a function is mutable and avoid calling it. + * + * This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate) + * that are passed where a frozen value is expected and rejects them. + */ +export function validateNoFreezingKnownMutableFunctions( + fn: HIRFunction, +): Result { + const errors = new CompilerError(); + const contextMutationEffects: Map< + IdentifierId, + Extract + > = new Map(); + + function visitOperand(operand: Place): void { + if (operand.effect === Effect.Freeze) { + const effect = contextMutationEffects.get(operand.identifier.id); + if (effect != null) { + errors.push({ + reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`, + description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`, + loc: operand.loc, + severity: ErrorSeverity.InvalidReact, + }); + errors.push({ + reason: `The function modifies a local variable here`, + loc: effect.loc, + severity: ErrorSeverity.InvalidReact, + }); + } + } + } + + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {lvalue, value} = instr; + switch (value.kind) { + case 'LoadLocal': { + const effect = contextMutationEffects.get(value.place.identifier.id); + if (effect != null) { + contextMutationEffects.set(lvalue.identifier.id, effect); + } + break; + } + case 'StoreLocal': { + const effect = contextMutationEffects.get(value.value.identifier.id); + if (effect != null) { + contextMutationEffects.set(lvalue.identifier.id, effect); + contextMutationEffects.set( + value.lvalue.place.identifier.id, + effect, + ); + } + break; + } + case 'FunctionExpression': { + const knownMutation = (value.loweredFunc.func.effects ?? []).find( + effect => { + return ( + effect.kind === 'ContextMutation' && + (effect.effect === Effect.Store || + effect.effect === Effect.Mutate) && + Iterable_some(effect.places, place => { + return ( + isMutableEffect(place.effect, place.loc) && + !isRefOrRefLikeMutableType(place.identifier.type) + ); + }) + ); + }, + ); + if (knownMutation && knownMutation.kind === 'ContextMutation') { + contextMutationEffects.set(lvalue.identifier.id, knownMutation); + } + break; + } + default: { + for (const operand of eachInstructionValueOperand(value)) { + visitOperand(operand); + } + } + } + } + for (const operand of eachTerminalOperand(block.terminal)) { + visitOperand(operand); + } + } + return errors.asResult(); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 85fec7a75333b..1829d77822998 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -141,14 +141,14 @@ function getCompareDependencyResultDescription( ): string { switch (result) { case CompareDependencyResult.Ok: - return 'dependencies equal'; + return 'Dependencies equal'; case CompareDependencyResult.RootDifference: case CompareDependencyResult.PathDifference: - return 'inferred different dependency than source'; + return 'Inferred different dependency than source'; case CompareDependencyResult.RefAccessDifference: - return 'differences in ref.current access'; + return 'Differences in ref.current access'; case CompareDependencyResult.Subpath: - return 'inferred less specific property than source'; + return 'Inferred less specific property than source'; } } @@ -279,17 +279,20 @@ function validateInferredDep( severity: ErrorSeverity.CannotPreserveMemoization, reason: 'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected', - description: DEBUG - ? `The inferred dependency was \`${prettyPrintScopeDependency( - dep, - )}\`, but the source dependencies were [${validDepsInMemoBlock - .map(dep => printManualMemoDependency(dep, true)) - .join(', ')}]. Detail: ${ - errorDiagnostic - ? getCompareDependencyResultDescription(errorDiagnostic) - : 'none' - }` - : null, + description: + DEBUG || + // If the dependency is a named variable then we can report it. Otherwise only print in debug mode + (dep.identifier.name != null && dep.identifier.name.kind === 'named') + ? `The inferred dependency was \`${prettyPrintScopeDependency( + dep, + )}\`, but the source dependencies were [${validDepsInMemoBlock + .map(dep => printManualMemoDependency(dep, true)) + .join(', ')}]. ${ + errorDiagnostic + ? getCompareDependencyResultDescription(errorDiagnostic) + : 'Inferred dependency not present in source' + }` + : null, loc: memoLocation, suggestions: null, }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md index d9c2b599998b7..048fee7ee1d58 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md @@ -41,7 +41,7 @@ function Component(props) { > 10 | return x; | ^^^^^^^^^^^^^^^^^ > 11 | }, [props?.items, props.cond]); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11) + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11) 12 | return ( 13 | 14 | ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md index 57b7d48facbd5..ca3ee2ae1388e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md @@ -41,7 +41,7 @@ function Component(props) { > 10 | return x; | ^^^^^^^^^^^^^^^^^ > 11 | }, [props?.items, props.cond]); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11) + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11) 12 | return ( 13 | 14 | ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.expect.md new file mode 100644 index 0000000000000..86a9e14d80e8e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @validateNoFreezingKnownMutableFunctions + +function useFoo() { + const cache = new Map(); + useHook(() => { + cache.set('key', 'value'); + }); +} + +``` + + +## Error + +``` + 3 | function useFoo() { + 4 | const cache = new Map(); +> 5 | useHook(() => { + | ^^^^^^^ +> 6 | cache.set('key', 'value'); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 7 | }); + | ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7) + +InvalidReact: The function modifies a local variable here (6:6) + 8 | } + 9 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.js new file mode 100644 index 0000000000000..323d35700a72c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.js @@ -0,0 +1,8 @@ +// @validateNoFreezingKnownMutableFunctions + +function useFoo() { + const cache = new Map(); + useHook(() => { + cache.set('key', 'value'); + }); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md index ba5b30418069a..7d6c4b38a7a4b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md @@ -29,7 +29,7 @@ function Component(props) { > 6 | // deps are optional | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 7 | }, [props.items?.edges?.nodes]); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:7) + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items.edges.nodes`, but the source dependencies were [props.items?.edges?.nodes]. Inferred different dependency than source (3:7) 8 | return ; 9 | } 10 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.expect.md new file mode 100644 index 0000000000000..0d4742f26c604 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.expect.md @@ -0,0 +1,30 @@ + +## Input + +```javascript +// @validateNoFreezingKnownMutableFunctions +function Component() { + const cache = new Map(); + const fn = () => { + cache.set('key', 'value'); + }; + return ; +} + +``` + + +## Error + +``` + 5 | cache.set('key', 'value'); + 6 | }; +> 7 | return ; + | ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7) + +InvalidReact: The function modifies a local variable here (5:5) + 8 | } + 9 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.js new file mode 100644 index 0000000000000..11793dfac5db0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.js @@ -0,0 +1,8 @@ +// @validateNoFreezingKnownMutableFunctions +function Component() { + const cache = new Map(); + const fn = () => { + cache.set('key', 'value'); + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.expect.md new file mode 100644 index 0000000000000..63a09bedaa0dd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +// @validateNoFreezingKnownMutableFunctions +import {useHook} from 'shared-runtime'; + +function useFoo() { + useHook(); // for inference to kick in + const cache = new Map(); + return () => { + cache.set('key', 'value'); + }; +} + +``` + + +## Error + +``` + 5 | useHook(); // for inference to kick in + 6 | const cache = new Map(); +> 7 | return () => { + | ^^^^^^^ +> 8 | cache.set('key', 'value'); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 9 | }; + | ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9) + +InvalidReact: The function modifies a local variable here (8:8) + 10 | } + 11 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.js new file mode 100644 index 0000000000000..3df37783dc114 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.js @@ -0,0 +1,10 @@ +// @validateNoFreezingKnownMutableFunctions +import {useHook} from 'shared-runtime'; + +function useFoo() { + useHook(); // for inference to kick in + const cache = new Map(); + return () => { + cache.set('key', 'value'); + }; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md index cf15ab13d12e1..2b4943ba491b4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md @@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = { > 12 | Ref.current?.click(); | ^^^^^^^^^^^^^^^^^^^^^^^^^ > 13 | }, []); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (11:13) + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `Ref.current`, but the source dependencies were []. Inferred dependency not present in source (11:13) 14 | 15 | return