Skip to content

Commit d8eb5fe

Browse files
committed
[compiler] Validate ref reads in render via improved impure value validation
Updates to guard against *reading* refs during render via the improved validateNoImpureValuesInRender() pass. InferMutationAliasingEffects generates `Impure` effects for ref reads, and then this pass validates that those accesses don't flow into `Render` effects. We now call the impure value validation first so that it takes priority over validateNoRefAccessInRender - the latter still has all the existing logic for now, but we can dramatically simplify it in a follow-up PR so that it only has to validate against ref writes.
1 parent 66bce76 commit d8eb5fe

File tree

28 files changed

+438
-184
lines changed

28 files changed

+438
-184
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,6 @@ function runWithEnvironment(
271271
assertValidMutableRanges(hir);
272272
}
273273

274-
if (env.config.validateRefAccessDuringRender) {
275-
validateNoRefAccessInRender(hir).unwrap();
276-
}
277-
278274
if (env.config.validateNoSetStateInRender) {
279275
validateNoSetStateInRender(hir).unwrap();
280276
}
@@ -296,10 +292,17 @@ function runWithEnvironment(
296292
env.logErrors(validateNoJSXInTryStatement(hir));
297293
}
298294

299-
if (env.config.validateNoImpureFunctionsInRender) {
295+
if (
296+
env.config.validateNoImpureFunctionsInRender ||
297+
env.config.validateRefAccessDuringRender
298+
) {
300299
validateNoImpureValuesInRender(hir).unwrap();
301300
}
302301

302+
if (env.config.validateRefAccessDuringRender) {
303+
validateNoRefAccessInRender(hir).unwrap();
304+
}
305+
303306
validateNoFreezingKnownMutableFunctions(hir).unwrap();
304307
}
305308

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,6 +1890,13 @@ export function isJsxType(type: Type): boolean {
18901890
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
18911891
}
18921892

1893+
export function isJsxOrJsxUnionType(type: Type): boolean {
1894+
return (
1895+
(type.kind === 'Object' && type.shapeId === 'BuiltInJsx') ||
1896+
(type.kind === 'Phi' && type.operands.some(op => isJsxOrJsxUnionType(op)))
1897+
);
1898+
}
1899+
18931900
export function isRefOrRefValue(id: Identifier): boolean {
18941901
return isUseRefType(id) || isRefValueType(id);
18951902
}
@@ -2058,4 +2065,23 @@ export function getHookKindForType(
20582065
return null;
20592066
}
20602067

2068+
export function areEqualSourceLocations(
2069+
loc1: SourceLocation,
2070+
loc2: SourceLocation,
2071+
): boolean {
2072+
if (typeof loc1 === 'symbol' || typeof loc2 === 'symbol') {
2073+
return false;
2074+
}
2075+
return (
2076+
loc1.filename === loc2.filename &&
2077+
loc1.identifierName === loc2.identifierName &&
2078+
loc1.start.line === loc2.start.line &&
2079+
loc1.start.column === loc2.start.column &&
2080+
loc1.start.index === loc2.start.index &&
2081+
loc1.end.line === loc2.end.line &&
2082+
loc1.end.column === loc2.end.column &&
2083+
loc1.end.index === loc2.end.index
2084+
);
2085+
}
2086+
20612087
export * from './Types';

compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ export function createTemporaryPlace(
988988
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
989989
reactive: false,
990990
effect: Effect.Unknown,
991-
loc: GeneratedSource,
991+
loc,
992992
};
993993
}
994994

compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError} from '../CompilerError';
8+
import {CompilerError, ErrorCategory} from '../CompilerError';
99
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
1010
import {assertExhaustive} from '../Utils/utils';
1111
import {
@@ -194,8 +194,11 @@ function parseAliasingSignatureConfig(
194194
return {
195195
kind: 'Impure',
196196
into,
197+
category: ErrorCategory.Purity,
197198
description: effect.description,
198199
reason: effect.reason,
200+
sourceMessage: effect.sourceMessage,
201+
usageMessage: effect.usageMessage,
199202
};
200203
}
201204
case 'Render': {

compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,17 @@ export type ImpureEffectConfig = {
188188
into: string;
189189
reason: string;
190190
description: string;
191+
sourceMessage: string;
192+
usageMessage: string;
191193
};
192194

193195
export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
194196
kind: z.literal('Impure'),
195197
into: LifetimeIdSchema,
196198
reason: z.string(),
197199
description: z.string(),
200+
sourceMessage: z.string(),
201+
usageMessage: z.string(),
198202
});
199203

