Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {TypeConfig} from './TypeSchema';
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
*/

export function defaultModuleTypeProvider(
moduleName: string,
): TypeConfig | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
} from './HIR';
import {
BuiltInMixedReadonlyId,
BuiltInUseFragmentId,
DefaultMutatingHook,
DefaultNonmutatingHook,
FunctionSignature,
Expand Down Expand Up @@ -648,6 +649,12 @@ export const EnvironmentConfigSchema = z.object({
*/
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),

/**
* Treat hook calls named "useFragment" as returning BuiltInUseFragment type.
* This enables tracking of Relay fragment-derived values through the program.
*/
enableTreatUseFragmentAsRelay: z.boolean().default(false),

/*
* If specified a value, the compiler lowers any calls to `useContext` to use
* this value as the callee.
Expand Down Expand Up @@ -819,14 +826,22 @@ export class Environment {
],
suggestions: null,
});
// Use BuiltInUseFragmentId for useFragment to enable tracking of fragment-derived values
const returnTypeShapeId =
hookName === 'useFragment'
? BuiltInUseFragmentId
: hook.transitiveMixedData
? BuiltInMixedReadonlyId
: null;
this.#globals.set(
hookName,
addHook(this.#shapes, {
positionalParams: [],
restParam: hook.effectKind,
returnType: hook.transitiveMixedData
? {kind: 'Object', shapeId: BuiltInMixedReadonlyId}
: {kind: 'Poly'},
returnType:
returnTypeShapeId != null
? {kind: 'Object', shapeId: returnTypeShapeId}
: {kind: 'Poly'},
returnValueKind: hook.valueKind,
calleeEffect: Effect.Read,
hookKind: 'Custom',
Expand Down
4 changes: 4 additions & 0 deletions compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,10 @@ export function isUseStateType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
}

export function isUseFragmentType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseFragment';
}

export function isJsxType(type: Type): boolean {
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ export const BuiltInSetActionStateId = 'BuiltInSetActionState';
export const BuiltInUseRefId = 'BuiltInUseRefId';
export const BuiltInRefValueId = 'BuiltInRefValue';
export const BuiltInMixedReadonlyId = 'BuiltInMixedReadonly';
export const BuiltInUseFragmentId = 'BuiltInUseFragment';
export const BuiltInUseEffectHookId = 'BuiltInUseEffectHook';
export const BuiltInUseLayoutEffectHookId = 'BuiltInUseLayoutEffectHook';
export const BuiltInUseInsertionEffectHookId = 'BuiltInUseInsertionEffectHook';
Expand Down Expand Up @@ -1475,6 +1476,16 @@ addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
['*', {kind: 'Object', shapeId: BuiltInMixedReadonlyId}],
]);

/**
* BuiltInUseFragment represents values returned from Relay's useFragment hook.
* The catch-all property ensures that property accesses on useFragment data
* are also typed as BuiltInUseFragmentId, enabling tracking of fragment-derived
* values through the program.
*/
addObject(BUILTIN_SHAPES, BuiltInUseFragmentId, [
['*', {kind: 'Object', shapeId: BuiltInUseFragmentId}],
]);

addObject(BUILTIN_SHAPES, BuiltInJsxId, []);
addObject(BUILTIN_SHAPES, BuiltInFunctionId, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
BuiltInPropsId,
BuiltInRefValueId,
BuiltInSetStateId,
BuiltInUseFragmentId,
BuiltInUseRefId,
} from '../HIR/ObjectShape';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
Expand Down Expand Up @@ -265,7 +266,18 @@ function* generateInstructionTypes(

case 'LoadGlobal': {
const globalType = env.getGlobalDeclaration(value.binding, value.loc);
if (globalType) {
// If the binding is useFragment and the flag is enabled, override the type
if (
env.config.enableTreatUseFragmentAsRelay &&
value.binding.name === 'useFragment'
) {
yield equation(left, {
kind: 'Function',
shapeId: BuiltInUseFragmentId,
return: {kind: 'Object', shapeId: BuiltInUseFragmentId},
isConstructor: false,
});
} else if (globalType) {
yield equation(left, globalType);
}
break;
Expand All @@ -279,9 +291,9 @@ function* generateInstructionTypes(
* (see https://github.com/facebook/react-forget/pull/1427)
*/
let shapeId: string | null = null;
const calleeName = getName(names, value.callee.identifier.id);
if (env.config.enableTreatSetIdentifiersAsStateSetters) {
const name = getName(names, value.callee.identifier.id);
if (name.startsWith('set')) {
if (calleeName.startsWith('set')) {
shapeId = BuiltInSetStateId;
}
}
Expand Down Expand Up @@ -730,26 +742,32 @@ class Unifier {
* T2
* Phi [
* T3
* Phi [
* T4
* ]
* Phi [T4]
* ]
* ]
*
* Which avoids the cycle
* Which then resolves to T1, T2, T3, T4 as candidates (because
* they're all resolved Phi types).
*/
const operands = [];
const operands: Array<Type> = [];
for (const operand of type.operands) {
if (operand.kind === 'Type' && operand.id === v.id) {
if (typeEquals(operand, v)) {
continue;
}
const resolved = this.tryResolveType(v, operand);
const resolved = this.tryResolveType(v, this.get(operand));
if (resolved === null) {
return null;
}
operands.push(resolved);
}
return {kind: 'Phi', operands};
if (operands.length === 0) {
// All operands were `v`
return null;
}
return {
kind: 'Phi',
operands,
};
}
case 'Type': {
const substitution = this.get(type);
Expand Down Expand Up @@ -859,12 +877,19 @@ function tryUnionTypes(ty1: Type, ty2: Type): Type | null {
} else if (ty2.kind === 'Object' && ty2.shapeId === BuiltInMixedReadonlyId) {
readonlyType = ty2;
otherType = ty1;
} else if (ty1.kind === 'Object' && ty1.shapeId === BuiltInUseFragmentId) {
readonlyType = ty1;
otherType = ty2;
} else if (ty2.kind === 'Object' && ty2.shapeId === BuiltInUseFragmentId) {
readonlyType = ty2;
otherType = ty1;
} else {
return null;
}
if (otherType.kind === 'Primitive') {
/**
* Union(Primitive | MixedReadonly) = MixedReadonly
* Union(Primitive | UseFragment) = UseFragment
*
* For example, `data ?? null` could return `data`, the fact that RHS
* is a primitive doesn't guarantee the result is a primitive.
Expand All @@ -876,6 +901,7 @@ function tryUnionTypes(ty1: Type, ty2: Type): Type | null {
) {
/**
* Union(Array | MixedReadonly) = Array
* Union(Array | UseFragment) = Array
*
* In practice this pattern means the result is always an array. Given
* that this behavior requires opting-in to the mixedreadonly type
Expand Down
Loading
Loading