Skip to content

Commit ef42987

Browse files
committed
[compiler] Validate against mutable functions being frozen
This revisits a validation I built a while ago, trying to make it more strict this time to ensure that it's high-signal. We detect function expressions which are *known* mutable — they definitely can modify a variable defined outside of the function expression itself (modulo control flow). This uses types to look for known Store and Mutate effects only, and disregards mutations of effects. Any such function passed to a location with a Freeze effect is reported as a validation error. This is behind a flag and disabled by default. If folks agree this makes sense to revisit, i'll test out internally and we can consider enabling by default. ghstack-source-id: 075a731 Pull Request resolved: #33079 DiffTrain build for [0db8db1](0db8db1)
1 parent 0ff1515 commit ef42987

35 files changed

+307
-87
lines changed

compiled/eslint-plugin-react-hooks/index.js

Lines changed: 221 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17792,6 +17792,11 @@ function isUseStateType(id) {
1779217792
function isRefOrRefValue(id) {
1779317793
return isUseRefType(id) || isRefValueType(id);
1779417794
}
17795+
function isRefOrRefLikeMutableType(type) {
17796+
return (type.kind === 'Object' &&
17797+
(type.shapeId === 'BuiltInUseRefId' ||
17798+
type.shapeId == 'ReanimatedSharedValueId'));
17799+
}
1779517800
function isSetStateType(id) {
1779617801
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
1779717802
}
@@ -29584,6 +29589,8 @@ const BuiltInPropsId = 'BuiltInProps';
2958429589
const BuiltInArrayId = 'BuiltInArray';
2958529590
const BuiltInSetId = 'BuiltInSet';
2958629591
const BuiltInMapId = 'BuiltInMap';
29592+
const BuiltInWeakSetId = 'BuiltInWeakSet';
29593+
const BuiltInWeakMapId = 'BuiltInWeakMap';
2958729594
const BuiltInFunctionId = 'BuiltInFunction';
2958829595
const BuiltInJsxId = 'BuiltInJsx';
2958929596
const BuiltInObjectId = 'BuiltInObject';
@@ -29605,6 +29612,7 @@ const BuiltInUseTransitionId = 'BuiltInUseTransition';
2960529612
const BuiltInStartTransitionId = 'BuiltInStartTransition';
2960629613
const BuiltInFireId = 'BuiltInFire';
2960729614
const BuiltInFireFunctionId = 'BuiltInFireFunction';
29615+
const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
2960829616
const BUILTIN_SHAPES = new Map();
2960929617
addObject(BUILTIN_SHAPES, BuiltInPropsId, [
2961029618
['ref', { kind: 'Object', shapeId: BuiltInUseRefId }],
@@ -30024,6 +30032,80 @@ addObject(BUILTIN_SHAPES, BuiltInMapId, [
3002430032
}),
3002530033
],
3002630034
]);
30035+
addObject(BUILTIN_SHAPES, BuiltInWeakSetId, [
30036+
[
30037+
'add',
30038+
addFunction(BUILTIN_SHAPES, [], {
30039+
positionalParams: [Effect.Capture],
30040+
restParam: null,
30041+
returnType: { kind: 'Object', shapeId: BuiltInWeakSetId },
30042+
calleeEffect: Effect.Store,
30043+
returnValueKind: ValueKind.Mutable,
30044+
}),
30045+
],
30046+
[
30047+
'delete',
30048+
addFunction(BUILTIN_SHAPES, [], {
30049+
positionalParams: [Effect.Read],
30050+
restParam: null,
30051+
returnType: PRIMITIVE_TYPE,
30052+
calleeEffect: Effect.Store,
30053+
returnValueKind: ValueKind.Primitive,
30054+
}),
30055+
],
30056+
[
30057+
'has',
30058+
addFunction(BUILTIN_SHAPES, [], {
30059+
positionalParams: [Effect.Read],
30060+
restParam: null,
30061+
returnType: PRIMITIVE_TYPE,
30062+
calleeEffect: Effect.Read,
30063+
returnValueKind: ValueKind.Primitive,
30064+
}),
30065+
],
30066+
]);
30067+
addObject(BUILTIN_SHAPES, BuiltInWeakMapId, [
30068+
[
30069+
'delete',
30070+
addFunction(BUILTIN_SHAPES, [], {
30071+
positionalParams: [Effect.Read],
30072+
restParam: null,
30073+
returnType: PRIMITIVE_TYPE,
30074+
calleeEffect: Effect.Store,
30075+
returnValueKind: ValueKind.Primitive,
30076+
}),
30077+
],
30078+
[
30079+
'get',
30080+
addFunction(BUILTIN_SHAPES, [], {
30081+
positionalParams: [Effect.Read],
30082+
restParam: null,
30083+
returnType: { kind: 'Poly' },
30084+
calleeEffect: Effect.Capture,
30085+
returnValueKind: ValueKind.Mutable,
30086+
}),
30087+
],
30088+
[
30089+
'has',
30090+
addFunction(BUILTIN_SHAPES, [], {
30091+
positionalParams: [Effect.Read],
30092+
restParam: null,
30093+
returnType: PRIMITIVE_TYPE,
30094+
calleeEffect: Effect.Read,
30095+
returnValueKind: ValueKind.Primitive,
30096+
}),
30097+
],
30098+
[
30099+
'set',
30100+
addFunction(BUILTIN_SHAPES, [], {
30101+
positionalParams: [Effect.Capture, Effect.Capture],
30102+
restParam: null,
30103+
returnType: { kind: 'Object', shapeId: BuiltInWeakMapId },
30104+
calleeEffect: Effect.Store,
30105+
returnValueKind: ValueKind.Mutable,
30106+
}),
30107+
],
30108+
]);
3002730109
addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
3002830110
['0', { kind: 'Poly' }],
3002930111
[
@@ -37957,6 +38039,26 @@ const TYPED_GLOBALS = [
3795738039
returnValueKind: ValueKind.Mutable,
3795838040
}, null, true),
3795938041
],
38042+
[
38043+
'WeakMap',
38044+
addFunction(DEFAULT_SHAPES, [], {
38045+
positionalParams: [Effect.ConditionallyMutateIterator],
38046+
restParam: null,
38047+
returnType: { kind: 'Object', shapeId: BuiltInWeakMapId },
38048+
calleeEffect: Effect.Read,
38049+
returnValueKind: ValueKind.Mutable,
38050+
}, null, true),
38051+
],
38052+
[
38053+
'WeakSet',
38054+
addFunction(DEFAULT_SHAPES, [], {
38055+
positionalParams: [Effect.ConditionallyMutateIterator],
38056+
restParam: null,
38057+
returnType: { kind: 'Object', shapeId: BuiltInWeakSetId },
38058+
calleeEffect: Effect.Read,
38059+
returnValueKind: ValueKind.Mutable,
38060+
}, null, true),
38061+
],
3796038062
];
3796138063
const REACT_APIS = [
3796238064
[
@@ -38282,7 +38384,7 @@ function getReanimatedModuleType(registry) {
3828238384
addHook(registry, {
3828338385
positionalParams: [],
3828438386
restParam: Effect.Freeze,
38285-
returnType: { kind: 'Poly' },
38387+
returnType: { kind: 'Object', shapeId: ReanimatedSharedValueId },
3828638388
returnValueKind: ValueKind.Mutable,
3828738389
noAlias: true,
3828838390
calleeEffect: Effect.Read,
@@ -38426,6 +38528,7 @@ const EnvironmentConfigSchema = zod.z.object({
3842638528
validateNoCapitalizedCalls: zod.z.nullable(zod.z.array(zod.z.string())).default(null),
3842738529
validateBlocklistedImports: zod.z.nullable(zod.z.array(zod.z.string())).default(null),
3842838530
validateNoImpureFunctionsInRender: zod.z.boolean().default(false),
38531+
validateNoFreezingKnownMutableFunctions: zod.z.boolean().default(false),
3842938532
enableAssumeHooksFollowRulesOfReact: zod.z.boolean().default(true),
3843038533
enableTransitivelyFreezeFunctionExpressions: zod.z.boolean().default(true),
3843138534
enableEmitFreeze: ExternalFunctionSchema.nullable().default(null),
@@ -48389,6 +48492,49 @@ class RewriteBlockIds extends ReactiveFunctionVisitor {
4838948492
}
4839048493
}
4839148494

48495+
function inferAliasForUncalledFunctions(fn, aliases) {
48496+
var _a;
48497+
for (const block of fn.body.blocks.values()) {
48498+
instrs: for (const instr of block.instructions) {
48499+
const { lvalue, value } = instr;
48500+
if (value.kind !== 'ObjectMethod' &&
48501+
value.kind !== 'FunctionExpression') {
48502+
continue;
48503+
}
48504+
const range = lvalue.identifier.mutableRange;
48505+
if (range.end > range.start + 1) {
48506+
continue;
48507+
}
48508+
for (const operand of eachInstructionValueOperand(value)) {
48509+
if (isMutable(instr, operand)) {
48510+
continue instrs;
48511+
}
48512+
}
48513+
const operands = new Set();
48514+
for (const effect of (_a = value.loweredFunc.func.effects) !== null && _a !== void 0 ? _a : []) {
48515+
if (effect.kind !== 'ContextMutation') {
48516+
continue;
48517+
}
48518+
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
48519+
for (const operand of effect.places) {
48520+
if (isMutableEffect(operand.effect, operand.loc) &&
48521+
!isRefOrRefLikeMutableType(operand.identifier.type)) {
48522+
operands.add(operand.identifier);
48523+
}
48524+
}
48525+
}
48526+
}
48527+
if (operands.size !== 0) {
48528+
operands.add(lvalue.identifier);
48529+
aliases.union([...operands]);
48530+
for (const operand of operands) {
48531+
operand.mutableRange.end = makeInstructionId(instr.id + 1);
48532+
}
48533+
}
48534+
}
48535+
}
48536+
}
48537+
4839248538
function inferAliases(func) {
4839348539
const aliases = new DisjointSet();
4839448540
for (const [_, block] of func.body.blocks) {
@@ -48630,6 +48776,7 @@ function inferMutableRanges(ir) {
4863048776
while (true) {
4863148777
inferMutableRangesForAlias(ir, aliases);
4863248778
inferAliasForPhis(ir, aliases);
48779+
inferAliasForUncalledFunctions(ir, aliases);
4863348780
const nextAliases = aliases.canonicalize();
4863448781
if (areEqualMaps(prevAliases, nextAliases)) {
4863548782
break;
@@ -55140,6 +55287,76 @@ function validateStaticComponents(fn) {
5514055287
return error.asResult();
5514155288
}
5514255289

55290+
function validateNoFreezingKnownMutableFunctions(fn) {
55291+
var _a;
55292+
const errors = new CompilerError();
55293+
const contextMutationEffects = new Map();
55294+
function visitOperand(operand) {
55295+
if (operand.effect === Effect.Freeze) {
55296+
const effect = contextMutationEffects.get(operand.identifier.id);
55297+
if (effect != null) {
55298+
errors.push({
55299+
reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
55300+
description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
55301+
loc: operand.loc,
55302+
severity: ErrorSeverity.InvalidReact,
55303+
});
55304+
errors.push({
55305+
reason: `The function modifies a local variable here`,
55306+
loc: effect.loc,
55307+
severity: ErrorSeverity.InvalidReact,
55308+
});
55309+
}
55310+
}
55311+
}
55312+
for (const block of fn.body.blocks.values()) {
55313+
for (const instr of block.instructions) {
55314+
const { lvalue, value } = instr;
55315+
switch (value.kind) {
55316+
case 'LoadLocal': {
55317+
const effect = contextMutationEffects.get(value.place.identifier.id);
55318+
if (effect != null) {
55319+
contextMutationEffects.set(lvalue.identifier.id, effect);
55320+
}
55321+
break;
55322+
}
55323+
case 'StoreLocal': {
55324+
const effect = contextMutationEffects.get(value.value.identifier.id);
55325+
if (effect != null) {
55326+
contextMutationEffects.set(lvalue.identifier.id, effect);
55327+
contextMutationEffects.set(value.lvalue.place.identifier.id, effect);
55328+
}
55329+
break;
55330+
}
55331+
case 'FunctionExpression': {
55332+
const knownMutation = ((_a = value.loweredFunc.func.effects) !== null && _a !== void 0 ? _a : []).find(effect => {
55333+
return (effect.kind === 'ContextMutation' &&
55334+
(effect.effect === Effect.Store ||
55335+
effect.effect === Effect.Mutate) &&
55336+
Iterable_some(effect.places, place => {
55337+
return (isMutableEffect(place.effect, place.loc) &&
55338+
!isRefOrRefLikeMutableType(place.identifier.type));
55339+
}));
55340+
});
55341+
if (knownMutation && knownMutation.kind === 'ContextMutation') {
55342+
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
55343+
}
55344+
break;
55345+
}
55346+
default: {
55347+
for (const operand of eachInstructionValueOperand(value)) {
55348+
visitOperand(operand);
55349+
}
55350+
}
55351+
}
55352+
}
55353+
for (const operand of eachTerminalOperand(block.terminal)) {
55354+
visitOperand(operand);
55355+
}
55356+
}
55357+
return errors.asResult();
55358+
}
55359+
5514355360
function run(func, config, fnType, mode, programContext, logger, filename, code) {
5514455361
var _a, _b;
5514555362
const contextIdentifiers = findContextIdentifiers(func);
@@ -55244,6 +55461,9 @@ function runWithEnvironment(func, env) {
5524455461
if (env.config.validateNoImpureFunctionsInRender) {
5524555462
validateNoImpureFunctionsInRender(hir).unwrap();
5524655463
}
55464+
if (env.config.validateNoFreezingKnownMutableFunctions) {
55465+
validateNoFreezingKnownMutableFunctions(hir).unwrap();
55466+
}
5524755467
}
5524855468
inferReactivePlaces(hir);
5524955469
log({ kind: 'hir', name: 'InferReactivePlaces', value: hir });

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
73d7e816b7746c700ab843964aaf11c17351fac1
1+
0db8db178c1521f979535bdba32bf9db9f47ca05
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
73d7e816b7746c700ab843964aaf11c17351fac1
1+
0db8db178c1521f979535bdba32bf9db9f47ca05

compiled/facebook-www/React-dev.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1538,7 +1538,7 @@ __DEV__ &&
15381538
exports.useTransition = function () {
15391539
return resolveDispatcher().useTransition();
15401540
};
1541-
exports.version = "19.2.0-www-classic-73d7e816-20250503";
1541+
exports.version = "19.2.0-www-classic-0db8db17-20250503";
15421542
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
15431543
"function" ===
15441544
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-dev.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1538,7 +1538,7 @@ __DEV__ &&
15381538
exports.useTransition = function () {
15391539
return resolveDispatcher().useTransition();
15401540
};
1541-
exports.version = "19.2.0-www-modern-73d7e816-20250503";
1541+
exports.version = "19.2.0-www-modern-0db8db17-20250503";
15421542
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
15431543
"function" ===
15441544
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,4 +636,4 @@ exports.useSyncExternalStore = function (
636636
exports.useTransition = function () {
637637
return ReactSharedInternals.H.useTransition();
638638
};
639-
exports.version = "19.2.0-www-classic-73d7e816-20250503";
639+
exports.version = "19.2.0-www-classic-0db8db17-20250503";

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,4 +636,4 @@ exports.useSyncExternalStore = function (
636636
exports.useTransition = function () {
637637
return ReactSharedInternals.H.useTransition();
638638
};
639-
exports.version = "19.2.0-www-modern-73d7e816-20250503";
639+
exports.version = "19.2.0-www-modern-0db8db17-20250503";

compiled/facebook-www/React-profiling.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ exports.useSyncExternalStore = function (
640640
exports.useTransition = function () {
641641
return ReactSharedInternals.H.useTransition();
642642
};
643-
exports.version = "19.2.0-www-classic-73d7e816-20250503";
643+
exports.version = "19.2.0-www-classic-0db8db17-20250503";
644644
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
645645
"function" ===
646646
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-profiling.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ exports.useSyncExternalStore = function (
640640
exports.useTransition = function () {
641641
return ReactSharedInternals.H.useTransition();
642642
};
643-
exports.version = "19.2.0-www-modern-73d7e816-20250503";
643+
exports.version = "19.2.0-www-modern-0db8db17-20250503";
644644
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
645645
"function" ===
646646
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19014,10 +19014,10 @@ __DEV__ &&
1901419014
(function () {
1901519015
var internals = {
1901619016
bundleType: 1,
19017-
version: "19.2.0-www-classic-73d7e816-20250503",
19017+
version: "19.2.0-www-classic-0db8db17-20250503",
1901819018
rendererPackageName: "react-art",
1901919019
currentDispatcherRef: ReactSharedInternals,
19020-
reconcilerVersion: "19.2.0-www-classic-73d7e816-20250503"
19020+
reconcilerVersion: "19.2.0-www-classic-0db8db17-20250503"
1902119021
};
1902219022
internals.overrideHookState = overrideHookState;
1902319023
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -19051,7 +19051,7 @@ __DEV__ &&
1905119051
exports.Shape = Shape;
1905219052
exports.Surface = Surface;
1905319053
exports.Text = Text;
19054-
exports.version = "19.2.0-www-classic-73d7e816-20250503";
19054+
exports.version = "19.2.0-www-classic-0db8db17-20250503";
1905519055
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
1905619056
"function" ===
1905719057
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

0 commit comments

Comments
 (0)