Skip to content

Commit 7f3826e

Browse files
authored
[compiler] validation against calling impure functions (facebook#31960)
For now we just reject all calls of impure functions, and the validation is off by default. Going forward we can make this more precise and only reject impure functions called during render. Note that I was intentionally imprecise in the return type of these functions in order to avoid changing output of existing code. We lie to the compiler and say that Date.now, performance.now, and Math.random return unknown mutable objects rather than primitives. Once the validation is complete and vetted we can switch this to be more precise.
1 parent 313c8c5 commit 7f3826e

File tree

8 files changed

+161
-0
lines changed

8 files changed

+161
-0
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHI
9999
import {outlineJSX} from '../Optimization/OutlineJsx';
100100
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
101101
import {transformFire} from '../Transform';
102+
import {validateNoImpureFunctionsInRender} from '../Validation/ValiateNoImpureFunctionsInRender';
102103

103104
export type CompilerPipelineValue =
104105
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -257,6 +258,10 @@ function runWithEnvironment(
257258
validateNoJSXInTryStatement(hir);
258259
}
259260

261+
if (env.config.validateNoImpureFunctionsInRender) {
262+
validateNoImpureFunctionsInRender(hir);
263+
}
264+
260265
inferReactivePlaces(hir);
261266
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
262267

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,11 @@ const EnvironmentConfigSchema = z.object({
353353
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
354354
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
355355

356+
/**
357+
* Validate against impure functions called during render
358+
*/
359+
validateNoImpureFunctionsInRender: z.boolean().default(false),
360+
356361
/*
357362
* When enabled, the compiler assumes that hooks follow the Rules of React:
358363
* - Hooks may memoize computation based on any of their parameters, thus

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,44 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
141141
],
142142
]),
143143
],
144+
[
145+
'performance',
146+
addObject(DEFAULT_SHAPES, 'performance', [
147+
// Static methods (TODO)
148+
[
149+
'now',
150+
// Date.now()
151+
addFunction(DEFAULT_SHAPES, [], {
152+
positionalParams: [],
153+
restParam: Effect.Read,
154+
returnType: {kind: 'Poly'}, // TODO: could be Primitive, but that would change existing compilation
155+
calleeEffect: Effect.Read,
156+
returnValueKind: ValueKind.Mutable, // same here
157+
impure: true,
158+
canonicalName: 'performance.now',
159+
}),
160+
],
161+
]),
162+
],
163+
[
164+
'Date',
165+
addObject(DEFAULT_SHAPES, 'Date', [
166+
// Static methods (TODO)
167+
[
168+
'now',
169+
// Date.now()
170+
addFunction(DEFAULT_SHAPES, [], {
171+
positionalParams: [],
172+
restParam: Effect.Read,
173+
returnType: {kind: 'Poly'}, // TODO: could be Primitive, but that would change existing compilation
174+
calleeEffect: Effect.Read,
175+
returnValueKind: ValueKind.Mutable, // same here
176+
impure: true,
177+
canonicalName: 'Date.now',
178+
}),
179+
],
180+
]),
181+
],
144182
[
145183
'Math',
146184
addObject(DEFAULT_SHAPES, 'Math', [
@@ -209,6 +247,18 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
209247
returnValueKind: ValueKind.Primitive,
210248
}),
211249
],
250+
[
251+
'random',
252+
addFunction(DEFAULT_SHAPES, [], {
253+
positionalParams: [],
254+
restParam: Effect.Read,
255+
returnType: {kind: 'Poly'}, // TODO: could be Primitive, but that would change existing compilation
256+
calleeEffect: Effect.Read,
257+
returnValueKind: ValueKind.Mutable, // same here
258+
impure: true,
259+
canonicalName: 'Math.random',
260+
}),
261+
],
212262
]),
213263
],
214264
['Infinity', {kind: 'Primitive'}],

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ export type FunctionSignature = {
172172
* - Else uses the effects specified by this signature.
173173
*/
174174
mutableOnlyIfOperandsAreMutable?: boolean;
175+
176+
impure?: boolean;
177+
178+
canonicalName?: string;
175179
};
176180

177181
/*

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export type FunctionTypeConfig = {
4040
returnValueKind: ValueKind;
4141
noAlias?: boolean | null | undefined;
4242
mutableOnlyIfOperandsAreMutable?: boolean | null | undefined;
43+
impure?: boolean | null | undefined;
44+
canonicalName?: string | null | undefined;
4345
};
4446
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
4547
kind: z.literal('function'),
@@ -50,6 +52,8 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
5052
returnValueKind: ValueKindSchema,
5153
noAlias: z.boolean().nullable().optional(),
5254
mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(),
55+
impure: z.boolean().nullable().optional(),
56+
canonicalName: z.string().nullable().optional(),
5357
});
5458

5559
export type HookTypeConfig = {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {CompilerError, ErrorSeverity} from '..';
9+
import {HIRFunction} from '../HIR';
10+
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
11+
12+
/**
13+
* Checks that known-impure functions are not called during render. Examples of invalid functions to
14+
* call during render are `Math.random()` and `Date.now()`. Users may extend this set of
15+
* impure functions via a module type provider and specifying functions with `impure: true`.
16+
*
17+
* TODO: add best-effort analysis of functions which are called during render. We have variations of
18+
* this in several of our validation passes and should unify those analyses into a reusable helper
19+
* and use it here.
20+
*/
21+
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
22+
const errors = new CompilerError();
23+
for (const [, block] of fn.body.blocks) {
24+
for (const instr of block.instructions) {
25+
const value = instr.value;
26+
if (value.kind === 'MethodCall' || value.kind == 'CallExpression') {
27+
const callee =
28+
value.kind === 'MethodCall' ? value.property : value.callee;
29+
const signature = getFunctionCallSignature(
30+
fn.env,
31+
callee.identifier.type,
32+
);
33+
if (signature != null && signature.impure === true) {
34+
errors.push({
35+
reason:
36+
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
37+
description:
38+
signature.canonicalName != null
39+
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
40+
: null,
41+
severity: ErrorSeverity.InvalidReact,
42+
loc: callee.loc,
43+
suggestions: null,
44+
});
45+
}
46+
}
47+
}
48+
}
49+
if (errors.hasErrors()) {
50+
throw errors;
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoImpureFunctionsInRender
6+
7+
function Component() {
8+
const date = Date.now();
9+
const now = performance.now();
10+
const rand = Math.random();
11+
return <Foo date={date} now={now} rand={rand} />;
12+
}
13+
14+
```
15+
16+
17+
## Error
18+
19+
```
20+
2 |
21+
3 | function Component() {
22+
> 4 | const date = Date.now();
23+
| ^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4)
24+
25+
InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `performance.now` is an impure function whose results may change on every call (5:5)
26+
27+
InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Math.random` is an impure function whose results may change on every call (6:6)
28+
5 | const now = performance.now();
29+
6 | const rand = Math.random();
30+
7 | return <Foo date={date} now={now} rand={rand} />;
31+
```
32+
33+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @validateNoImpureFunctionsInRender
2+
3+
function Component() {
4+
const date = Date.now();
5+
const now = performance.now();
6+
const rand = Math.random();
7+
return <Foo date={date} now={now} rand={rand} />;
8+
}

0 commit comments

Comments
 (0)