Compare commits

..

2 Commits

Author SHA1 Message Date
Joe Savona
75be876f2a [compiler] Add definitions for Object entries/keys/values
Fixes remaining issue in #32261, where passing a previously useMemo()-d value to `Object.entries()` makes the compiler think the value is mutated and fail validatePreserveExistingMemo. While I was there I added Object.keys() and Object.values() too.
2025-07-29 21:58:58 -07:00
Joe Savona
7d696dc3b8 Enable ref validation in linter 2025-07-29 12:22:05 -07:00
108 changed files with 1320 additions and 6074 deletions

View File

@@ -92,6 +92,7 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
@@ -105,7 +106,6 @@ import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -292,10 +292,6 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir));
}
@@ -326,6 +322,13 @@ function runWithEnvironment(
value: hir,
});
propagatePhiTypes(hir);
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));

View File

@@ -1,752 +0,0 @@
/**
* TypeScript definitions for Flow type JSON representations
* Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml
*/
// Base type for all Flow types with a kind field
export interface BaseFlowType {
kind: string;
}
// Type for representing polarity
export type Polarity = 'positive' | 'negative' | 'neutral';
// Type for representing a name that might be null
export type OptionalName = string | null;
// Open type
export interface OpenType extends BaseFlowType {
kind: 'Open';
}
// Def type
export interface DefType extends BaseFlowType {
kind: 'Def';
def: DefT;
}
// Eval type
export interface EvalType extends BaseFlowType {
kind: 'Eval';
type: FlowType;
destructor: Destructor;
}
// Generic type
export interface GenericType extends BaseFlowType {
kind: 'Generic';
name: string;
bound: FlowType;
no_infer: boolean;
}
// ThisInstance type
export interface ThisInstanceType extends BaseFlowType {
kind: 'ThisInstance';
instance: InstanceT;
is_this: boolean;
name: string;
}
// ThisTypeApp type
export interface ThisTypeAppType extends BaseFlowType {
kind: 'ThisTypeApp';
t1: FlowType;
t2: FlowType;
t_list?: Array<FlowType>;
}
// TypeApp type
export interface TypeAppType extends BaseFlowType {
kind: 'TypeApp';
type: FlowType;
targs: Array<FlowType>;
from_value: boolean;
use_desc: boolean;
}
// FunProto type
export interface FunProtoType extends BaseFlowType {
kind: 'FunProto';
}
// ObjProto type
export interface ObjProtoType extends BaseFlowType {
kind: 'ObjProto';
}
// NullProto type
export interface NullProtoType extends BaseFlowType {
kind: 'NullProto';
}
// FunProtoBind type
export interface FunProtoBindType extends BaseFlowType {
kind: 'FunProtoBind';
}
// Intersection type
export interface IntersectionType extends BaseFlowType {
kind: 'Intersection';
members: Array<FlowType>;
}
// Union type
export interface UnionType extends BaseFlowType {
kind: 'Union';
members: Array<FlowType>;
}
// Maybe type
export interface MaybeType extends BaseFlowType {
kind: 'Maybe';
type: FlowType;
}
// Optional type
export interface OptionalType extends BaseFlowType {
kind: 'Optional';
type: FlowType;
use_desc: boolean;
}
// Keys type
export interface KeysType extends BaseFlowType {
kind: 'Keys';
type: FlowType;
}
// Annot type
export interface AnnotType extends BaseFlowType {
kind: 'Annot';
type: FlowType;
use_desc: boolean;
}
// Opaque type
export interface OpaqueType extends BaseFlowType {
kind: 'Opaque';
opaquetype: {
opaque_id: string;
underlying_t: FlowType | null;
super_t: FlowType | null;
opaque_type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
opaque_name: string;
};
}
// Namespace type
export interface NamespaceType extends BaseFlowType {
kind: 'Namespace';
namespace_symbol: {
symbol: string;
};
values_type: FlowType;
types_tmap: PropertyMap;
}
// Any type
export interface AnyType extends BaseFlowType {
kind: 'Any';
}
// StrUtil type
export interface StrUtilType extends BaseFlowType {
kind: 'StrUtil';
op: 'StrPrefix' | 'StrSuffix';
prefix?: string;
suffix?: string;
remainder?: FlowType;
}
// TypeParam definition
export interface TypeParam {
name: string;
bound: FlowType;
polarity: Polarity;
default: FlowType | null;
}
// EnumInfo types
export type EnumInfo = ConcreteEnum | AbstractEnum;
export interface ConcreteEnum {
kind: 'ConcreteEnum';
enum_name: string;
enum_id: string;
members: Array<string>;
representation_t: FlowType;
has_unknown_members: boolean;
}
export interface AbstractEnum {
kind: 'AbstractEnum';
representation_t: FlowType;
}
// CanonicalRendersForm types
export type CanonicalRendersForm =
| InstrinsicRenders
| NominalRenders
| StructuralRenders
| DefaultRenders;
export interface InstrinsicRenders {
kind: 'InstrinsicRenders';
name: string;
}
export interface NominalRenders {
kind: 'NominalRenders';
renders_id: string;
renders_name: string;
renders_super: FlowType;
}
export interface StructuralRenders {
kind: 'StructuralRenders';
renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar';
renders_structural_type: FlowType;
}
export interface DefaultRenders {
kind: 'DefaultRenders';
}
// InstanceT definition
export interface InstanceT {
inst: InstType;
static: FlowType;
super: FlowType;
implements: Array<FlowType>;
}
// InstType definition
export interface InstType {
class_name: string | null;
class_id: string;
type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
own_props: PropertyMap;
proto_props: PropertyMap;
call_t: null | {
id: number;
call: FlowType;
};
}
// DefT types
export type DefT =
| NumGeneralType
| StrGeneralType
| BoolGeneralType
| BigIntGeneralType
| EmptyType
| MixedType
| NullType
| VoidType
| SymbolType
| FunType
| ObjType
| ArrType
| ClassType
| InstanceType
| SingletonStrType
| NumericStrKeyType
| SingletonNumType
| SingletonBoolType
| SingletonBigIntType
| TypeType
| PolyType
| ReactAbstractComponentType
| RendersType
| EnumValueType
| EnumObjectType;
export interface NumGeneralType extends BaseFlowType {
kind: 'NumGeneral';
}
export interface StrGeneralType extends BaseFlowType {
kind: 'StrGeneral';
}
export interface BoolGeneralType extends BaseFlowType {
kind: 'BoolGeneral';
}
export interface BigIntGeneralType extends BaseFlowType {
kind: 'BigIntGeneral';
}
export interface EmptyType extends BaseFlowType {
kind: 'Empty';
}
export interface MixedType extends BaseFlowType {
kind: 'Mixed';
}
export interface NullType extends BaseFlowType {
kind: 'Null';
}
export interface VoidType extends BaseFlowType {
kind: 'Void';
}
export interface SymbolType extends BaseFlowType {
kind: 'Symbol';
}
export interface FunType extends BaseFlowType {
kind: 'Fun';
static: FlowType;
funtype: FunTypeObj;
}
export interface ObjType extends BaseFlowType {
kind: 'Obj';
objtype: ObjTypeObj;
}
export interface ArrType extends BaseFlowType {
kind: 'Arr';
arrtype: ArrTypeObj;
}
export interface ClassType extends BaseFlowType {
kind: 'Class';
type: FlowType;
}
export interface InstanceType extends BaseFlowType {
kind: 'Instance';
instance: InstanceT;
}
export interface SingletonStrType extends BaseFlowType {
kind: 'SingletonStr';
from_annot: boolean;
value: string;
}
export interface NumericStrKeyType extends BaseFlowType {
kind: 'NumericStrKey';
number: string;
string: string;
}
export interface SingletonNumType extends BaseFlowType {
kind: 'SingletonNum';
from_annot: boolean;
number: string;
string: string;
}
export interface SingletonBoolType extends BaseFlowType {
kind: 'SingletonBool';
from_annot: boolean;
value: boolean;
}
export interface SingletonBigIntType extends BaseFlowType {
kind: 'SingletonBigInt';
from_annot: boolean;
value: string;
}
export interface TypeType extends BaseFlowType {
kind: 'Type';
type_kind: TypeTKind;
type: FlowType;
}
export type TypeTKind =
| 'TypeAliasKind'
| 'TypeParamKind'
| 'OpaqueKind'
| 'ImportTypeofKind'
| 'ImportClassKind'
| 'ImportEnumKind'
| 'InstanceKind'
| 'RenderTypeKind';
export interface PolyType extends BaseFlowType {
kind: 'Poly';
tparams: Array<TypeParam>;
t_out: FlowType;
id: string;
}
export interface ReactAbstractComponentType extends BaseFlowType {
kind: 'ReactAbstractComponent';
config: FlowType;
renders: FlowType;
instance: ComponentInstance;
component_kind: ComponentKind;
}
export type ComponentInstance =
| {kind: 'RefSetterProp'; type: FlowType}
| {kind: 'Omitted'};
export type ComponentKind =
| {kind: 'Structural'}
| {kind: 'Nominal'; id: string; name: string; types: Array<FlowType> | null};
export interface RendersType extends BaseFlowType {
kind: 'Renders';
form: CanonicalRendersForm;
}
export interface EnumValueType extends BaseFlowType {
kind: 'EnumValue';
enum_info: EnumInfo;
}
export interface EnumObjectType extends BaseFlowType {
kind: 'EnumObject';
enum_value_t: FlowType;
enum_info: EnumInfo;
}
// ObjKind types
export type ObjKind =
| {kind: 'Exact'}
| {kind: 'Inexact'}
| {kind: 'Indexed'; dicttype: DictType};
// DictType definition
export interface DictType {
dict_name: string | null;
key: FlowType;
value: FlowType;
dict_polarity: Polarity;
}
// ArrType types
export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT;
export interface ArrayAT {
kind: 'ArrayAT';
elem_t: FlowType;
}
export interface TupleAT {
kind: 'TupleAT';
elem_t: FlowType;
elements: Array<TupleElement>;
min_arity: number;
max_arity: number;
inexact: boolean;
}
export interface ROArrayAT {
kind: 'ROArrayAT';
elem_t: FlowType;
}
// TupleElement definition
export interface TupleElement {
name: string | null;
t: FlowType;
polarity: Polarity;
optional: boolean;
}
// Flags definition
export interface Flags {
obj_kind: ObjKind;
}
// Property types
export type Property =
| FieldProperty
| GetProperty
| SetProperty
| GetSetProperty
| MethodProperty;
export interface FieldProperty {
kind: 'Field';
type: FlowType;
polarity: Polarity;
}
export interface GetProperty {
kind: 'Get';
type: FlowType;
}
export interface SetProperty {
kind: 'Set';
type: FlowType;
}
export interface GetSetProperty {
kind: 'GetSet';
get_type: FlowType;
set_type: FlowType;
}
export interface MethodProperty {
kind: 'Method';
type: FlowType;
}
// PropertyMap definition
export interface PropertyMap {
[key: string]: Property; // For other properties in the map
}
// ObjType definition
export interface ObjTypeObj {
flags: Flags;
props: PropertyMap;
proto_t: FlowType;
call_t: number | null;
}
// FunType definition
export interface FunTypeObj {
this_t: {
type: FlowType;
status: ThisStatus;
};
params: Array<{
name: string | null;
type: FlowType;
}>;
rest_param: null | {
name: string | null;
type: FlowType;
};
return_t: FlowType;
type_guard: null | {
inferred: boolean;
param_name: string;
type_guard: FlowType;
one_sided: boolean;
};
effect: Effect;
}
// ThisStatus types
export type ThisStatus =
| {kind: 'This_Method'; unbound: boolean}
| {kind: 'This_Function'};
// Effect types
export type Effect =
| {kind: 'HookDecl'; id: string}
| {kind: 'HookAnnot'}
| {kind: 'ArbitraryEffect'}
| {kind: 'AnyEffect'};
// Destructor types
export type Destructor =
| NonMaybeTypeDestructor
| PropertyTypeDestructor
| ElementTypeDestructor
| OptionalIndexedAccessNonMaybeTypeDestructor
| OptionalIndexedAccessResultTypeDestructor
| ExactTypeDestructor
| ReadOnlyTypeDestructor
| PartialTypeDestructor
| RequiredTypeDestructor
| SpreadTypeDestructor
| SpreadTupleTypeDestructor
| RestTypeDestructor
| ValuesTypeDestructor
| ConditionalTypeDestructor
| TypeMapDestructor
| ReactElementPropsTypeDestructor
| ReactElementConfigTypeDestructor
| ReactCheckComponentConfigDestructor
| ReactDRODestructor
| MakeHooklikeDestructor
| MappedTypeDestructor
| EnumTypeDestructor;
export interface NonMaybeTypeDestructor {
kind: 'NonMaybeType';
}
export interface PropertyTypeDestructor {
kind: 'PropertyType';
name: string;
}
export interface ElementTypeDestructor {
kind: 'ElementType';
index_type: FlowType;
}
export interface OptionalIndexedAccessNonMaybeTypeDestructor {
kind: 'OptionalIndexedAccessNonMaybeType';
index: OptionalIndexedAccessIndex;
}
export type OptionalIndexedAccessIndex =
| {kind: 'StrLitIndex'; name: string}
| {kind: 'TypeIndex'; type: FlowType};
export interface OptionalIndexedAccessResultTypeDestructor {
kind: 'OptionalIndexedAccessResultType';
}
export interface ExactTypeDestructor {
kind: 'ExactType';
}
export interface ReadOnlyTypeDestructor {
kind: 'ReadOnlyType';
}
export interface PartialTypeDestructor {
kind: 'PartialType';
}
export interface RequiredTypeDestructor {
kind: 'RequiredType';
}
export interface SpreadTypeDestructor {
kind: 'SpreadType';
target: SpreadTarget;
operands: Array<SpreadOperand>;
operand_slice: Slice | null;
}
export type SpreadTarget =
| {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'}
| {kind: 'Annot'; make_exact: boolean};
export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice;
export interface Slice {
kind: 'Slice';
prop_map: PropertyMap;
generics: Array<string>;
dict: DictType | null;
reachable_targs: Array<{
type: FlowType;
polarity: Polarity;
}>;
}
export interface SpreadTupleTypeDestructor {
kind: 'SpreadTupleType';
inexact: boolean;
resolved_rev: string;
unresolved: string;
}
export interface RestTypeDestructor {
kind: 'RestType';
merge_mode: RestMergeMode;
type: FlowType;
}
export type RestMergeMode =
| {kind: 'SpreadReversal'}
| {kind: 'ReactConfigMerge'; polarity: Polarity}
| {kind: 'Omit'};
export interface ValuesTypeDestructor {
kind: 'ValuesType';
}
export interface ConditionalTypeDestructor {
kind: 'ConditionalType';
distributive_tparam_name: string | null;
infer_tparams: string;
extends_t: FlowType;
true_t: FlowType;
false_t: FlowType;
}
export interface TypeMapDestructor {
kind: 'ObjectKeyMirror';
}
export interface ReactElementPropsTypeDestructor {
kind: 'ReactElementPropsType';
}
export interface ReactElementConfigTypeDestructor {
kind: 'ReactElementConfigType';
}
export interface ReactCheckComponentConfigDestructor {
kind: 'ReactCheckComponentConfig';
props: {
[key: string]: Property;
};
}
export interface ReactDRODestructor {
kind: 'ReactDRO';
dro_type:
| 'HookReturn'
| 'HookArg'
| 'Props'
| 'ImmutableAnnot'
| 'DebugAnnot';
}
export interface MakeHooklikeDestructor {
kind: 'MakeHooklike';
}
export interface MappedTypeDestructor {
kind: 'MappedType';
homomorphic: Homomorphic;
distributive_tparam_name: string | null;
property_type: FlowType;
mapped_type_flags: {
variance: Polarity;
optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality';
};
}
export type Homomorphic =
| {kind: 'Homomorphic'}
| {kind: 'Unspecialized'}
| {kind: 'SemiHomomorphic'; type: FlowType};
export interface EnumTypeDestructor {
kind: 'EnumType';
}
// Union of all possible Flow types
export type FlowType =
| OpenType
| DefType
| EvalType
| GenericType
| ThisInstanceType
| ThisTypeAppType
| TypeAppType
| FunProtoType
| ObjProtoType
| NullProtoType
| FunProtoBindType
| IntersectionType
| UnionType
| MaybeType
| OptionalType
| KeysType
| AnnotType
| OpaqueType
| NamespaceType
| AnyType
| StrUtilType;

View File

@@ -1,131 +0,0 @@
import {CompilerError, SourceLocation} from '..';
import {
ConcreteType,
printConcrete,
printType,
StructuralValue,
Type,
VariableId,
} from './Types';
export function unsupportedLanguageFeature(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support language feature: ${desc}`,
loc,
});
}
export type UnificationError =
| {
kind: 'TypeUnification';
left: ConcreteType<Type>;
right: ConcreteType<Type>;
}
| {
kind: 'StructuralUnification';
left: StructuralValue;
right: ConcreteType<Type>;
};
function printUnificationError(err: UnificationError): string {
if (err.kind === 'TypeUnification') {
return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`;
} else {
return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`;
}
}
export function raiseUnificationErrors(
errs: null | Array<UnificationError>,
loc: SourceLocation,
): void {
if (errs != null) {
if (errs.length === 0) {
CompilerError.invariant(false, {
reason: 'Should not have array of zero errors',
loc,
});
} else if (errs.length === 1) {
CompilerError.throwInvalidJS({
reason: `Unable to unify types because ${printUnificationError(errs[0])}`,
loc,
});
} else {
const messages = errs
.map(err => `\t* ${printUnificationError(err)}`)
.join('\n');
CompilerError.throwInvalidJS({
reason: `Unable to unify types because:\n${messages}`,
loc,
});
}
}
}
export function unresolvableTypeVariable(
id: VariableId,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Unable to resolve free variable ${id} to a concrete type`,
loc,
});
}
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
if (explicit) {
CompilerError.throwInvalidJS({
reason: `Undefined is not a valid operand of \`+\``,
loc,
});
} else {
CompilerError.throwInvalidJS({
reason: `Value may be undefined, which is not a valid operand of \`+\``,
loc,
});
}
}
export function unsupportedTypeAnnotation(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support type annotation: ${desc}`,
loc,
});
}
export function checkTypeArgumentArity(
desc: string,
expected: number,
actual: number,
loc: SourceLocation,
): void {
if (expected !== actual) {
CompilerError.throwInvalidJS({
reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
loc,
});
}
}
export function notAFunction(desc: string, loc: SourceLocation): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} because it is not a function`,
loc,
});
}
export function notAPolymorphicFunction(
desc: string,
loc: SourceLocation,
): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
loc,
});
}