200204
export type RenderEffectConfig = {

compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerDiagnostic} from '../CompilerError';
8+
import {CompilerDiagnostic, ErrorCategory} from '../CompilerError';
99
import {
1010
FunctionExpression,
1111
GeneratedSource,
@@ -162,7 +162,15 @@ export type AliasingEffect =
162162
/**
163163
* Indicates a side-effect that is not safe during render
164164
*/
165-
| {kind: 'Impure'; into: Place; reason: string; description: string}
165+
| {
166+
kind: 'Impure';
167+
into: Place;
168+
category: ErrorCategory;
169+
reason: string;
170+
description: string;
171+
usageMessage: string;
172+
sourceMessage: string;
173+
}
166174
/**
167175
* Indicates that a given place is accessed during render. Used to distingush
168176
* hook arguments that are known to be called immediately vs those used for
@@ -227,6 +235,8 @@ export function hashEffect(effect: AliasingEffect): string {
227235
effect.into.identifier.id,
228236
effect.reason,
229237
effect.description,
238+
effect.usageMessage,
239+
effect.sourceMessage,
230240
].join(':');
231241
case 'Render': {
232242
return [effect.kind, effect.place.identifier.id].join(':');

compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
InstructionKind,
2828
InstructionValue,
2929
isArrayType,
30+
isJsxOrJsxUnionType,
3031
isMapType,
3132
isPrimitiveType,
3233
isRefOrRefValue,
@@ -1988,8 +1989,11 @@ function computeSignatureForInstruction(
19881989
effects.push({
19891990
kind: 'Impure',
19901991
into: lvalue,
1992+
category: ErrorCategory.Refs,
19911993
reason: `Cannot access ref value during render`,
19921994
description: REF_ERROR_DESCRIPTION,
1995+
sourceMessage: `Ref is initially accessed`,
1996+
usageMessage: `Ref value is used during render`,
19931997
});
19941998
}
19951999
break;
@@ -2156,6 +2160,15 @@ function computeSignatureForInstruction(
21562160
into: lvalue,
21572161
});
21582162
}
2163+
if (value.children != null) {
2164+
// Children are typically called during render, not used as an event/effect callback
2165+
for (const child of value.children) {
2166+
effects.push({
2167+
kind: 'Render',
2168+
place: child,
2169+
});
2170+
}
2171+
}
21592172
if (value.kind === 'JsxExpression') {
21602173
if (value.tag.kind === 'Identifier') {
21612174
// Tags are render function, by definition they're called during render
@@ -2164,24 +2177,23 @@ function computeSignatureForInstruction(
21642177
place: value.tag,
21652178
});
21662179
}
2167-
if (value.children != null) {
2168-
// Children are typically called during render, not used as an event/effect callback
2169-
for (const child of value.children) {
2180+
for (const prop of value.props) {
2181+
const place =
2182+
prop.kind === 'JsxAttribute' ? prop.place : prop.argument;
2183+
if (place.identifier.type.kind === 'Function') {
2184+
if (isJsxOrJsxUnionType(place.identifier.type.return)) {
2185+
effects.push({
2186+
kind: 'Render',
2187+
place,
2188+
});
2189+
}
2190+
} else {
21702191
effects.push({
21712192
kind: 'Render',
2172-
place: child,
2193+
place,
21732194
});
21742195
}
21752196
}
2176-
for (const prop of value.props) {
2177-
if (prop.kind === 'JsxAttribute' && /^on[A-Z]/.test(prop.name)) {
2178-
continue;
2179-
}
2180-
effects.push({
2181-
kind: 'Render',
2182-
place: prop.kind === 'JsxAttribute' ? prop.place : prop.argument,
2183-
});
2184-
}
21852197
}
21862198
break;
21872199
}
@@ -2448,14 +2460,17 @@ function computeEffectsForLegacySignature(
24482460
effects.push({
24492461
kind: 'Impure',
24502462
into: lvalue,
2451-
reason:
2452-
signature.canonicalName != null
2453-
? `\`${signature.canonicalName}\` is an impure function.`
2454-
: 'This function is impure',
2463+
category: ErrorCategory.Purity,
2464+
reason: 'Cannot access impure value during render',
24552465
description:
24562466
'Calling an impure function can produce unstable results that update ' +
24572467
'unpredictably when the component happens to re-render. ' +
24582468
'(https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
2469+
sourceMessage:
2470+
signature.canonicalName != null
2471+
? `\`${signature.canonicalName}\` is an impure function.`
2472+
: 'This function is impure',
2473+
usageMessage: 'Cannot access impure value during render',
24592474
});
24602475
}
24612476
if (signature.knownIncompatible != null && state.env.enableValidations) {
@@ -2755,14 +2770,19 @@ function computeEffectsForSignature(
27552770
break;
27562771
}
27572772
case 'Impure': {
2758-
const values = substitutions.get(effect.into.identifier.id) ?? [];
2759-
for (const value of values) {
2760-
effects.push({
2761-
kind: effect.kind,
2762-
into: value,
2763-
reason: effect.reason,
2764-
description: effect.description,
2765-
});
2773+
if (env.config.validateNoImpureFunctionsInRender) {
2774+
const values = substitutions.get(effect.into.identifier.id) ?? [];
2775+
for (const value of values) {
2776+
effects.push({
2777+
kind: effect.kind,
2778+
into: value,
2779+
category: effect.category,
2780+
reason: effect.reason,
2781+
description: effect.description,
2782+
sourceMessage: effect.sourceMessage,
2783+
usageMessage: effect.usageMessage,
2784+
});
2785+
}
27662786
}
27672787
break;
27682788
}

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureValuesInRender.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerDiagnostic, CompilerError, Effect, ErrorCategory} from '..';
9-
import {HIRFunction, IdentifierId, isUseRefType} from '../HIR';
8+
import {CompilerDiagnostic, CompilerError, Effect} from '..';
9+
import {
10+
areEqualSourceLocations,
11+
HIRFunction,
12+
IdentifierId,
13+
isUseRefType,
14+
} from '../HIR';
1015
import {
1116
eachInstructionLValue,
1217
eachInstructionValueOperand,
@@ -246,23 +251,23 @@ function inferImpureValues(
246251
continue;
247252
}
248253
const impureEffect = impure.get(effect.place.identifier.id)!;
249-
error.pushDiagnostic(
250-
CompilerDiagnostic.create({
251-
category: ErrorCategory.Purity,
252-
reason: 'Cannot access impure value during render',
253-
description: impureEffect.description,
254-
})
255-
.withDetails({
256-
kind: 'error',
257-
loc: effect.place.loc,
258-
message: 'Cannot access impure value during render',
259-
})
260-
.withDetails({
261-
kind: 'error',
262-
loc: impureEffect.into.loc,
263-
message: impureEffect.reason,
264-
}),
265-
);
254+
const diagnostic = CompilerDiagnostic.create({
255+
category: impureEffect.category,
256+
reason: impureEffect.reason,
257+
description: impureEffect.description,
258+
}).withDetails({
259+
kind: 'error',
260+
loc: effect.place.loc,
261+
message: impureEffect.usageMessage,
262+
});
263+
if (!areEqualSourceLocations(effect.place.loc, impureEffect.into.loc)) {
264+
diagnostic.withDetails({
265+
kind: 'error',
266+
loc: impureEffect.into.loc,
267+
message: impureEffect.sourceMessage,
268+
});
269+
}
270+
error.pushDiagnostic(diagnostic);
266271
}
267272
}
268273
}
@@ -274,8 +279,11 @@ function inferImpureValues(
274279
impureEffects.push({
275280
kind: 'Impure',
276281
into: {...place},
282+
category: impureEffect.category,
277283
reason: impureEffect.reason,
278284
description: impureEffect.description,
285+
sourceMessage: impureEffect.sourceMessage,
286+
usageMessage: impureEffect.usageMessage,
279287
});
280288
}
281289
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)