diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index cc600d2085e..ab0e71b83cf 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 8dfdc76978c..831d1ca3805 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 276e4f7b404..a487b5086c0 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 7ed42cbce13..b8504494662 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 611b5bd2102..99b8c189ee0 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 a599fc2d747..03f4120149b 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 00000000000..1dc5743b737 --- /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 a8f33b31d58..624c302fbf7 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 790a64b4073..33a124dcec6 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 00000000000..81612a74417 --- /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 85fec7a7533..1829d778229 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 d9c2b599998..048fee7ee1d 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 57b7d48facb..ca3ee2ae138 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 00000000000..86a9e14d80e --- /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 00000000000..323d35700a7 --- /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 ba5b3041806..7d6c4b38a7a 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 00000000000..0d4742f26c6 --- /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 00000000000..11793dfac5d --- /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 00000000000..63a09bedaa0 --- /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 00000000000..3df37783dc1 --- /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 cf15ab13d12..2b4943ba491 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