View File

@@ -1,312 +0,0 @@
import {GeneratedSource} from '../HIR';
import {assertExhaustive} from '../Utils/utils';
import {unsupportedLanguageFeature} from './TypeErrors';
import {
ConcreteType,
ResolvedType,
TypeParameter,
TypeParameterId,
DEBUG,
printConcrete,
printType,
} from './Types';
export function substitute(
type: ConcreteType<ResolvedType>,
typeParameters: Array<TypeParameter<ResolvedType>>,
typeArguments: Array<ResolvedType>,
): ResolvedType {
const substMap = new Map<TypeParameterId, ResolvedType>();
for (let i = 0; i < typeParameters.length; i++) {
// TODO: Length checks to make sure type params match up with args
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substMap.set(typeParameter.id, typeArgument);
}
const substitutionFunction = (t: ResolvedType): ResolvedType => {
// TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types
if (t.type.kind === 'Generic' && substMap.has(t.type.id)) {
const substitutedType = substMap.get(t.type.id)!;
return substitutedType;
}
return {
kind: 'Concrete',
type: mapType(substitutionFunction, t.type),
platform: t.platform,
};
};
const substituted = mapType(substitutionFunction, type);
if (DEBUG) {
let substs = '';
for (let i = 0; i < typeParameters.length; i++) {
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`;
}
console.log(
`${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`,
);
}
return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'};
}
export function mapType<T, U>(
f: (t: T) => U,
type: ConcreteType<T>,
): ConcreteType<U> {
switch (type.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
return type;
case 'Nullable':
return {
kind: 'Nullable',
type: f(type.type),
};
case 'Array':
return {
kind: 'Array',
element: f(type.element),
};
case 'Set':
return {
kind: 'Set',
element: f(type.element),
};
case 'Map':
return {
kind: 'Map',
key: f(type.key),
value: f(type.value),
};
case 'Function':
return {
kind: 'Function',
typeParameters:
type.typeParameters?.map(param => ({
id: param.id,
name: param.name,
bound: f(param.bound),
})) ?? null,
params: type.params.map(f),
returnType: f(type.returnType),
};
case 'Component': {
return {
kind: 'Component',
children: type.children != null ? f(type.children) : null,
props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])),
};
}
case 'Generic':
return {
kind: 'Generic',
id: type.id,
bound: f(type.bound),
};
case 'Object':
return type;
case 'Tuple':
return {
kind: 'Tuple',
id: type.id,
members: type.members.map(f),
};
case 'Structural':
return type;
case 'Enum':
case 'Union':
case 'Instance':
unsupportedLanguageFeature(type.kind, GeneratedSource);
default:
assertExhaustive(type, 'Unknown type kind');
}
}
export function diff<R, T>(
a: ConcreteType<T>,
b: ConcreteType<T>,
onChild: (a: T, b: T) => R,
onChildMismatch: (child: R, cur: R) => R,
onMismatch: (a: ConcreteType<T>, b: ConcreteType<T>, cur: R) => R,
init: R,
): R {
let errors = init;
// Check if kinds match
if (a.kind !== b.kind) {
errors = onMismatch(a, b, errors);
return errors;
}
// Based on kind, check other properties
switch (a.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
// Simple types, no further checks needed
break;
case 'Nullable':
// Check the nested type
errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors);
break;
case 'Array':
case 'Set':
// Check the element type
errors = onChildMismatch(
onChild(a.element, (b as typeof a).element),
errors,
);
break;
case 'Map':
// Check both key and value types
errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors);
errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors);
break;
case 'Function': {
const bFunc = b as typeof a;
// Check type parameters
if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) {
errors = onMismatch(a, b, errors);
}
if (a.typeParameters != null && bFunc.typeParameters != null) {
if (a.typeParameters.length !== bFunc.typeParameters.length) {
errors = onMismatch(a, b, errors);
}
// Type parameters are just numbers, so we can compare them directly
for (let i = 0; i < a.typeParameters.length; i++) {
if (a.typeParameters[i] !== bFunc.typeParameters[i]) {
errors = onMismatch(a, b, errors);
}
}
}
// Check parameters
if (a.params.length !== bFunc.params.length) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.params.length; i++) {
errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors);
}
// Check return type
errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors);
break;
}
case 'Component': {
const bComp = b as typeof a;
// Check children
if (a.children !== bComp.children) {
errors = onMismatch(a, b, errors);
}
// Check props
if (a.props.size !== bComp.props.size) {
errors = onMismatch(a, b, errors);
}
for (const [k, v] of a.props) {
const bProp = bComp.props.get(k);
if (bProp == null) {
errors = onMismatch(a, b, errors);
} else {
errors = onChildMismatch(onChild(v, bProp), errors);
}
}
break;
}
case 'Generic': {
// Check that the type parameter IDs match
if (a.id !== (b as typeof a).id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Structural': {
const bStruct = b as typeof a;
// Check that the structural IDs match
if (a.id !== bStruct.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Object': {
const bNom = b as typeof a;
// Check that the nominal IDs match
if (a.id !== bNom.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Tuple': {
const bTuple = b as typeof a;
// Check that the tuple IDs match
if (a.id !== bTuple.id) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.members.length; i++) {
errors = onChildMismatch(
onChild(a.members[i], bTuple.members[i]),
errors,
);
}
break;
}
case 'Enum':
case 'Instance':
case 'Union': {
unsupportedLanguageFeature(a.kind, GeneratedSource);
}
default:
assertExhaustive(a, 'Unknown type kind');
}
return errors;
}
export function filterOptional(t: ResolvedType): ResolvedType {
if (t.kind === 'Concrete' && t.type.kind === 'Nullable') {
return t.type.type;
}
return t;
}

View File

@@ -49,7 +49,6 @@ import {
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -244,12 +243,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Allows specifying a function that can populate HIR with type information from
* Flow
*/
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* Enable a new model for mutability and aliasing inference
*/
@@ -330,12 +323,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInEffects: z.boolean().default(false),
/**
* Validates that effects are not used to calculate derived data which could instead be computed
* during render.
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.
@@ -704,8 +691,6 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
@@ -774,26 +759,6 @@ export class Environment {
this.parentFunction = parentFunction;
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
if (config.flowTypeProvider != null) {
this.#flowTypeEnvironment = new FlowTypeEnv();
CompilerError.invariant(code != null, {
reason:
'Expected Environment to be initialized with source code when a Flow type provider is specified',
loc: null,
});
this.#flowTypeEnvironment.init(this, code);
} else {
this.#flowTypeEnvironment = null;
}
}
get typeContext(): FlowTypeEnv {
CompilerError.invariant(this.#flowTypeEnvironment != null, {
reason: 'Flow type environment not initialized',
loc: null,
});
return this.#flowTypeEnvironment;
}
get isInferredMemoEnabled(): boolean {

View File

@@ -14,7 +14,6 @@ import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
/*
* *******************************************************************************************
@@ -1321,21 +1320,12 @@ export function forkTemporaryIdentifier(
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
if (isReservedWord(name)) {
CompilerError.throwInvalidJS({
reason: 'Expected a non-reserved identifier name',
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
});
} else {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
}
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
return {
kind: 'named',
value: name as ValidIdentifierName,

View File

@@ -119,7 +119,6 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
lastUsage: Map<DeclarationId, InstructionId>;
temporaries: Map<DeclarationId, DeclarationId> = new Map();
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
super();
@@ -216,12 +215,6 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
current.lvalues.add(
instr.instruction.lvalue.identifier.declarationId,
);
if (instr.instruction.value.kind === 'LoadLocal') {
this.temporaries.set(
instr.instruction.lvalue.identifier.declarationId,
instr.instruction.value.place.identifier.declarationId,
);
}
}
break;
}
@@ -243,13 +236,6 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
)) {
current.lvalues.add(lvalue.identifier.declarationId);
}
this.temporaries.set(
instr.instruction.value.lvalue.place.identifier
.declarationId,
this.temporaries.get(
instr.instruction.value.value.identifier.declarationId,
) ?? instr.instruction.value.value.identifier.declarationId,
);
} else {
log(
`Reset scope @${current.block.scope.id} from StoreLocal in [${instr.instruction.id}]`,
@@ -274,7 +260,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
case 'scope': {
if (
current !== null &&
canMergeScopes(current.block, instr, this.temporaries) &&
canMergeScopes(current.block, instr) &&
areLValuesLastUsedByScope(
instr.scope,
current.lvalues,
@@ -440,7 +426,6 @@ function areLValuesLastUsedByScope(
function canMergeScopes(
current: ReactiveScopeBlock,
next: ReactiveScopeBlock,
temporaries: Map<DeclarationId, DeclarationId>,
): boolean {
// Don't merge scopes with reassignments
if (
@@ -480,14 +465,11 @@ function canMergeScopes(
(next.scope.dependencies.size !== 0 &&
[...next.scope.dependencies].every(
dep =>
dep.path.length === 0 &&
isAlwaysInvalidatingType(dep.identifier.type) &&
Iterable_some(
current.scope.declarations.values(),
decl =>
decl.identifier.declarationId === dep.identifier.declarationId ||
decl.identifier.declarationId ===
temporaries.get(dep.identifier.declarationId),
decl.identifier.declarationId === dep.identifier.declarationId,
),
))
) {
@@ -495,16 +477,12 @@ function canMergeScopes(
return true;
}
log(` cannot merge scopes:`);
log(
` ${printReactiveScopeSummary(current.scope)} ${[...current.scope.declarations.values()].map(decl => decl.identifier.declarationId)}`,
);
log(
` ${printReactiveScopeSummary(next.scope)} ${[...next.scope.dependencies].map(dep => `${dep.identifier.declarationId} ${temporaries.get(dep.identifier.declarationId) ?? dep.identifier.declarationId}`)}`,
);
log(` ${printReactiveScopeSummary(current.scope)}`);
log(` ${printReactiveScopeSummary(next.scope)}`);
return false;
}
export function isAlwaysInvalidatingType(type: Type): boolean {
function isAlwaysInvalidatingType(type: Type): boolean {
switch (type.kind) {
case 'Object': {
switch (type.shapeId) {

View File

@@ -0,0 +1,110 @@
/**
* 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 {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR';
/**
* Temporary workaround for InferTypes not propagating the types of phis.
* Previously, LeaveSSA would replace all the identifiers for each phi (operands and
* the phi itself) with a single "canonical" identifier, generally chosen as the first
* operand to flow into the phi. In case of a phi whose operand was a phi, this could
* sometimes be an operand from the earlier phi.
*
* As a result, even though InferTypes did not propagate types for phis, LeaveSSA
* could end up replacing the phi Identifier with another identifer from an operand,
* which _did_ have a type inferred.
*
* This didn't affect the initial construction of mutable ranges because InferMutableRanges
* runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
* notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
* will always invalidate with its input.
*
* The long-term correct approach is to update InferTypes to infer the types of phis,
* but this is complicated because InferMutableRanges inadvertently depends on phis
* never having a known type, such that a Store effect cannot occur on a phi value.
* Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
* to handle this case.
*
* As a temporary workaround, this pass propagates the type of phis and can be called
* safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
* type if all operands have the same type, it's its more correct.
*/
export function propagatePhiTypes(fn: HIRFunction): void {
/**
* We track which SSA ids have had their types propagated to handle nested ternaries,
* see the StoreLocal handling below
*/
const propagated = new Set<IdentifierId>();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
/*
* We replicate the previous LeaveSSA behavior and only propagate types for
* unnamed variables. LeaveSSA would have chosen one of the operands as the
* canonical id and taken its type as the type of all identifiers. We're
* more conservative and only propagate if the types are the same and the
* phi didn't have a type inferred.
*
* Note that this can change output slightly in cases such as
* `cond ? <div /> : null`.
*
* Previously the first operand's type (BuiltInJsx) would have been propagated,
* and this expression may have been merged with subsequent reactive scopes
* since it appears (based on that type) to always invalidate.
*
* But the correct type is `BuiltInJsx | null`, which we can't express and
* so leave as a generic `Type`, which does not always invalidate and therefore
* does not merge with subsequent scopes.
*
* We also don't propagate scopes for named variables, to preserve compatibility
* with previous LeaveSSA behavior.
*/
if (
phi.place.identifier.type.kind !== 'Type' ||
phi.place.identifier.name !== null
) {
continue;
}
let type: Type | null = null;
for (const [, operand] of phi.operands) {
if (type === null) {
type = operand.identifier.type;
} else if (!typeEquals(type, operand.identifier.type)) {
type = null;
break;
}
}
if (type !== null) {
phi.place.identifier.type = type;
propagated.add(phi.place.identifier.id);
}
}
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'StoreLocal': {
/**
* Nested ternaries can lower to a form with an intermediate StoreLocal where
* the value.lvalue is the temporary of the outer ternary, and the value.value
* is the result of the inner ternary.
*
* This is a common pattern in practice and easy enough to support. Again, the
* long-term approach is to update InferTypes and InferMutableRanges.
*/
const lvalue = value.lvalue.place;
if (
propagated.has(value.value.identifier.id) &&
lvalue.identifier.type.kind === 'Type' &&
lvalue.identifier.name === null
) {
lvalue.identifier.type = value.value.identifier.type;
propagated.add(lvalue.identifier.id);
}
}
}
}
}
}

View File

@@ -78,10 +78,6 @@ export default class DisjointSet<T> {
return root;
}
has(item: T): boolean {
return this.#entries.has(item);
}
/*
* Forces the set into canonical form, ie with all items pointing directly to
* their root, and returns a Map representing the mapping of items to their roots.

View File

@@ -1,87 +0,0 @@
/**
* 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.
*/
/**
* https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
*/
/**
* Note: `await` and `yield` are contextually allowed as identifiers.
* await: reserved inside async functions and modules
* yield: reserved inside generator functions
*
* Note: `async` is not reserved.
*/
const RESERVED_WORDS = new Set([
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'new',
'null',
'return',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
]);
/**
* Reserved when a module has a 'use strict' directive.
*/
const STRICT_MODE_RESERVED_WORDS = new Set([
'let',
'static',
'implements',
'interface',
'package',
'private',
'protected',
'public',
]);
/**
* The names arguments and eval are not keywords, but they are subject to some restrictions in
* strict mode code.
*/
const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
/**
* Conservative check for whether an identifer name is reserved or not. We assume that code is
* written with strict mode.
*/
export function isReservedWord(identifierName: string): boolean {
return (
RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
);
}

View File

@@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never {
// Modifies @param array in place, retaining only the items where the predicate returns true.
export function retainWhere<T>(
array: Array<T>,
predicate: (item: T, index: number) => boolean,
predicate: (item: T) => boolean,
): void {
let writeIndex = 0;
for (let readIndex = 0; readIndex < array.length; readIndex++) {
const item = array[readIndex];
if (predicate(item, readIndex) === true) {
if (predicate(item) === true) {
array[writeIndex++] = item;
}
}

View File

@@ -1,230 +0,0 @@
/**
* 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, ErrorSeverity, SourceLocation} from '..';
import {
ArrayExpression,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
/**
* Validates that useEffect is not used for derived computations which could/should
* be performed in render.
*
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
*
* Example:
*
* ```
* // 🔴 Avoid: redundant state and unnecessary Effect
* const [fullName, setFullName] = useState('');
* useEffect(() => {
* setFullName(firstName + ' ' + lastName);
* }, [firstName, lastName]);
* ```
*
* Instead use:
*
* ```
* // ✅ Good: calculated during rendering
* const fullName = firstName + ' ' + lastName;
* ```
*/
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
candidateDependencies.set(lvalue.identifier.id, value);
} else if (value.kind === 'FunctionExpression') {
functions.set(lvalue.identifier.id, value);
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const effectFunction = functions.get(value.args[0].identifier.id);
const deps = candidateDependencies.get(value.args[1].identifier.id);
if (
effectFunction != null &&
deps != null &&
deps.elements.length !== 0 &&
deps.elements.every(element => element.kind === 'Identifier')
) {
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
CompilerError.invariant(dep.kind === 'Identifier', {
reason: `Dependency is checked as a place above`,
loc: value.loc,
});
return locals.get(dep.identifier.id) ?? dep.identifier.id;
});
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
);
}
}
}
}
}
if (errors.hasErrors()) {
throw errors;
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
for (const dep of effectDeps) {
if (
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
// effect dep wasn't actually used in the function
return;
}
}
const seenBlocks: Set<BlockId> = new Set();
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
}
const setStateLocations: Array<SourceLocation> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
return;
}
}
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
case 'LoadGlobal': {
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad':
case 'BinaryExpression':
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
// doesn't depend on any deps
return;
}
}
break;
}
default: {
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
for (const loc of setStateLocations) {
errors.push({
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc,
suggestions: null,
});
}
}

View File

@@ -19,7 +19,7 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(5);
const $ = _c(7);
let t0;
if ($[0] !== props.x) {
t0 = foo(props.x);
@@ -31,19 +31,26 @@ function Component(props) {
const x = t0;
let t1;
if ($[2] !== props || $[3] !== x) {
const fn = function () {
t1 = function () {
const arr = [...bar(props)];
return arr.at(x);
};
t1 = fn();
$[2] = props;
$[3] = x;
$[4] = t1;
} else {
t1 = $[4];
}
const fnResult = t1;
const fn = t1;
let t2;
if ($[5] !== fn) {
t2 = fn();
$[5] = fn;
$[6] = t2;
} else {
t2 = $[6];
}
const fnResult = t2;
return fnResult;
}

View File

@@ -23,18 +23,34 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(6);
let t0;
if ($[0] !== props.a) {
const item = { a: props.a };
const items = [item];
t0 = items.map(_temp);
t0 = { a: props.a };
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const mapped = t0;
const item = t0;
let t1;
if ($[2] !== item) {
t1 = [item];
$[2] = item;
$[3] = t1;
} else {
t1 = $[3];
}
const items = t1;
let t2;
if ($[4] !== items) {
t2 = items.map(_temp);
$[4] = items;
$[5] = t2;
} else {
t2 = $[5];
}
const mapped = t2;
return mapped;
}
function _temp(item_0) {

View File

@@ -21,18 +21,26 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
const f = _temp;
let t0;
if ($[0] !== props.items) {
const x = [...props.items].map(f);
t0 = [x, f];
t0 = [...props.items].map(f);
$[0] = props.items;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const x = t0;
let t1;
if ($[2] !== x) {
t1 = [x, f];
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
function _temp(item) {
return item;

View File

@@ -23,19 +23,27 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== a) {
const z = { a };
t0 = () => {
console.log(z);
};
t0 = { a };
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const z = t0;
let t1;
if ($[2] !== z) {
t1 = () => {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
return x;
}

View File

@@ -23,19 +23,27 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== a) {
const z = { a };
t0 = function () {
console.log(z);
};
t0 = { a };
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
return x;
}

View File

@@ -22,19 +22,35 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(2);
const $ = _c(6);
const { a } = t0;
let t1;
if ($[0] !== a) {
const z = { a };
const p = () => <Stringify>{z}</Stringify>;
t1 = p();
t1 = { a };
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
const z = t1;
let t2;
if ($[2] !== z) {
t2 = () => <Stringify>{z}</Stringify>;
$[2] = z;
$[3] = t2;
} else {
t2 = $[3];
}
const p = t2;
let t3;
if ($[4] !== p) {
t3 = p();
$[4] = p;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -25,19 +25,27 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== a) {
const z = { a };
t0 = function () {
console.log(z);
};
t0 = { a };
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
return x;
}

View File

@@ -25,21 +25,29 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== a) {
const z = { a };
t0 = function () {
(function () {
console.log(z);
})();
};
t0 = { a };
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
(function () {
console.log(z);
})();
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
return x;
}

View File

@@ -1,32 +0,0 @@
## Input
```javascript
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}
```
## Error
```
Found 1 error:
Error: Expected a non-reserved identifier name
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -1,13 +0,0 @@
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}

View File

@@ -1,46 +0,0 @@
## Input
```javascript
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}
```
## Error
```
Found 1 error:
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
<unknown> thunk$14.
error.bug-infer-mutation-aliasing-effects.ts:10:22
8 | function thunk(action) {
9 | if (typeof action === 'function') {
> 10 | return action(thunk, () => stateRef.current, extraArg);
| ^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized
11 | } else {
12 | dispatch(action);
13 | return undefined;
```

View File

@@ -1,18 +0,0 @@
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}

View File

@@ -1,31 +0,0 @@
## Input
```javascript
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};
```
## Error
```
Found 1 error:
Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
error.bug-invariant-codegen-methodcall.ts:3:17
1 | const YearsAndMonthsSince = () => {
2 | const diff = foo();
> 3 | const months = Math.floor(diff.bar());
| ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
4 | return <>{months}</>;
5 | };
6 |
```

View File

@@ -1,5 +0,0 @@
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};

View File

@@ -1,37 +0,0 @@
## Input
```javascript
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}
```
## Error
```
Found 1 error:
Invariant: (BuildHIR::lowerAssignment) Could not find binding for declaration.
error.bug-invariant-couldnt-find-binding-for-decl.ts:7:14
5 | try {
6 | // do something
> 7 | } catch ({status}) {
| ^^^^^^ (BuildHIR::lowerAssignment) Could not find binding for declaration.
8 | // do something
9 | }
10 | }, []);
```

View File

@@ -1,11 +0,0 @@
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}

View File

@@ -1,32 +0,0 @@
## Input
```javascript
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}
```
## Error
```
Found 1 error:
Invariant: Expected a break target
```

View File

@@ -1,15 +0,0 @@
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}

View File

@@ -1,44 +0,0 @@
## Input
```javascript
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};
```
## Error
```
Found 1 error:
Invariant: Expected consistent kind for destructuring
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
error.bug-invariant-expected-consistent-destructuring.ts:9:9
7 |
8 | if (data) {
> 9 | ({a, b} = data);
| ^ Expected consistent kind for destructuring
10 | }
11 |
12 | const foo = useFoo(a);
```

View File

@@ -1,16 +0,0 @@
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};

View File

@@ -1,46 +0,0 @@
## Input
```javascript
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};
```
## Error
```
Found 1 error:
Invariant: Expected all references to a variable to be consistently local or context references
Identifier <unknown> err$7 is referenced as a context variable, but was previously referenced as a [object Object] variable.
error.bug-invariant-local-or-context-references.ts:15:13
13 | setState(_prevState => ({
14 | loading: false,
> 15 | error: err,
| ^^^ Expected all references to a variable to be consistently local or context references
16 | }));
17 | }
18 | };
```

View File

@@ -1,18 +0,0 @@
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};

View File

@@ -1,34 +0,0 @@
## Input
```javascript
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};
```
## Error
```
Found 1 error:
Invariant: Unexpected terminal in optional
error.bug-invariant-unexpected-terminal-in-optional.ts:3:16
1 | const Foo = ({json}) => {
2 | try {
> 3 | const foo = JSON.parse(json)?.foo;
| ^^^^ Unexpected terminal in optional
4 | return <span>{foo}</span>;
5 | } catch {
6 | return null;
```

View File

@@ -1,8 +0,0 @@
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};

View File

@@ -1,30 +0,0 @@
## Input
```javascript
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}
```
## Error
```
Found 1 error:
Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass
identifier 15 is unnamed.
```

View File

@@ -1,11 +0,0 @@
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}

View File

@@ -1,39 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;
```

View File

@@ -1,13 +0,0 @@
// @validateNoDerivedComputationsInEffects
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
}

View File

@@ -40,29 +40,36 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(5);
const $ = _c(7);
let t0;
if ($[0] !== props.a) {
const a = [props.a];
t0 = [a];
t0 = [props.a];
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const b = t0;
const a = t0;
let t1;
if ($[2] !== a) {
t1 = [a];
$[2] = a;
$[3] = t1;
} else {
t1 = $[3];
}
const b = t1;
let c;
if ($[2] !== b || $[3] !== props.b) {
if ($[4] !== b || $[5] !== props.b) {
c = [];
const d = {};
d.b = b;
c.push(props.b);
$[2] = b;
$[3] = props.b;
$[4] = c;
$[4] = b;
$[5] = props.b;
$[6] = c;
} else {
c = $[4];
c = $[6];
}
return c;
}

View File

@@ -32,10 +32,10 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props.name) {
const element = fbt._(
t0 = fbt._(
"Hello {a really long description that got split into multiple lines}",
[
fbt._param(
@@ -46,14 +46,21 @@ function Component(props) {
],
{ hk: "1euPUp" },
);
t0 = element.toString();
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const element = t0;
let t1;
if ($[2] !== element) {
t1 = element.toString();
$[2] = element;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -27,28 +27,27 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props.name) {
const element = fbt._(
'Hello {"user" name}',
[
fbt._param(
'"user" name',
props.name,
),
],
{ hk: "S0vMe" },
);
t0 = element.toString();
t0 = fbt._('Hello {"user" name}', [fbt._param('"user" name', props.name)], {
hk: "S0vMe",
});
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const element = t0;
let t1;
if ($[2] !== element) {
t1 = element.toString();
$[2] = element;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -27,28 +27,29 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props.name) {
const element = fbt._(
t0 = fbt._(
"Hello {user name ☺}",
[
fbt._param(
"user name \u263A",
props.name,
),
],
[fbt._param("user name \u263A", props.name)],
{ hk: "1En1lp" },
);
t0 = element.toString();
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const element = t0;
let t1;
if ($[2] !== element) {
t1 = element.toString();
$[2] = element;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -27,28 +27,27 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props.name) {
const element = fbt._(
"Hello {user name}",
[
fbt._param(
"user name",
props.name,
),
],
{ hk: "2zEDKF" },
);
t0 = element.toString();
t0 = fbt._("Hello {user name}", [fbt._param("user name", props.name)], {
hk: "2zEDKF",
});
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const element = t0;
let t1;
if ($[2] !== element) {
t1 = element.toString();
$[2] = element;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -22,21 +22,28 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
const id = useSelectedEntitytId();
let t0;
if ($[0] !== id) {
const onLoad = () => {
t0 = () => {
log(id);
};
t0 = <Foo onLoad={onLoad} />;
$[0] = id;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const onLoad = t0;
let t1;
if ($[2] !== onLoad) {
t1 = <Foo onLoad={onLoad} />;
$[2] = onLoad;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
```

View File

@@ -21,20 +21,27 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props) {
const f = function () {
t0 = function () {
return <div>{props.name}</div>;
};
t0 = f.call();
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const f = t0;
let t1;
if ($[2] !== f) {
t1 = f.call();
$[2] = f;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -55,26 +55,33 @@ import { Stringify } from "shared-runtime";
* (kind: exception) Cannot read properties of null (reading 'prop')
*/
function Component(t0) {
const $ = _c(3);
const $ = _c(5);
const { obj, isObjNull } = t0;
let t1;
if ($[0] !== isObjNull || $[1] !== obj) {
const callback = () => {
t1 = () => {
if (!isObjNull) {
return obj.prop;
} else {
return null;
}
};
t1 = <Stringify shouldInvokeFns={true} callback={callback} />;
$[0] = isObjNull;
$[1] = obj;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
const callback = t1;
let t2;
if ($[3] !== callback) {
t2 = <Stringify shouldInvokeFns={true} callback={callback} />;
$[3] = callback;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -27,7 +27,7 @@ function useFoo() {
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
function useFoo() {
const $ = _c(7);
const $ = _c(9);
const onClick = (response) => {
setState(DISABLED_FORM);
};
@@ -48,24 +48,31 @@ function useFoo() {
const handleLogout = t1;
let t2;
if ($[2] !== handleLogout) {
const getComponent = () => <ColumnItem onPress={() => handleLogout()} />;
t2 = getComponent();
t2 = () => <ColumnItem onPress={() => handleLogout()} />;
$[2] = handleLogout;
$[3] = t2;
} else {
t2 = $[3];
}
const getComponent = t2;
let t3;
if ($[4] !== onClick || $[5] !== t2) {
t3 = [t2, onClick];
$[4] = onClick;
$[5] = t2;
$[6] = t3;
if ($[4] !== getComponent) {
t3 = getComponent();
$[4] = getComponent;
$[5] = t3;
} else {
t3 = $[6];
t3 = $[5];
}
return t3;
let t4;
if ($[6] !== onClick || $[7] !== t3) {
t4 = [t3, onClick];
$[6] = onClick;
$[7] = t3;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
}
```

View File

@@ -42,58 +42,74 @@ import { c as _c } from "react/compiler-runtime"; /**
* conservative and assume that all named lambdas are conditionally called.
*/
function useFoo(t0) {
const $ = _c(13);
const $ = _c(17);
const { arr1, arr2 } = t0;
let t1;
if ($[0] !== arr1[0]) {
const getVal1 = () => arr1[0].value;
t1 = (e) => getVal1() + e.value;
t1 = () => arr1[0].value;
$[0] = arr1[0];
$[1] = t1;
} else {
t1 = $[1];
}
const cb1 = t1;
const getVal1 = t1;
let t2;
if ($[2] !== arr1 || $[3] !== cb1) {
t2 = arr1.map(cb1);
$[2] = arr1;
$[3] = cb1;
$[4] = t2;
if ($[2] !== getVal1) {
t2 = (e) => getVal1() + e.value;
$[2] = getVal1;
$[3] = t2;
} else {
t2 = $[4];
t2 = $[3];
}
const x = t2;
const cb1 = t2;
let t3;
if ($[5] !== arr2) {
const getVal2 = () => arr2[0].value;
t3 = (e_0) => getVal2() + e_0.value;
$[5] = arr2;
if ($[4] !== arr1 || $[5] !== cb1) {
t3 = arr1.map(cb1);
$[4] = arr1;
$[5] = cb1;
$[6] = t3;
} else {
t3 = $[6];
}
const cb2 = t3;
const x = t3;
let t4;
if ($[7] !== arr1 || $[8] !== cb2) {
t4 = arr1.map(cb2);
$[7] = arr1;
$[8] = cb2;
$[9] = t4;
if ($[7] !== arr2) {
t4 = () => arr2[0].value;
$[7] = arr2;
$[8] = t4;
} else {
t4 = $[9];
t4 = $[8];
}
const y = t4;
const getVal2 = t4;
let t5;
if ($[10] !== x || $[11] !== y) {
t5 = [x, y];
$[10] = x;
$[11] = y;
$[12] = t5;
if ($[9] !== getVal2) {
t5 = (e_0) => getVal2() + e_0.value;
$[9] = getVal2;
$[10] = t5;
} else {
t5 = $[12];
t5 = $[10];
}
return t5;
const cb2 = t5;
let t6;
if ($[11] !== arr1 || $[12] !== cb2) {
t6 = arr1.map(cb2);
$[11] = arr1;
$[12] = cb2;
$[13] = t6;
} else {
t6 = $[13];
}
const y = t6;
let t7;
if ($[14] !== x || $[15] !== y) {
t7 = [x, y];
$[14] = x;
$[15] = y;
$[16] = t7;
} else {
t7 = $[16];
}
return t7;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -42,7 +42,7 @@ import { useRef } from "react";
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(7);
const $ = _c(9);
const { a, b } = t0;
let t1;
if ($[0] !== a.value) {
@@ -70,22 +70,29 @@ function Component(t0) {
const hasLogged = useRef(false);
let t3;
if ($[4] !== logA || $[5] !== logB) {
const log = () => {
t3 = () => {
if (!hasLogged.current) {
logA();
logB();
hasLogged.current = true;
}
};
t3 = <Stringify log={log} shouldInvokeFns={true} />;
$[4] = logA;
$[5] = logB;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
const log = t3;
let t4;
if ($[7] !== log) {
t4 = <Stringify log={log} shouldInvokeFns={true} />;
$[7] = log;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -24,20 +24,28 @@ import { c as _c } from "react/compiler-runtime";
import * as SharedRuntime from "shared-runtime";
import { invoke } from "shared-runtime";
function useComponentFactory(t0) {
const $ = _c(2);
const $ = _c(4);
const { name } = t0;
let t1;
if ($[0] !== name) {
const cb = () => (
t1 = () => (
<SharedRuntime.Stringify>hello world {name}</SharedRuntime.Stringify>
);
t1 = invoke(cb);
$[0] = name;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
const cb = t1;
let t2;
if ($[2] !== cb) {
t2 = invoke(cb);
$[2] = cb;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -25,18 +25,34 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function Component(props) {
const $ = _c(2);
const $ = _c(6);
let t0;
if ($[0] !== props.a) {
const item = { a: props.a };
const items = [item];
t0 = items.map(_temp);
t0 = { a: props.a };
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const mapped = t0;
const item = t0;
let t1;
if ($[2] !== item) {
t1 = [item];
$[2] = item;
$[3] = t1;
} else {
t1 = $[3];
}
const items = t1;
let t2;
if ($[4] !== items) {
t2 = items.map(_temp);
$[4] = items;
$[5] = t2;
} else {
t2 = $[5];
}
const mapped = t2;
return mapped;
}
function _temp(item_0) {

View File

@@ -23,20 +23,28 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a, b) {
const $ = _c(3);
const $ = _c(5);
let t0;
if ($[0] !== a || $[1] !== b) {
const z = { a, b };
t0 = function () {
console.log(z);
};
t0 = { a, b };
$[0] = a;
$[1] = b;
$[2] = t0;
} else {
t0 = $[2];
}
const x = t0;
const z = t0;
let t1;
if ($[3] !== z) {
t1 = function () {
console.log(z);
};
$[3] = z;
$[4] = t1;
} else {
t1 = $[4];
}
const x = t1;
return x;
}

View File

@@ -29,7 +29,7 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { mutate, shallowCopy, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const $ = _c(6);
const { a } = t0;
let local;
if ($[0] !== a) {
@@ -42,14 +42,22 @@ function useFoo(t0) {
}
let t1;
if ($[2] !== local.b.c) {
const fn = () => local.b.c;
t1 = <Stringify fn={fn} shouldInvokeFns={true} />;
t1 = () => local.b.c;
$[2] = local.b.c;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
const fn = t1;
let t2;
if ($[4] !== fn) {
t2 = <Stringify fn={fn} shouldInvokeFns={true} />;
$[4] = fn;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -29,7 +29,7 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { shallowCopy, Stringify, mutate } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const $ = _c(6);
const { a } = t0;
let local;
if ($[0] !== a) {
@@ -42,14 +42,22 @@ function useFoo(t0) {
}
let t1;
if ($[2] !== local) {
const fn = () => [() => local.b.c];
t1 = <Stringify fn={fn} shouldInvokeFns={true} />;
t1 = () => [() => local.b.c];
$[2] = local;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
const fn = t1;
let t2;
if ($[4] !== fn) {
t2 = <Stringify fn={fn} shouldInvokeFns={true} />;
$[4] = fn;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -31,19 +31,26 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(2);
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a.b.c) {
const fn = () => () => ({ value: a.b.c });
t1 = <Stringify fn={fn} shouldInvokeFns={true} />;
t1 = () => () => ({ value: a.b.c });
$[0] = a.b.c;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
const fn = t1;
let t2;
if ($[2] !== fn) {
t2 = <Stringify fn={fn} shouldInvokeFns={true} />;
$[2] = fn;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -31,23 +31,30 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { identity, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(2);
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a) {
const x = {
t1 = {
fn() {
return identity(a.b.c);
},
};
t1 = <Stringify x={x} shouldInvokeFns={true} />;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
const x = t1;
let t2;
if ($[2] !== x) {
t2 = <Stringify x={x} shouldInvokeFns={true} />;
$[2] = x;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -29,7 +29,7 @@ import { c as _c } from "react/compiler-runtime";
import { useHook } from "shared-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(6);
const o = {};
let t0;
if ($[0] !== props.value) {
@@ -44,15 +44,22 @@ function Component(props) {
o.value = props.value;
let t1;
if ($[2] !== x) {
const y = <div>{x}</div>;
t1 = <div>{y}</div>;
t1 = <div>{x}</div>;
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
const y = t1;
let t2;
if ($[4] !== y) {
t2 = <div>{y}</div>;
$[4] = y;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -31,7 +31,7 @@ import { c as _c } from "react/compiler-runtime";
import { useHook, identity } from "shared-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let x = 42;
if (props.cond) {
x = [];
@@ -41,15 +41,22 @@ function Component(props) {
identity(x);
let t0;
if ($[0] !== x) {
const y = [x];
t0 = [y];
t0 = [x];
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const y = t0;
let t1;
if ($[2] !== y) {
t1 = [y];
$[2] = y;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -66,17 +66,25 @@ function Parent(t0) {
}
function ChildImpl(_props, ref) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== ref) {
const cb = () => ref.current;
t0 = <Stringify cb={cb} shouldInvokeFns={true} />;
t0 = () => ref.current;
$[0] = ref;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const cb = t0;
let t1;
if ($[2] !== cb) {
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[2] = cb;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
const Child = forwardRef(ChildImpl);

View File

@@ -41,21 +41,29 @@ import { Stringify } from "shared-runtime";
* `pruneNonReactiveDependencies`
*/
function Component(t0) {
const $ = _c(2);
const $ = _c(4);
const { cond } = t0;
const ref1 = useRef(1);
const ref2 = useRef(2);
const ref = cond ? ref1 : ref2;
let t1;
if ($[0] !== ref) {
const cb = () => ref.current;
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
t1 = () => ref.current;
$[0] = ref;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
const cb = t1;
let t2;
if ($[2] !== cb) {
t2 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[2] = cb;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -35,23 +35,34 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(6);
let a;
let t0;
if ($[0] !== props.b) {
const a = {};
a = {};
const b = [];
b.push(props.b);
a.a = null;
const c = [a];
t0 = [c, a];
t0 = [a];
$[0] = props.b;
$[1] = t0;
$[1] = a;
$[2] = t0;
} else {
t0 = $[1];
a = $[1];
t0 = $[2];
}
return t0;
const c = t0;
let t1;
if ($[3] !== a || $[4] !== c) {
t1 = [c, a];
$[3] = a;
$[4] = c;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -32,24 +32,18 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let x;
const $ = _c(2);
let t0;
if ($[0] !== props.input) {
x = [];
const x = [];
const y = x;
y.push(props.input);
$[0] = props.input;
$[1] = x;
} else {
x = $[1];
}
let t0;
if ($[2] !== x[0]) {
t0 = [x[0]];
$[2] = x[0];
$[3] = t0;
$[0] = props.input;
$[1] = t0;
} else {
t0 = $[3];
t0 = $[1];
}
return t0;
}

View File

@@ -35,28 +35,22 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let x;
const $ = _c(2);
let t0;
if ($[0] !== props.input) {
x = [];
const x = [];
const f = (arg) => {
const y = x;
y.push(arg);
};
f(props.input);
$[0] = props.input;
$[1] = x;
} else {
x = $[1];
}
let t0;
if ($[2] !== x[0]) {
t0 = [x[0]];
$[2] = x[0];
$[3] = t0;
$[0] = props.input;
$[1] = t0;
} else {
t0 = $[3];
t0 = $[1];
}
return t0;
}

View File

@@ -18,21 +18,28 @@ function Foo({a}) {
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender:false
function Foo(t0) {
const $ = _c(2);
const $ = _c(4);
const { a } = t0;
const ref = useRef();
const val = ref.current;
let t1;
if ($[0] !== a) {
const x = { a, val };
t1 = <VideoList videos={x} />;
t1 = { a, val };
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
const x = t1;
let t2;
if ($[2] !== x) {
t2 = <VideoList videos={x} />;
$[2] = x;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```

View File

@@ -17,20 +17,27 @@ function Foo({a}) {
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender:false
function Foo(t0) {
const $ = _c(2);
const $ = _c(4);
const { a } = t0;
const ref = useRef();
let t1;
if ($[0] !== a) {
const x = { a, val: ref.current };
t1 = <VideoList videos={x} />;
t1 = { a, val: ref.current };
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
const x = t1;
let t2;
if ($[2] !== x) {
t2 = <VideoList videos={x} />;
$[2] = x;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```

View File

@@ -41,11 +41,11 @@ const $ = "module_$";
const t0 = "module_t0";
const c_0 = "module_c_0";
function useFoo(props) {
const $0 = _c(2);
const $0 = _c(4);
const c_00 = $0[0] !== props.value;
let t1;
if (c_00) {
const a = () => {
t1 = () => {
const b = () => {
const c = () => {
console.log($);
@@ -57,14 +57,22 @@ function useFoo(props) {
};
return b;
};
t1 = a()()();
$0[0] = props.value;
$0[1] = t1;
} else {
t1 = $0[1];
}
return t1;
const a = t1;
const c_2 = $0[2] !== a;
let t2;
if (c_2) {
t2 = a()()();
$0[2] = a;
$0[3] = t2;
} else {
t2 = $0[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -35,44 +35,60 @@ import { useMemo } from "react";
import { useFragment } from "shared-runtime";
function Component() {
const $ = _c(7);
const $ = _c(11);
const data = useFragment();
let t0;
if ($[0] !== data.nodes) {
const nodes = data.nodes ?? [];
const flatMap = nodes.flatMap(_temp);
t0 = flatMap.filter(_temp2);
t0 = data.nodes ?? [];
$[0] = data.nodes;
$[1] = t0;
} else {
t0 = $[1];
}
const filtered = t0;
const nodes = t0;
let t1;
if ($[2] !== filtered) {
t1 = filtered.map();
$[2] = filtered;
if ($[2] !== nodes) {
t1 = nodes.flatMap(_temp);
$[2] = nodes;
$[3] = t1;
} else {
t1 = $[3];
}
const map = t1;
const index = filtered.findIndex(_temp3);
const flatMap = t1;
let t2;
if ($[4] !== index || $[5] !== map) {
t2 = (
if ($[4] !== flatMap) {
t2 = flatMap.filter(_temp2);
$[4] = flatMap;
$[5] = t2;
} else {
t2 = $[5];
}
const filtered = t2;
let t3;
if ($[6] !== filtered) {
t3 = filtered.map();
$[6] = filtered;
$[7] = t3;
} else {
t3 = $[7];
}
const map = t3;
const index = filtered.findIndex(_temp3);
let t4;
if ($[8] !== index || $[9] !== map) {
t4 = (
<div>
{map}
{index}
</div>
);
$[4] = index;
$[5] = map;
$[6] = t2;
$[8] = index;
$[9] = map;
$[10] = t4;
} else {
t2 = $[6];
t4 = $[10];
}
return t2;
return t4;
}
function _temp3(x) {
return x === null;

View File

@@ -32,44 +32,60 @@ import { useMemo } from "react";
import { useFragment } from "shared-runtime";
function Component() {
const $ = _c(7);
const $ = _c(11);
const data = useFragment();
let t0;
if ($[0] !== data.nodes) {
const nodes = data.nodes ?? [];
const flatMap = nodes.flatMap(_temp);
t0 = flatMap.filter(_temp2);
t0 = data.nodes ?? [];
$[0] = data.nodes;
$[1] = t0;
} else {
t0 = $[1];
}
const filtered = t0;
const nodes = t0;
let t1;
if ($[2] !== filtered) {
t1 = filtered.map();
$[2] = filtered;
if ($[2] !== nodes) {
t1 = nodes.flatMap(_temp);
$[2] = nodes;
$[3] = t1;
} else {
t1 = $[3];
}
const map = t1;
const index = filtered.findIndex(_temp3);
const flatMap = t1;
let t2;
if ($[4] !== index || $[5] !== map) {
t2 = (
if ($[4] !== flatMap) {
t2 = flatMap.filter(_temp2);
$[4] = flatMap;
$[5] = t2;
} else {
t2 = $[5];
}
const filtered = t2;
let t3;
if ($[6] !== filtered) {
t3 = filtered.map();
$[6] = filtered;
$[7] = t3;
} else {
t3 = $[7];
}
const map = t3;
const index = filtered.findIndex(_temp3);
let t4;
if ($[8] !== index || $[9] !== map) {
t4 = (
<div>
{map}
{index}
</div>
);
$[4] = index;
$[5] = map;
$[6] = t2;
$[8] = index;
$[9] = map;
$[10] = t4;
} else {
t2 = $[6];
t4 = $[10];
}
return t2;
return t4;
}
function _temp3(x) {
return x === null;

View File

@@ -30,33 +30,40 @@ import { c as _c } from "react/compiler-runtime";
import { identity, makeObject_Primitives, Stringify } from "shared-runtime";
function Example(props) {
const $ = _c(5);
const $ = _c(7);
const object = props.object;
let t0;
if ($[0] !== object || $[1] !== props.value) {
const f = () => {
t0 = () => {
const obj = identity(object);
obj.property = props.value;
return obj;
};
t0 = f();
$[0] = object;
$[1] = props.value;
$[2] = t0;
} else {
t0 = $[2];
}
const obj_0 = t0;
const f = t0;
let t1;
if ($[3] !== obj_0) {
t1 = <Stringify obj={obj_0} />;
$[3] = obj_0;
if ($[3] !== f) {
t1 = f();
$[3] = f;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
const obj_0 = t1;
let t2;
if ($[5] !== obj_0) {
t2 = <Stringify obj={obj_0} />;
$[5] = obj_0;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -30,33 +30,40 @@ import { c as _c } from "react/compiler-runtime";
import { makeObject_Primitives, Stringify } from "shared-runtime";
function Example(props) {
const $ = _c(5);
const $ = _c(7);
const object = props.object;
let t0;
if ($[0] !== object || $[1] !== props.value) {
const f = () => {
t0 = () => {
const obj = object.makeObject();
obj.property = props.value;
return obj;
};
t0 = f();
$[0] = object;
$[1] = props.value;
$[2] = t0;
} else {
t0 = $[2];
}
const obj_0 = t0;
const f = t0;
let t1;
if ($[3] !== obj_0) {
t1 = <Stringify obj={obj_0} />;
$[3] = obj_0;
if ($[3] !== f) {
t1 = f();
$[3] = f;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
const obj_0 = t1;
let t2;
if ($[5] !== obj_0) {
t2 = <Stringify obj={obj_0} />;
$[5] = obj_0;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -45,7 +45,7 @@ import { Stringify, identity, makeArray, toJSON } from "shared-runtime";
import { useMemo } from "react";
function Component(props) {
const $ = _c(10);
const $ = _c(12);
let t0;
let t1;
if ($[0] !== props) {
@@ -71,50 +71,57 @@ function Component(props) {
}
let t2;
if ($[3] !== t0) {
const linkProps = { url: t0 };
const x = {};
let t3;
let t4;
let t5;
let t6;
let t7;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t3 = [1];
t4 = [2];
t5 = [3];
t6 = [4];
t7 = [5];
$[5] = t3;
$[6] = t4;
$[7] = t5;
$[8] = t6;
$[9] = t7;
} else {
t3 = $[5];
t4 = $[6];
t5 = $[7];
t6 = $[8];
t7 = $[9];
}
t2 = (
<Stringify
link={linkProps}
val1={t3}
val2={t4}
val3={t5}
val4={t6}
val5={t7}
>
{makeArray(x, 2)}
</Stringify>
);
t2 = { url: t0 };
$[3] = t0;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
const linkProps = t2;
let t3;
if ($[5] !== linkProps) {
const x = {};
let t4;
let t5;
let t6;
let t7;
let t8;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = [1];
t5 = [2];
t6 = [3];
t7 = [4];
t8 = [5];
$[7] = t4;
$[8] = t5;
$[9] = t6;
$[10] = t7;
$[11] = t8;
} else {
t4 = $[7];
t5 = $[8];
t6 = $[9];
t7 = $[10];
t8 = $[11];
}
t3 = (
<Stringify
link={linkProps}
val1={t4}
val2={t5}
val3={t6}
val4={t7}
val5={t8}
>
{makeArray(x, 2)}
</Stringify>
);
$[5] = linkProps;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -48,21 +48,28 @@ import { c as _c } from "react/compiler-runtime";
import { StaticText1, Stringify, Text } from "shared-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
const { buttons } = props;
let t0;
if ($[0] !== buttons) {
const [, ...nonPrimaryButtons] = buttons;
const renderedNonPrimaryButtons = nonPrimaryButtons.map(_temp);
t0 = <StaticText1>{renderedNonPrimaryButtons}</StaticText1>;
t0 = nonPrimaryButtons.map(_temp);
$[0] = buttons;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const renderedNonPrimaryButtons = t0;
let t1;
if ($[2] !== renderedNonPrimaryButtons) {
t1 = <StaticText1>{renderedNonPrimaryButtons}</StaticText1>;
$[2] = renderedNonPrimaryButtons;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
function _temp(buttonProps, i) {
return (

View File

@@ -29,10 +29,10 @@ import { c as _c } from "react/compiler-runtime";
import { throwInput } from "shared-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props) {
const callback = () => {
t0 = () => {
try {
throwInput([props.value]);
} catch (t1) {
@@ -40,14 +40,21 @@ function Component(props) {
return e;
}
};
t0 = callback();
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const callback = t0;
let t1;
if ($[2] !== callback) {
t1 = callback();
$[2] = callback;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -25,17 +25,25 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableUseTypeAnnotations
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props.id) {
const x = makeArray(props.id);
t0 = x.at(0);
t0 = makeArray(props.id);
$[0] = props.id;
$[1] = t0;
} else {
t0 = $[1];
}
const y = t0;
const x = t0;
let t1;
if ($[2] !== x) {
t1 = x.at(0);
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
const y = t1;
return y;
}

View File

@@ -29,17 +29,25 @@ import { c as _c } from "react/compiler-runtime";
import { identity } from "shared-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props.id) {
const x = makeArray(props.id);
t0 = x.at(0);
t0 = makeArray(props.id);
$[0] = props.id;
$[1] = t0;
} else {
t0 = $[1];
}
const y = t0;
const x = t0;
let t1;
if ($[2] !== x) {
t1 = x.at(0);
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
const y = t1;
return y;
}

View File

@@ -22,17 +22,25 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
function Component(props) {
"use memo";
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props.foo) {
const x = [props.foo];
t0 = <div x={x}>"foo"</div>;
t0 = [props.foo];
$[0] = props.foo;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const x = t0;
let t1;
if ($[2] !== x) {
t1 = <div x={x}>"foo"</div>;
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -39,34 +39,41 @@ import { Stringify } from "shared-runtime";
const FooContext = createContext({ current: true });
function Component(props) {
const $ = _c(4);
const $ = _c(6);
const foo = useContext(FooContext);
let t0;
if ($[0] !== foo.current) {
const getValue = () => {
t0 = () => {
if (foo.current) {
return {};
} else {
return null;
}
};
t0 = getValue();
$[0] = foo.current;
$[1] = t0;
} else {
t0 = $[1];
}
const value = t0;
const getValue = t0;
let t1;
if ($[2] !== value) {
t1 = <Stringify value={value} />;
$[2] = value;
if ($[2] !== getValue) {
t1 = getValue();
$[2] = getValue;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
const value = t1;
let t2;
if ($[4] !== value) {
t2 = <Stringify value={value} />;
$[4] = value;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -879,7 +879,7 @@ function initializeDebugChunk(
waitForReference(
debugChunk,
{}, // noop, since we'll have already added an entry to debug info
'debug', // noop, but we need it to not be empty string since that indicates the root object
'', // noop
response,
initializeDebugInfo,
[''], // path

View File

@@ -490,7 +490,7 @@ export function logComponentAwait(
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0, '');
} else if (value !== undefined) {
addValueToProperties('awaited value', value, properties, 0, '');
addValueToProperties('Resolved', value, properties, 0, '');
}
const tooltipText = getIOLongName(
asyncInfo.awaited,
@@ -547,7 +547,7 @@ export function logIOInfoErrored(
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
const properties = [['rejected with', message]];
const properties = [['Rejected', message]];
const tooltipText =
getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
debugTask.run(

View File

@@ -52,7 +52,7 @@ test.describe('Components', () => {
test('Should allow elements to be inspected', async () => {
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', '<List>\n<App>');
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
// Prop names/values may not be editable based on the React version.
// If they're not editable, make sure they degrade gracefully
@@ -119,7 +119,7 @@ test.describe('Components', () => {
runOnlyForReactRange('>=16.8');
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', '<List>\n<App>', true);
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp', true);
// Then read the inspected values.
const sourceText = await page.evaluate(() => {
@@ -142,7 +142,7 @@ test.describe('Components', () => {
runOnlyForReactRange('>=16.8');
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', '<List>\n<App>');
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
// Then edit the label prop.
await page.evaluate(() => {
@@ -177,7 +177,7 @@ test.describe('Components', () => {
runOnlyForReactRange('>=16.8');
// Select the List component DevTools.
await devToolsUtils.selectElement(page, 'List', '<App>');
await devToolsUtils.selectElement(page, 'List', 'App');
// Then click to load and parse hook names.
await devToolsUtils.clickButton(page, 'LoadHookNamesButton');

View File

@@ -17,10 +17,8 @@ describe('Store', () => {
let act;
let actAsync;
let bridge;
let createDisplayNameFilter;
let getRendererID;
let legacyRender;
let previousComponentFilters;
let store;
let withErrorsOrWarningsIgnored;
@@ -31,8 +29,6 @@ describe('Store', () => {
bridge = global.bridge;
store = global.store;
previousComponentFilters = store.componentFilters;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
@@ -42,14 +38,9 @@ describe('Store', () => {
actAsync = utils.actAsync;
getRendererID = utils.getRendererID;
legacyRender = utils.legacyRender;
createDisplayNameFilter = utils.createDisplayNameFilter;
withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
});
afterEach(() => {
store.componentFilters = previousComponentFilters;
});
const {render, unmount, createContainer} = getVersionedRenderImplementation();
// @reactVersion >= 18.0
@@ -138,72 +129,6 @@ describe('Store', () => {
`);
});
it('should handle reorder of filtered elements', async () => {
function IgnoreMePassthrough({children}) {
return children;
}
function PassThrough({children}) {
return children;
}
await actAsync(
async () =>
(store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]),
);
await act(() => {
render(
<PassThrough key="e" name="e">
<IgnoreMePassthrough key="e1">
<PassThrough name="e-child-one">
<p>e1</p>
</PassThrough>
</IgnoreMePassthrough>
<IgnoreMePassthrough key="e2">
<PassThrough name="e-child-two">
<div>e2</div>
</PassThrough>
</IgnoreMePassthrough>
</PassThrough>,
);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <PassThrough key="e">
▾ <PassThrough>
<p>
▾ <PassThrough>
<div>
`);
await act(() => {
render(
<PassThrough key="e" name="e">
<IgnoreMePassthrough key="e2">
<PassThrough name="e-child-two">
<div>e2</div>
</PassThrough>
</IgnoreMePassthrough>
<IgnoreMePassthrough key="e1">
<PassThrough name="e-child-one">
<p>e1</p>
</PassThrough>
</IgnoreMePassthrough>
</PassThrough>,
);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <PassThrough key="e">
▾ <PassThrough>
<div>
▾ <PassThrough>
<p>
`);
});
describe('StrictMode compliance', () => {
it('should mark strict root elements as strict', async () => {
const App = () => <Component />;

View File

@@ -207,11 +207,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Activity>
<div>
<Activity>
`);
[root]
▾ <Activity>
<div>
<Activity>
<div>
`);
await actAsync(
async () =>
@@ -221,9 +222,10 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
<div>
`);
[root]
<div>
<div>
`);
await actAsync(
async () =>
@@ -233,11 +235,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Activity>
<div>
<Activity>
`);
[root]
▾ <Activity>
<div>
<Activity>
<div>
`);
}
});
@@ -259,12 +262,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
await actAsync(
async () =>
@@ -274,12 +277,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
await actAsync(
async () =>
@@ -289,12 +292,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
}
});

View File

@@ -19,8 +19,8 @@ import {
formatWithStyles,
gt,
gte,
parseSourceFromComponentStack,
} from 'react-devtools-shared/src/backend/utils';
import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
import {
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
REACT_STRICT_MODE_TYPE as StrictMode,
@@ -306,20 +306,20 @@ describe('utils', () => {
});
});
describe('extractLocationFromComponentStack', () => {
describe('parseSourceFromComponentStack', () => {
it('should return null if passed empty string', () => {
expect(extractLocationFromComponentStack('')).toEqual(null);
expect(parseSourceFromComponentStack('')).toEqual(null);
});
it('should construct the source from the first frame if available', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
'at l (https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js:1:10389)\n' +
'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' +
'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n',
),
).toEqual([
'l',
'',
'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
1,
10389,
@@ -328,7 +328,7 @@ describe('utils', () => {
it('should construct the source from highest available frame', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
' at Q\n' +
' at a\n' +
' at m (https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236)\n' +
@@ -342,7 +342,7 @@ describe('utils', () => {
' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)',
),
).toEqual([
'm',
'',
'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
5,
9236,
@@ -351,7 +351,7 @@ describe('utils', () => {
it('should construct the source from frame, which has only url specified', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
' at Q\n' +
' at a\n' +
' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n',
@@ -366,13 +366,13 @@ describe('utils', () => {
it('should parse sourceURL correctly if it includes parentheses', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
'at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:307:11)\n' +
' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' +
' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)',
),
).toEqual([
'HotReload',
'',
'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
307,
11,
@@ -381,13 +381,13 @@ describe('utils', () => {
it('should support Firefox stack', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
'tt@https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165558\n' +
'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' +
'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513',
),
).toEqual([
'tt',
'',
'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
1,
165558,
@@ -401,7 +401,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.f = f;
function f() { }
//# sourceMappingURL=`;
const result = ['', 'http://test/a.mts', 1, 17];
const result = ['', 'http://test/a.mts', 1, 16];
const fs = {
'http://test/a.mts': `export function f() {}`,
'http://test/a.mjs.map': `{"version":3,"file":"a.mjs","sourceRoot":"","sources":["a.mts"],"names":[],"mappings":";;AAAA,cAAsB;AAAtB,SAAgB,CAAC,KAAI,CAAC"}`,

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,12 @@ export function formatOwnerStack(error: Error): string {
const prevPrepareStackTrace = Error.prepareStackTrace;
// $FlowFixMe[incompatible-type] It does accept undefined.
Error.prepareStackTrace = undefined;
let stack = error.stack;
const stack = error.stack;
Error.prepareStackTrace = prevPrepareStackTrace;
return formatOwnerStackString(stack);
}
export function formatOwnerStackString(stack: string): string {
if (stack.startsWith('Error: react-stack-top-frame\n')) {
// V8's default formatting prefixes with the error message which we
// don't want/need.

View File

@@ -12,11 +12,14 @@ import {compareVersions} from 'compare-versions';
import {dehydrate} from 'react-devtools-shared/src/hydration';
import isArray from 'shared/isArray';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
export {default as formatWithStyles} from './formatWithStyles';
export {default as formatConsoleArguments} from './formatConsoleArguments';
import {formatOwnerStackString} from '../shared/DevToolsOwnerStack';
// TODO: update this to the first React version that has a corresponding DevTools backend
const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
export function hasAssignedBackend(version?: string): boolean {
@@ -255,6 +258,186 @@ export const isReactNativeEnvironment = (): boolean => {
return window.document == null;
};
function extractLocation(url: string): null | {
functionName?: string,
sourceURL: string,
line?: string,
column?: string,
} {
if (url.indexOf(':') === -1) {
return null;
}
// remove any parentheses from start and end
const withoutParentheses = url.replace(/^\(+/, '').replace(/\)+$/, '');
const locationParts = /(at )?(.+?)(?::(\d+))?(?::(\d+))?$/.exec(
withoutParentheses,
);
if (locationParts == null) {
return null;
}
const functionName = ''; // TODO: Parse this in the regexp.
const [, , sourceURL, line, column] = locationParts;
return {functionName, sourceURL, line, column};
}
const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
function parseSourceFromChromeStack(
stack: string,
): ReactFunctionLocation | null {
const frames = stack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
const sanitizedFrame = frame.trim();
const locationInParenthesesMatch = sanitizedFrame.match(/ (\(.+\)$)/);
const possibleLocation = locationInParenthesesMatch
? locationInParenthesesMatch[1]
: sanitizedFrame;
const location = extractLocation(possibleLocation);
// Continue the search until at least sourceURL is found
if (location == null) {
continue;
}
const {functionName, sourceURL, line = '1', column = '1'} = location;
return [
functionName || '',
sourceURL,
parseInt(line, 10),
parseInt(column, 10),
];
}
return null;
}
function parseSourceFromFirefoxStack(
stack: string,
): ReactFunctionLocation | null {
const frames = stack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
const sanitizedFrame = frame.trim();
const frameWithoutFunctionName = sanitizedFrame.replace(
/((.*".+"[^@]*)?[^@]*)(?:@)/,
'',
);
const location = extractLocation(frameWithoutFunctionName);
// Continue the search until at least sourceURL is found
if (location == null) {
continue;
}
const {functionName, sourceURL, line = '1', column = '1'} = location;
return [
functionName || '',
sourceURL,
parseInt(line, 10),
parseInt(column, 10),
];
}
return null;
}
export function parseSourceFromComponentStack(
componentStack: string,
): ReactFunctionLocation | null {
if (componentStack.match(CHROME_STACK_REGEXP)) {
return parseSourceFromChromeStack(componentStack);
}
return parseSourceFromFirefoxStack(componentStack);
}
let collectedLocation: ReactFunctionLocation | null = null;
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
let result: null | ReactFunctionLocation = null;
// Collect structured stack traces from the callsites.
// We mirror how V8 serializes stack frames and how we later parse them.
for (let i = 0; i < structuredStackTrace.length; i++) {
const callSite = structuredStackTrace[i];
const name = callSite.getFunctionName();
if (
name != null &&
(name.includes('react_stack_bottom_frame') ||
name.includes('react-stack-bottom-frame'))
) {
// We pick the last frame that matches before the bottom frame since
// that will be immediately inside the component as opposed to some helper.
// If we don't find a bottom frame then we bail to string parsing.
collectedLocation = result;
// Skip everything after the bottom frame since it'll be internals.
break;
} else {
const sourceURL = callSite.getScriptNameOrSourceURL();
const line =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingLineNumber === 'function'
? (callSite: any).getEnclosingLineNumber()
: callSite.getLineNumber();
const col =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber()
: callSite.getColumnNumber();
if (!sourceURL || !line || !col) {
// Skip eval etc. without source url. They don't have location.
continue;
}
result = [name, sourceURL, line, col];
}
}
// At the same time we generate a string stack trace just in case someone
// else reads it.
const name = error.name || 'Error';
const message = error.message || '';
let stack = name + ': ' + message;
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
return stack;
}
export function parseSourceFromOwnerStack(
error: Error,
): ReactFunctionLocation | null {
// First attempt to collected the structured data using prepareStackTrace.
collectedLocation = null;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTrace;
let stack;
try {
stack = error.stack;
} catch (e) {
// $FlowFixMe[incompatible-type] It does accept undefined.
Error.prepareStackTrace = undefined;
stack = error.stack;
} finally {
Error.prepareStackTrace = previousPrepare;
}
if (collectedLocation !== null) {
return collectedLocation;
}
if (stack == null) {
return null;
}
// Fallback to parsing the string form.
const componentStack = formatOwnerStackString(stack);
return parseSourceFromComponentStack(componentStack);
}
// 0.123456789 => 0.123
// Expects high-resolution timestamp in milliseconds, like from performance.now()
// Mainly used for optimizing the size of serialized profiling payload

View File

@@ -1,331 +0,0 @@
/**
/**
* 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.
*
* @flow
*/
import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes';
function parseStackTraceFromChromeStack(
stack: string,
skipFrames: number,
): ReactStackTrace {
if (stack.startsWith('Error: react-stack-top-frame\n')) {
// V8's default formatting prefixes with the error message which we
// don't want/need.
stack = stack.slice(29);
}
let idx = stack.indexOf('react_stack_bottom_frame');
if (idx === -1) {
idx = stack.indexOf('react-stack-bottom-frame');
}
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}
if (idx !== -1) {
// Cut off everything after the bottom frame since it'll be internals.
stack = stack.slice(0, idx);
}
const frames = stack.split('\n');
const parsedFrames: ReactStackTrace = [];
// We skip top frames here since they may or may not be parseable but we
// want to skip the same number of frames regardless. I.e. we can't do it
// in the caller.
for (let i = skipFrames; i < frames.length; i++) {
const parsed = chromeFrameRegExp.exec(frames[i]);
if (!parsed) {
continue;
}
let name = parsed[1] || '';
let isAsync = parsed[8] === 'async ';
if (name === '<anonymous>') {
name = '';
} else if (name.startsWith('async ')) {
name = name.slice(5);
isAsync = true;
}
let filename = parsed[2] || parsed[5] || '';
if (filename === '<anonymous>') {
filename = '';
}
const line = +(parsed[3] || parsed[6]);
const col = +(parsed[4] || parsed[7]);
parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
}
return parsedFrames;
}
const firefoxFrameRegExp = /^((?:.*".+")?[^@]*)@(.+):(\d+):(\d+)$/;
function parseStackTraceFromFirefoxStack(
stack: string,
skipFrames: number,
): ReactStackTrace {
let idx = stack.indexOf('react_stack_bottom_frame');
if (idx === -1) {
idx = stack.indexOf('react-stack-bottom-frame');
}
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}
if (idx !== -1) {
// Cut off everything after the bottom frame since it'll be internals.
stack = stack.slice(0, idx);
}
const frames = stack.split('\n');
const parsedFrames: ReactStackTrace = [];
// We skip top frames here since they may or may not be parseable but we
// want to skip the same number of frames regardless. I.e. we can't do it
// in the caller.
for (let i = skipFrames; i < frames.length; i++) {
const parsed = firefoxFrameRegExp.exec(frames[i]);
if (!parsed) {
continue;
}
const name = parsed[1] || '';
const filename = parsed[2] || '';
const line = +parsed[3];
const col = +parsed[4];
parsedFrames.push([name, filename, line, col, 0, 0, false]);
}
return parsedFrames;
}
const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
export function parseStackTraceFromString(
stack: string,
skipFrames: number,
): ReactStackTrace {
if (stack.match(CHROME_STACK_REGEXP)) {
return parseStackTraceFromChromeStack(stack, skipFrames);
}
return parseStackTraceFromFirefoxStack(stack, skipFrames);
}
let framesToSkip: number = 0;
let collectedStackTrace: null | ReactStackTrace = null;
const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
function getMethodCallName(callSite: CallSite): string {
const typeName = callSite.getTypeName();
const methodName = callSite.getMethodName();
const functionName = callSite.getFunctionName();
let result = '';
if (functionName) {
if (
typeName &&
identifierRegExp.test(functionName) &&
functionName !== typeName
) {
result += typeName + '.';
}
result += functionName;
if (
methodName &&
functionName !== methodName &&
!functionName.endsWith('.' + methodName) &&
!functionName.endsWith(' ' + methodName)
) {
result += ' [as ' + methodName + ']';
}
} else {
if (typeName) {
result += typeName + '.';
}
if (methodName) {
result += methodName;
} else {
result += '<anonymous>';
}
}
return result;
}
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
const result: ReactStackTrace = [];
// Collect structured stack traces from the callsites.
// We mirror how V8 serializes stack frames and how we later parse them.
for (let i = framesToSkip; i < structuredStackTrace.length; i++) {
const callSite = structuredStackTrace[i];
let name = callSite.getFunctionName() || '<anonymous>';
if (
name.includes('react_stack_bottom_frame') ||
name.includes('react-stack-bottom-frame')
) {
// Skip everything after the bottom frame since it'll be internals.
break;
} else if (callSite.isNative()) {
// $FlowFixMe[prop-missing]
const isAsync = callSite.isAsync();
result.push([name, '', 0, 0, 0, 0, isAsync]);
} else {
// We encode complex function calls as if they're part of the function
// name since we cannot simulate the complex ones and they look the same
// as function names in UIs on the client as well as stacks.
if (callSite.isConstructor()) {
name = 'new ' + name;
} else if (!callSite.isToplevel()) {
name = getMethodCallName(callSite);
}
if (name === '<anonymous>') {
name = '';
}
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
if (filename === '<anonymous>') {
filename = '';
if (callSite.isEval()) {
const origin = callSite.getEvalOrigin();
if (origin) {
filename = origin.toString() + ', <anonymous>';
}
}
}
const line = callSite.getLineNumber() || 0;
const col = callSite.getColumnNumber() || 0;
const enclosingLine: number =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingLineNumber === 'function'
? (callSite: any).getEnclosingLineNumber() || 0
: 0;
const enclosingCol: number =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber() || 0
: 0;
// $FlowFixMe[prop-missing]
const isAsync = callSite.isAsync();
result.push([
name,
filename,
line,
col,
enclosingLine,
enclosingCol,
isAsync,
]);
}
}
collectedStackTrace = result;
// At the same time we generate a string stack trace just in case someone
// else reads it. Ideally, we'd call the previous prepareStackTrace to
// ensure it's in the expected format but it's common for that to be
// source mapped and since we do a lot of eager parsing of errors, it
// would be slow in those environments. We could maybe just rely on those
// environments having to disable source mapping globally to speed things up.
// For now, we just generate a default V8 formatted stack trace without
// source mapping as a fallback.
const name = error.name || 'Error';
const message = error.message || '';
let stack = name + ': ' + message;
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
return stack;
}
// This matches either of these V8 formats.
// at name (filename:0:0)
// at filename:0:0
// at async filename:0:0
const chromeFrameRegExp =
/^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\<anonymous\>)\)|(?:async )?(.+):(\d+):(\d+)|\<anonymous\>)$/;
const stackTraceCache: WeakMap<Error, ReactStackTrace> = new WeakMap();
export function parseStackTrace(
error: Error,
skipFrames: number,
): ReactStackTrace {
// We can only get structured data out of error objects once. So we cache the information
// so we can get it again each time. It also helps performance when the same error is
// referenced more than once.
const existing = stackTraceCache.get(error);
if (existing !== undefined) {
return existing;
}
// We override Error.prepareStackTrace with our own version that collects
// the structured data. We need more information than the raw stack gives us
// and we need to ensure that we don't get the source mapped version.
collectedStackTrace = null;
framesToSkip = skipFrames;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTrace;
let stack;
try {
stack = String(error.stack);
} finally {
Error.prepareStackTrace = previousPrepare;
}
if (collectedStackTrace !== null) {
const result = collectedStackTrace;
collectedStackTrace = null;
stackTraceCache.set(error, result);
return result;
}
// If the stack has already been read, or this is not actually a V8 compatible
// engine then we might not get a normalized stack and it might still have been
// source mapped. Regardless we try our best to parse it.
const parsedFrames = parseStackTraceFromString(stack, skipFrames);
stackTraceCache.set(error, parsedFrames);
return parsedFrames;
}
export function extractLocationFromOwnerStack(
error: Error,
): ReactFunctionLocation | null {
const stackTrace = parseStackTrace(error, 0);
const stack = error.stack;
if (
!stack.includes('react_stack_bottom_frame') &&
!stack.includes('react-stack-bottom-frame')
) {
// This didn't have a bottom to it, we can't trust it.
return null;
}
// We start from the bottom since that will have the best location for the owner itself.
for (let i = stackTrace.length - 1; i >= 0; i--) {
const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
// Take the first match with a colon in the file name.
if (fileName.indexOf(':') !== -1) {
return [
functionName,
fileName,
// Use enclosing line if available, since that points to the start of the function.
encLine || line,
encCol || col,
];
}
}
return null;
}
export function extractLocationFromComponentStack(
stack: string,
): ReactFunctionLocation | null {
const stackTrace = parseStackTraceFromString(stack, 0);
for (let i = 0; i < stackTrace.length; i++) {
const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
// Take the first match with a colon in the file name.
if (fileName.indexOf(':') !== -1) {
return [
functionName,
fileName,
// Use enclosing line if available. (Never the case here because we parse from string.)
encLine || line,
encCol || col,
];
}
}
return null;
}

View File

@@ -34,12 +34,6 @@ export type IconType =
| 'save'
| 'search'
| 'settings'
| 'panel-left-close'
| 'panel-left-open'
| 'panel-right-close'
| 'panel-right-open'
| 'panel-bottom-open'
| 'panel-bottom-close'
| 'error'
| 'suspend'
| 'undo'
@@ -52,10 +46,8 @@ type Props = {
type: IconType,
};
const materialIconsViewBox = '0 -960 960 960';
export default function ButtonIcon({className = '', type}: Props): React.Node {
let pathData = null;
let viewBox = '0 0 24 24';
switch (type) {
case 'add':
pathData = PATH_ADD;
@@ -129,30 +121,6 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
case 'error':
pathData = PATH_ERROR;
break;
case 'panel-left-close':
pathData = PATH_MATERIAL_PANEL_LEFT_CLOSE;
viewBox = materialIconsViewBox;
break;
case 'panel-left-open':
pathData = PATH_MATERIAL_PANEL_LEFT_OPEN;
viewBox = materialIconsViewBox;
break;
case 'panel-right-close':
pathData = PATH_MATERIAL_PANEL_RIGHT_CLOSE;
viewBox = materialIconsViewBox;
break;
case 'panel-right-open':
pathData = PATH_MATERIAL_PANEL_RIGHT_OPEN;
viewBox = materialIconsViewBox;
break;
case 'panel-bottom-open':
pathData = PATH_MATERIAL_PANEL_BOTTOM_OPEN;
viewBox = materialIconsViewBox;
break;
case 'panel-bottom-close':
pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE;
viewBox = materialIconsViewBox;
break;
case 'suspend':
pathData = PATH_SUSPEND;
break;
@@ -179,7 +147,7 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
className={`${styles.ButtonIcon} ${className}`}
width="24"
height="24"
viewBox={viewBox}>
viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
{typeof pathData === 'string' ? (
<path fill="currentColor" d={pathData} />
@@ -308,33 +276,3 @@ const PATH_VIEW_SOURCE = `
const PATH_EDITOR = `
M7 5h10v2h2V3c0-1.1-.9-1.99-2-1.99L7 1c-1.1 0-2 .9-2 2v4h2V5zm8.41 11.59L20 12l-4.59-4.59L14 8.83 17.17 12 14 15.17l1.41 1.42zM10 15.17L6.83 12 10 8.83 8.59 7.41 4 12l4.59 4.59L10 15.17zM17 19H7v-2H5v4c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-4h-2v2z
`;
// Source: Material Design Icons left_panel_close
const PATH_MATERIAL_PANEL_LEFT_CLOSE = `
M648-324v-312L480-480l168 156ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm125-72v-528H216v528h120Zm72 0h336v-528H408v528Zm-72 0H216h120Z
`;
// Source: Material Design Icons left_panel_open
const PATH_MATERIAL_PANEL_LEFT_OPEN = `
M504-595v230q0 12.25 10.5 16.62Q525-344 534-352l110-102q11-11.18 11-26.09T644-506L534-608q-8.82-8-19.41-3.5T504-595ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm125-72v-528H216v528h120Zm72 0h336v-528H408v528Zm-72 0H216h120Z
`;
// Source: Material Design Icons right_panel_close
const PATH_MATERIAL_PANEL_RIGHT_CLOSE = `
M312-365q0 12.25 10.5 16.62Q333-344 342-352l110-102q11-11.18 11-26.09T452-506L342-608q-8.82-8-19.41-3.5T312-595v230ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm413-72h120v-528H624v528Zm-72 0v-528H216v528h336Zm72 0h120-120Z
`;
// Source: Material Design Icons right_panel_open
const PATH_MATERIAL_PANEL_RIGHT_OPEN = `
M456-365v-230q0-12.25-10.5-16.63Q435-616 426-608L316-506q-11 11.18-11 26.09T316-454l110 102q8.82 8 19.41 3.5T456-365ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm413-72h120v-528H624v528Zm-72 0v-528H216v528h336Zm72 0h120-120Z
`;
// Source: Material Design Icons bottom_panel_open
const PATH_MATERIAL_PANEL_BOTTOM_OPEN = `
M365-504h230q12.25 0 16.63-10.5Q616-525 608-534L506-644q-11.18-11-26.09-11T454-644L352-534q-8 8.82-3.5 19.41T365-504ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm5-192v120h528v-120H216Zm0-72h528v-336H216v336Zm0 72v120-120Z
`;
// Source: Material Design Icons bottom_panel_close
const PATH_MATERIAL_PANEL_BOTTOM_CLOSE = `
m506-508 102-110q8-8.82 3.5-19.41T595-648H365q-12.25 0-16.62 10.5Q344-627 352-618l102 110q11.18 11 26.09 11T506-508Zm243-308q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538ZM216-336v120h528v-120H216Zm528-72v-336H216v336h528Zm-528 72v120-120Z
`;

View File

@@ -97,11 +97,11 @@
}
.CollapsableContent {
margin-top: -0.25rem;
padding: 0.25rem 0;
}
.PreviewContainer {
padding: 0.25rem;
padding: 0 0.25rem 0.25rem 0.25rem;
}
.TimeBarContainer {
@@ -123,10 +123,3 @@
.TimeBarSpanErrored {
background-color: var(--color-timespan-background-errored);
}
.SmallHeader {
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
padding-left: 1.25rem;
margin-top: 0.25rem;
}

View File

@@ -80,13 +80,21 @@ function SuspendedByRow({
maxTime,
}: RowProps) {
const [isOpen, setIsOpen] = useState(false);
const ioInfo = asyncInfo.awaited;
const name = ioInfo.name;
const description = ioInfo.description;
const name = asyncInfo.awaited.name;
const description = asyncInfo.awaited.description;
const longName = description === '' ? name : name + ' (' + description + ')';
const shortDescription = getShortDescription(name, description);
const start = ioInfo.start;
const end = ioInfo.end;
let stack;
let owner;
if (asyncInfo.stack === null || asyncInfo.stack.length === 0) {
stack = asyncInfo.awaited.stack;
owner = asyncInfo.awaited.owner;
} else {
stack = asyncInfo.stack;
owner = asyncInfo.owner;
}
const start = asyncInfo.awaited.start;
const end = asyncInfo.awaited.end;
const timeScale = 100 / (maxTime - minTime);
let left = (start - minTime) * timeScale;
let width = (end - start) * timeScale;
@@ -98,23 +106,12 @@ function SuspendedByRow({
}
}
const ioOwner = ioInfo.owner;
const asyncOwner = asyncInfo.owner;
const showIOStack = ioInfo.stack !== null && ioInfo.stack.length !== 0;
// Only show the awaited stack if the I/O started in a different owner
// than where it was awaited. If it's started by the same component it's
// probably easy enough to infer and less noise in the common case.
const showAwaitStack =
!showIOStack ||
(ioOwner === null
? asyncOwner !== null
: asyncOwner === null || ioOwner.id !== asyncOwner.id);
const value: any = asyncInfo.awaited.value;
const isErrored =
value !== null &&
typeof value === 'object' &&
value[meta.name] === 'rejected Thenable';
const value: any = ioInfo.value;
const metaName =
value !== null && typeof value === 'object' ? value[meta.name] : null;
const isFulfilled = metaName === 'fulfilled Thenable';
const isRejected = metaName === 'rejected Thenable';
return (
<div className={styles.CollapsableRow}>
<Button
@@ -139,7 +136,7 @@ function SuspendedByRow({
<div className={styles.TimeBarContainer}>
<div
className={
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
!isErrored ? styles.TimeBarSpan : styles.TimeBarSpanErrored
}
style={{
left: left.toFixed(2) + '%',
@@ -150,39 +147,6 @@ function SuspendedByRow({
</Button>
{isOpen && (
<div className={styles.CollapsableContent}>
{showIOStack && <StackTraceView stack={ioInfo.stack} />}
{(showIOStack || !showAwaitStack) &&
ioOwner !== null &&
ioOwner.id !== inspectedElement.id ? (
<OwnerView
key={ioOwner.id}
displayName={ioOwner.displayName || 'Anonymous'}
hocDisplayNames={ioOwner.hocDisplayNames}
compiledWithForget={ioOwner.compiledWithForget}
id={ioOwner.id}
isInStore={store.containsElement(ioOwner.id)}
type={ioOwner.type}
/>
) : null}
{showAwaitStack ? (
<>
<div className={styles.SmallHeader}>awaited at:</div>
{asyncInfo.stack !== null && asyncInfo.stack.length > 0 && (
<StackTraceView stack={asyncInfo.stack} />
)}
{asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? (
<OwnerView
key={asyncOwner.id}
displayName={asyncOwner.displayName || 'Anonymous'}
hocDisplayNames={asyncOwner.hocDisplayNames}
compiledWithForget={asyncOwner.compiledWithForget}
id={asyncOwner.id}
isInStore={store.containsElement(asyncOwner.id)}
type={asyncOwner.type}
/>
) : null}
</>
) : null}
<div className={styles.PreviewContainer}>
<KeyValue
alphaSort={true}
@@ -194,27 +158,27 @@ function SuspendedByRow({
element={element}
hidden={false}
inspectedElement={inspectedElement}
name={
isFulfilled
? 'awaited value'
: isRejected
? 'rejected with'
: 'pending value'
}
path={
isFulfilled
? [index, 'awaited', 'value', 'value']
: isRejected
? [index, 'awaited', 'value', 'reason']
: [index, 'awaited', 'value']
}
name={'Promise'}
path={[index, 'awaited', 'value']}
pathRoot="suspendedBy"
store={store}
value={
isFulfilled ? value.value : isRejected ? value.reason : value
}
value={asyncInfo.awaited.value}
/>
</div>
{stack !== null && stack.length > 0 && (
<StackTraceView stack={stack} />
)}
{owner !== null && owner.id !== inspectedElement.id ? (
<OwnerView
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
) : null}
</div>
)}
</div>
@@ -228,15 +192,6 @@ type Props = {
store: Store,
};
function compareTime(a: SerializedAsyncInfo, b: SerializedAsyncInfo): number {
const ioA = a.awaited;
const ioB = b.awaited;
if (ioA.start === ioB.start) {
return ioA.end - ioB.end;
}
return ioA.start - ioB.start;
}
export default function InspectedElementSuspendedBy({
bridge,
element,
@@ -273,9 +228,6 @@ export default function InspectedElementSuspendedBy({
minTime = maxTime - 25;
}
const sortedSuspendedBy = suspendedBy.slice(0);
sortedSuspendedBy.sort(compareTime);
return (
<div>
<div className={styles.HeaderRow}>
@@ -284,7 +236,7 @@ export default function InspectedElementSuspendedBy({
<ButtonIcon type="copy" />
</Button>
</div>
{sortedSuspendedBy.map((asyncInfo, index) => (
{suspendedBy.map((asyncInfo, index) => (
<SuspendedByRow
key={index}
index={index}

View File

@@ -210,13 +210,6 @@ export default function KeyValue({
canRenameTheCurrentPath = canRenamePathsAtDepth(depth);
}
const hasChildren =
typeof value === 'object' &&
value !== null &&
(canEditValues ||
(isArray(value) && value.length > 0) ||
Object.entries(value).length > 0);
let renderedName;
if (isDirectChildOfAnArray) {
if (canDeletePaths) {
@@ -225,37 +218,27 @@ export default function KeyValue({
);
} else {
renderedName = (
<span
className={styles.Name}
onClick={isInspectable || hasChildren ? toggleIsOpen : null}>
<span className={styles.Name}>
{name}
{!!hookName && <span className={styles.HookName}>({hookName})</span>}
<span className={styles.AfterName}>:</span>
</span>
);
}
} else if (canRenameTheCurrentPath) {
renderedName = (
<>
<EditableName
allowEmpty={canDeletePaths}
className={styles.EditableName}
initialValue={name}
overrideName={renamePath}
path={path}
/>
<span className={styles.AfterName}>:</span>
</>
<EditableName
allowEmpty={canDeletePaths}
className={styles.EditableName}
initialValue={name}
overrideName={renamePath}
path={path}
/>
);
} else {
renderedName = (
<span
className={styles.Name}
data-testname="NonEditableName"
onClick={isInspectable || hasChildren ? toggleIsOpen : null}>
<span className={styles.Name} data-testname="NonEditableName">
{name}
{!!hookName && <span className={styles.HookName}>({hookName})</span>}
<span className={styles.AfterName}>:</span>
</span>
);
}
@@ -303,6 +286,7 @@ export default function KeyValue({
style={style}>
<div className={styles.ExpandCollapseToggleSpacer} />
{renderedName}
<div className={styles.AfterName}>:</div>
{canEditValues ? (
<EditableValue
overrideValue={overrideValue}
@@ -344,6 +328,7 @@ export default function KeyValue({
style={style}>
<div className={styles.ExpandCollapseToggleSpacer} />
{renderedName}
<div className={styles.AfterName}>:</div>
<span
className={styles.Link}
onClick={() => {
@@ -380,6 +365,7 @@ export default function KeyValue({
<div className={styles.ExpandCollapseToggleSpacer} />
)}
{renderedName}
<div className={styles.AfterName}>:</div>
<span
className={styles.Value}
onClick={isInspectable ? toggleIsOpen : undefined}>
@@ -402,6 +388,7 @@ export default function KeyValue({
}
} else {
if (isArray(value)) {
const hasChildren = value.length > 0 || canEditValues;
const displayName = getMetaValueLabel(value);
children = value.map((innerValue, index) => (
@@ -462,11 +449,12 @@ export default function KeyValue({
ref={contextMenuTriggerRef}
style={style}>
{hasChildren ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} />
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
{renderedName}
<div className={styles.AfterName}>:</div>
<span
className={styles.Value}
onClick={hasChildren ? toggleIsOpen : undefined}>
@@ -484,6 +472,7 @@ export default function KeyValue({
entries.sort(alphaSortEntries);
}
const hasChildren = entries.length > 0 || canEditValues;
const displayName = getMetaValueLabel(value);
children = entries.map(([key, keyValue]): ReactElement<any> => (
@@ -542,11 +531,12 @@ export default function KeyValue({
ref={contextMenuTriggerRef}
style={style}>
{hasChildren ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} />
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
{renderedName}
<div className={styles.AfterName}>:</div>
<span
className={styles.Value}
onClick={hasChildren ? toggleIsOpen : undefined}>
@@ -577,10 +567,7 @@ function DeleteToggle({deletePath, name, path}) {
title="Delete entry">
<ButtonIcon type="delete" />
</Button>
<span className={styles.Name}>
{name}
<span className={styles.AfterName}>:</span>
</span>
<span className={styles.Name}>{name}</span>
</>
);
}

View File

@@ -2,7 +2,6 @@
color: var(--color-component-name);
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -59,7 +59,7 @@ export default function OwnerView({
<span
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
title={displayName}>
{'<' + displayName + '>'}
{displayName}
</span>
<ElementBadges

View File

@@ -8,21 +8,12 @@
*/
import * as React from 'react';
import {use, useContext} from 'react';
import useOpenResource from '../useOpenResource';
import styles from './StackTraceView.css';
import type {
ReactStackTrace,
ReactCallSite,
ReactFunctionLocation,
} from 'shared/ReactTypes';
import FetchFileWithCachingContext from './FetchFileWithCachingContext';
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
import type {ReactStackTrace, ReactCallSite} from 'shared/ReactTypes';
import formatLocationForDisplay from './formatLocationForDisplay';
@@ -31,23 +22,7 @@ type CallSiteViewProps = {
};
export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
callSite;
const symbolicatedCallSite: null | ReactFunctionLocation =
fetchFileWithCaching !== null
? use(
symbolicateSourceWithCache(
fetchFileWithCaching,
virtualURL,
virtualLine,
virtualColumn,
),
)
: null;
const symbolicatedCallSite: null | ReactCallSite = null; // TODO
const [linkIsEnabled, viewSource] = useOpenResource(
callSite,
symbolicatedCallSite,
@@ -56,7 +31,7 @@ export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
symbolicatedCallSite !== null ? symbolicatedCallSite : callSite;
return (
<div className={styles.CallSite}>
{functionName || virtualFunctionName}
{functionName}
{' @ '}
<span
className={linkIsEnabled ? styles.Link : null}

View File

@@ -24,7 +24,7 @@ import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import Icon from '../Icon';
import {SettingsContext} from '../Settings/SettingsContext';
import {BridgeContext, StoreContext, OptionsContext} from '../context';
import ComponentsTreeElement from './Element';
import Element from './Element';
import InspectHostNodesToggle from './InspectHostNodesToggle';
import OwnersStack from './OwnersStack';
import ComponentSearchInput from './ComponentSearchInput';
@@ -93,47 +93,8 @@ export default function Tree(): React.Node {
const treeRef = useRef<HTMLDivElement | null>(null);
const focusTargetRef = useRef<HTMLDivElement | null>(null);
const listDOMElementRef = useRef<Element | null>(null);
const setListDOMElementRef = useCallback((listDOMElement: Element) => {
listDOMElementRef.current = listDOMElement;
// Controls the initial horizontal offset of the Tree if the element was pre-selected. For example, via Elements panel in browser DevTools.
// Initial vertical offset is controlled via initialScrollOffset prop of the FixedSizeList component.
if (
!componentsPanelVisible ||
inspectedElementIndex == null ||
listDOMElement == null
) {
return;
}
const element = store.getElementAtIndex(inspectedElementIndex);
if (element == null) {
return;
}
const viewportLeft = listDOMElement.scrollLeft;
const viewportRight = viewportLeft + listDOMElement.clientWidth;
const elementLeft = calculateElementOffset(element.depth);
// Because of virtualization, this element might not be rendered yet; we can't look up its width.
// Assuming that it may take up to the half of the viewport.
const elementRight = elementLeft + listDOMElement.clientWidth / 2;
const isElementFullyVisible =
elementLeft >= viewportLeft && elementRight <= viewportRight;
if (!isElementFullyVisible) {
const horizontalDelta =
Math.min(0, elementLeft - viewportLeft) +
Math.max(0, elementRight - viewportRight);
// $FlowExpectedError[incompatible-call] Flow doesn't support instant as an option for behavior.
listDOMElement.scrollBy({
left: horizontalDelta,
behavior: 'instant',
});
}
}, []);
const listRef = useRef(null);
const listDOMElementRef = useRef(null);
useEffect(() => {
if (!componentsPanelVisible || inspectedElementIndex == null) {
@@ -157,7 +118,7 @@ export default function Tree(): React.Node {
}
const elementLeft = calculateElementOffset(element.depth);
// Because of virtualization, this element might not be rendered yet; we can't look up its width.
// Assuming that it may take up to the half of the viewport.
// Assuming that it may take up to the half of the vieport.
const elementRight = elementLeft + listDOMElement.clientWidth / 2;
const elementTop = inspectedElementIndex * lineHeight;
const elementBottom = elementTop + lineHeight;
@@ -176,7 +137,6 @@ export default function Tree(): React.Node {
Math.min(0, elementLeft - viewportLeft) +
Math.max(0, elementRight - viewportRight);
// $FlowExpectedError[incompatible-call] Flow doesn't support instant as an option for behavior.
listDOMElement.scrollBy({
top: verticalDelta,
left: horizontalDelta,
@@ -511,10 +471,11 @@ export default function Tree(): React.Node {
itemData={itemData}
itemKey={itemKey}
itemSize={lineHeight}
outerRef={setListDOMElementRef}
ref={listRef}
outerRef={listDOMElementRef}
overscanCount={10}
width={width}>
{ComponentsTreeElement}
{Element}
</FixedSizeList>
)}
</AutoSizer>

View File

@@ -803,45 +803,6 @@ type Props = {
defaultInspectedElementIndex?: ?number,
};
function getInitialState({
defaultOwnerID,
defaultInspectedElementID,
defaultInspectedElementIndex,
store,
}: {
defaultOwnerID?: ?number,
defaultInspectedElementID?: ?number,
defaultInspectedElementIndex?: ?number,
store: Store,
}): State {
return {
// Tree
numElements: store.numElements,
ownerSubtreeLeafElementID: null,
// Search
searchIndex: null,
searchResults: [],
searchText: '',
// Owners
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
ownerFlatTree: null,
// Inspection element panel
inspectedElementID:
defaultInspectedElementID != null
? defaultInspectedElementID
: store.lastSelectedHostInstanceElementId,
inspectedElementIndex:
defaultInspectedElementIndex != null
? defaultInspectedElementIndex
: store.lastSelectedHostInstanceElementId
? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
: null,
};
}
// TODO Remove TreeContextController wrapper element once global Context.write API exists.
function TreeContextController({
children,
@@ -905,16 +866,32 @@ function TreeContextController({
[store],
);
const [state, dispatch] = useReducer(
reducer,
{
defaultOwnerID,
defaultInspectedElementID,
defaultInspectedElementIndex,
store,
},
getInitialState,
);
const [state, dispatch] = useReducer(reducer, {
// Tree
numElements: store.numElements,
ownerSubtreeLeafElementID: null,
// Search
searchIndex: null,
searchResults: [],
searchText: '',
// Owners
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
ownerFlatTree: null,
// Inspection element panel
inspectedElementID:
defaultInspectedElementID != null
? defaultInspectedElementID
: store.lastSelectedHostInstanceElementId,
inspectedElementIndex:
defaultInspectedElementIndex != null
? defaultInspectedElementIndex
: store.lastSelectedHostInstanceElementId
? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
: null,
});
const transitionDispatch = useMemo(
() => (action: Action) =>
startTransition(() => {

View File

@@ -1,117 +0,0 @@
.SuspenseTab {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
background-color: var(--color-background);
color: var(--color-text);
font-family: var(--font-family-sans);
}
.SuspenseTab, .SuspenseTab * {
box-sizing: border-box;
-webkit-font-smoothing: var(--font-smoothing);
}
.TreeWrapper {
flex: 1 1 var(--horizontal-resize-tree-percentage);
display: flex;
flex-direction: row;
overflow: auto;
border-top: 1px solid var(--color-border);
}
.InspectedElementWrapper {
flex: 1 1 calc(100% - var(--horizontal-resize-tree-percentage));
overflow-x: hidden;
overflow-y: auto;
}
.ResizeBarWrapper {
flex: 0 0 0px;
position: relative;
}
.ResizeBar {
position: absolute;
/*
* moving the bar out of its bounding box might cause its hitbox to overlap
* with another scrollbar creating disorienting UX where you both resize and scroll
* at the same time.
* If you adjust this value, double check that starting resize right on this edge
* doesn't also cause scroll
*/
left: 1px;
width: 5px;
height: 100%;
cursor: ew-resize;
}
.TreeView footer {
display: none;
}
@container devtools (width < 600px) {
.SuspenseTab {
flex-direction: column;
}
.TreeWrapper {
border-top: 1px solid var(--color-border);
flex: 1 0 var(--vertical-resize-tree-percentage);
}
.InspectedElementWrapper {
flex: 1 1 50%;
}
.TreeWrapper + .ResizeBarWrapper .ResizeBar {
top: 1px;
left: 0;
width: 100%;
height: 5px;
cursor: ns-resize;
}
.TreeView footer {
display: flex;
justify-content: end;
}
.ToggleInspectedElement[data-orientation="horizontal"] {
display: none;
}
}
.TreeList {
flex: 0 0 var(--horizontal-resize-tree-list-percentage);
border-right: 1px solid var(--color-border);
padding: 0.25rem
}
.TreeView {
flex: 1 1 35%;
display: flex;
flex-direction: column;
}
.Rects {
border-top: 1px solid var(--color-border);
padding: 0.25rem;
flex-grow: 1;
}
.TimelineWrapper {
padding: 0.25rem;
display: flex;
flex-direction: row;
}
.Timeline {
flex-grow: 1;
}

Some files were not shown because too many files have changed in this diff Show More