Flatten built-in utility types in the API snapshot (#52280)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52280

Changelog: [Internal]

Adds a type-simplifyng transform for the API snapshot, with the goal of resolving some built-in TS types during build time. Most notably, it's able to simplify `Omit` structures emitted by the `flow-api-translator` when translating Flow's type spread operator.

It builds upon a simplified type inlining transform from the previous approach. The type inlining transform is able to handle inlining type references and resolution of built-in TS types on literal types:
- `Omit`
- `Readonly`
- `Partial`
- `keyof`

Reference inlining is performed top-down and built-in type resolution is performed bottom-up, which makes it possible for the second step to assume working on type literals.

Type simplifying transform uses the type inlining to reduce type references encountered inside `Omits` to their literal shapes, which makes possible to determine whether `Omit` is neccessary case-by-case. If `Omit` is redundant, it can be safely removed. If it's not, the omitted keys can be reduced to represent a subset of keys existing in the target type.

It also keeps the ability to resolve `Partial` and `Readonly` types on type literals, simplifying the snapshot further.

An example diff the transform can handle:
Before:
```
export declare type AccessibilityProps = Readonly<
  Omit<
    AccessibilityPropsAndroid,
    | keyof {
        accessibilityActions?: ReadonlyArray<AccessibilityActionInfo>
        accessibilityHint?: string
        accessibilityLabel?: string
        accessibilityRole?: AccessibilityRole
        accessibilityState?: AccessibilityState
        accessibilityValue?: AccessibilityValue
        accessible?: boolean
        "aria-busy"?: boolean
        "aria-checked"?: "mixed" | (boolean | undefined)
        "aria-disabled"?: boolean
        "aria-expanded"?: boolean
        "aria-hidden"?: boolean
        "aria-label"?: string
        "aria-selected"?: boolean
        "aria-valuemax"?: AccessibilityValue["max"]
        "aria-valuemin"?: AccessibilityValue["min"]
        "aria-valuenow"?: AccessibilityValue["now"]
        "aria-valuetext"?: AccessibilityValue["text"]
        role?: Role
      }
    | keyof AccessibilityPropsIOS
  > &
    Omit<
      AccessibilityPropsIOS,
      keyof {
        accessibilityActions?: ReadonlyArray<AccessibilityActionInfo>
        accessibilityHint?: string
        accessibilityLabel?: string
        accessibilityRole?: AccessibilityRole
        accessibilityState?: AccessibilityState
        accessibilityValue?: AccessibilityValue
        accessible?: boolean
        "aria-busy"?: boolean
        "aria-checked"?: "mixed" | (boolean | undefined)
        "aria-disabled"?: boolean
        "aria-expanded"?: boolean
        "aria-hidden"?: boolean
        "aria-label"?: string
        "aria-selected"?: boolean
        "aria-valuemax"?: AccessibilityValue["max"]
        "aria-valuemin"?: AccessibilityValue["min"]
        "aria-valuenow"?: AccessibilityValue["now"]
        "aria-valuetext"?: AccessibilityValue["text"]
        role?: Role
      }
    > & {
      accessibilityActions?: ReadonlyArray<AccessibilityActionInfo>
      accessibilityHint?: string
      accessibilityLabel?: string
      accessibilityRole?: AccessibilityRole
      accessibilityState?: AccessibilityState
      accessibilityValue?: AccessibilityValue
      accessible?: boolean
      "aria-busy"?: boolean
      "aria-checked"?: "mixed" | (boolean | undefined)
      "aria-disabled"?: boolean
      "aria-expanded"?: boolean
      "aria-hidden"?: boolean
      "aria-label"?: string
      "aria-selected"?: boolean
      "aria-valuemax"?: AccessibilityValue["max"]
      "aria-valuemin"?: AccessibilityValue["min"]
      "aria-valuenow"?: AccessibilityValue["now"]
      "aria-valuetext"?: AccessibilityValue["text"]
      role?: Role
    }
>
```

After:
```
export declare type AccessibilityProps = Readonly<
  AccessibilityPropsAndroid &
    AccessibilityPropsIOS & {
      accessibilityActions?: ReadonlyArray<AccessibilityActionInfo>
      accessibilityHint?: string
      accessibilityLabel?: string
      accessibilityRole?: AccessibilityRole
      accessibilityState?: AccessibilityState
      accessibilityValue?: AccessibilityValue
      accessible?: boolean
      "aria-busy"?: boolean
      "aria-checked"?: "mixed" | (boolean | undefined)
      "aria-disabled"?: boolean
      "aria-expanded"?: boolean
      "aria-hidden"?: boolean
      "aria-label"?: string
      "aria-selected"?: boolean
      "aria-valuemax"?: AccessibilityValue["max"]
      "aria-valuemin"?: AccessibilityValue["min"]
      "aria-valuenow"?: AccessibilityValue["now"]
      "aria-valuetext"?: AccessibilityValue["text"]
      role?: Role
    }
>
```

Reviewed By: huntie

Differential Revision: D77295302

fbshipit-source-id: 213aef46035bde4f9783353b5344a6986a418399
This commit is contained in:
Jakub Piasecki
2025-06-26 03:05:27 -07:00
committed by Facebook GitHub Bot
parent c6608685cb
commit df5cd55cdb
15 changed files with 1977 additions and 0 deletions
+1
View File
@@ -36,6 +36,7 @@ const inputFilesPostTransforms: $ReadOnlyArray<PluginObj<mixed>> = [
];
const postTransforms: $ReadOnlyArray<PluginObj<mixed>> = [
require('./transforms/typescript/simplifyTypes'),
require('./transforms/typescript/sortProperties'),
require('./transforms/typescript/sortUnions'),
require('./transforms/typescript/removeUndefinedFromOptionalMembers'),
@@ -0,0 +1,416 @@
/**
* 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 strict-local
* @format
*/
const babel = require('@babel/core');
const postTransforms = [require('../simplifyTypes/inlineTypesVisitor')];
async function applyPostTransforms(inSrc: string): Promise<string> {
const result = await babel.transformAsync(inSrc, {
plugins: ['@babel/plugin-syntax-typescript', ...postTransforms],
});
return result.code;
}
describe('inlineTypesVisitor', () => {
test('should inline type non-generic type aliases', async () => {
const code = `
type AnimatedNodeConfig = {
readonly debugID?: string | undefined;
};
export type Example = Omit<
AnimatedNodeConfig,
keyof {
useNativeDriver: boolean;
}
> & {
useNativeDriver: boolean;
};
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type AnimatedNodeConfig = {
readonly debugID?: string | undefined;
};
export type Example = {
readonly debugID?: string | undefined;
useNativeDriver: boolean;
};"
`);
});
test('should skip recursive definitions', async () => {
const code = `
export type LinkedNode = {
value: number;
next: LinkedNode;
};
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"export type LinkedNode = {
value: number;
next: LinkedNode;
};"
`);
});
test('should skip co-recursive definitions', async () => {
const code = `
type Expr = Add | Multiply | number;
export type Add = {
type: 'add';
lhs: Expr;
rhs: Expr;
};
export type Multiply = {
type: 'multiply';
lhs: Expr;
rhs: Expr;
};
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Expr = Add | Multiply | number;
export type Add = {
type: 'add';
lhs: Add | Multiply | number;
rhs: Expr;
};
export type Multiply = {
type: 'multiply';
lhs: Expr;
rhs: Expr;
};"
`);
});
test('keyof {} is never', async () => {
const code = `
export type NeverInDisguise = keyof {};
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(
`"export type NeverInDisguise = never;"`,
);
});
test('keyof for objects with computed string properties', async () => {
const code = `
export type Foo = keyof { 'a-key': number, 'b-key': number };
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(
`"export type Foo = \\"a-key\\" | \\"b-key\\";"`,
);
});
test('resolves Readonly type', async () => {
const code = `
export type Foo = Readonly<{ a?: number, 'b-key': number }>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"export type Foo = {
readonly a?: number;
readonly 'b-key': number;
};"
`);
});
test('resolves Partial type', async () => {
const code = `
export type Foo = Partial<{ a: number, 'b-key': number }>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"export type Foo = {
a?: number;
'b-key'?: number;
};"
`);
});
test('resolves simple intersection', async () => {
const code = `
export type Foo = { a?: number, 'b-key': number } & { c: number, 'd-key'?: number };
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"export type Foo = {
a?: number;
\\"b-key\\": number;
c: number;
\\"d-key\\"?: number;
};"
`);
});
test('can inline multiple references to a single type in a declaration', async () => {
const code = `
export declare type Result =
& Omit<{ alpha: 1; }, keyof Beta>
& Omit<Beta, "gamma">
;
declare type Beta = Readonly<{
beta: 2;
}>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"export declare type Result = {
alpha: 1;
readonly beta: 2;
};
declare type Beta = {
readonly beta: 2;
};"
`);
});
test('should preserve dec comments when inlining types', async () => {
const code = `
declare type A = {
/**
* Comment for prop1 in A
*/
prop1: string;
/**
* Comment for prop2 in A
*/
prop2: number;
};
declare type B = {
/**
* Comment for prop1 in B
*/
prop1: string[];
/**
* Comment for prop3 in B
*/
prop3: boolean;
};
declare type C = {
/**
* Comment for prop4 in D
*/
prop4: number;
};
export declare type D = Omit<Omit<A, keyof B> & B, keyof C> & C;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"declare type A = {
/**
* Comment for prop1 in A
*/
prop1: string;
/**
* Comment for prop2 in A
*/
prop2: number;
};
declare type B = {
/**
* Comment for prop1 in B
*/
prop1: string[];
/**
* Comment for prop3 in B
*/
prop3: boolean;
};
declare type C = {
/**
* Comment for prop4 in D
*/
prop4: number;
};
export declare type D = {
/**
* Comment for prop2 in A
*/
prop2: number;
/**
* Comment for prop1 in B
*/
prop1: string[];
/**
* Comment for prop3 in B
*/
prop3: boolean;
/**
* Comment for prop4 in D
*/
prop4: number;
};"
`);
});
test('should inline generic type aliases', async () => {
const code = `
type Required<T, U = string> = {
data: T[];
} & {
title: U;
}
type Optional<T> = {
header: T;
}
export type Example<DataT> = Omit<
Required<DataT>,
keyof Optional<DataT>
> & Optional<DataT>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Required<T, U = string> = {
data: T[];
title: U;
};
type Optional<T> = {
header: T;
};
export type Example<DataT> = {
data: DataT[];
title: string;
header: DataT;
};"
`);
});
test('should inline generic type equal to one of its type parameters', async () => {
const code = `
type PropsAlias<Props extends {}> = Props;
export type Example = PropsAlias<{a: string}>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type PropsAlias<Props extends {}> = Props;
export type Example = {
a: string;
};"
`);
});
test('should not confuse local symbols with global ones', async () => {
const code = `
type PropsAlias<Props extends {}> = Props;
type Props = {
b: number;
};
export type Example = PropsAlias<{a: string}>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type PropsAlias<Props extends {}> = Props;
type Props = {
b: number;
};
export type Example = {
a: string;
};"
`);
});
test('should inline types inside Omit when used in union', async () => {
const code = `
declare type ProgressBarAndroidBaseProps = {
readonly animating?: boolean | undefined;
readonly color?: ColorValue | undefined;
readonly testID?: string | undefined;
};
declare type DeterminateProgressBarAndroidStyleAttrProp = {
styleAttr: "Horizontal";
indeterminate: false;
progress: number;
};
declare type IndeterminateProgressBarAndroidStyleAttrProp = {
styleAttr: "Normal"
indeterminate: true;
};
export declare type ProgressBarAndroidProps =
| Readonly<
Omit<
ProgressBarAndroidBaseProps,
"styleAttr" | "indeterminate" | "progress"
> &
Omit<DeterminateProgressBarAndroidStyleAttrProp, never> & {}
>
| Readonly<
Omit<
ProgressBarAndroidBaseProps,
"styleAttr" | "indeterminate"
> &
Omit<IndeterminateProgressBarAndroidStyleAttrProp, never> & {}
>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"declare type ProgressBarAndroidBaseProps = {
readonly animating?: boolean | undefined;
readonly color?: ColorValue | undefined;
readonly testID?: string | undefined;
};
declare type DeterminateProgressBarAndroidStyleAttrProp = {
styleAttr: \\"Horizontal\\";
indeterminate: false;
progress: number;
};
declare type IndeterminateProgressBarAndroidStyleAttrProp = {
styleAttr: \\"Normal\\";
indeterminate: true;
};
export declare type ProgressBarAndroidProps = {
readonly animating?: boolean | undefined;
readonly color?: ColorValue | undefined;
readonly testID?: string | undefined;
readonly styleAttr: \\"Horizontal\\";
readonly indeterminate: false;
readonly progress: number;
} | {
readonly animating?: boolean | undefined;
readonly color?: ColorValue | undefined;
readonly testID?: string | undefined;
readonly styleAttr: \\"Normal\\";
readonly indeterminate: true;
};"
`);
});
});
@@ -0,0 +1,261 @@
/**
* 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 strict-local
* @format
*/
const babel = require('@babel/core');
const postTransforms = [require('../simplifyTypes')];
async function applyPostTransforms(inSrc: string): Promise<string> {
const result = await babel.transformAsync(inSrc, {
plugins: ['@babel/plugin-syntax-typescript', ...postTransforms],
});
return result.code;
}
describe('simplifyTypes', () => {
test('should resolve Readonly on a type literal', async () => {
const code = `
type Baz = Readonly<{
foo: string;
}>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Baz = {
readonly foo: string;
};"
`);
});
test('should keep Readonly on a type reference', async () => {
const code = `
type Foo = {
foo: string;
};
type Baz = Readonly<Foo>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Foo = {
foo: string;
};
type Baz = Readonly<Foo>;"
`);
});
test('should resolve Partial on a type literal', async () => {
const code = `
type Baz = Partial<{
foo: string;
}>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Baz = {
foo?: string;
};"
`);
});
test('should keep Partial on a type reference', async () => {
const code = `
type Foo = {
foo: string;
};
type Baz = Partial<Foo>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Foo = {
foo: string;
};
type Baz = Partial<Foo>;"
`);
});
test('should resolve nested utility types on a type literal', async () => {
const code = `
type Baz = Readonly<Partial<{
foo: string;
}>>;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Baz = {
readonly foo?: string;
};"
`);
});
test('should resolve intersection on type literals', async () => {
const code = `
type Baz = {
foo: string;
} & {
bar: number;
};
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Baz = {
foo: string;
bar: number;
};"
`);
});
test('should resolve intersection with an empty type', async () => {
const code = `
type Foo = {
foo: string;
};
type Baz = Foo & {};
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Foo = {
foo: string;
};
type Baz = Foo;"
`);
});
test('should resolve a simple Omit', async () => {
const code = `
type Foo = {
foo: string;
};
type Baz = (arg: Omit<Foo, 'bar'>) => void;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Foo = {
foo: string;
};
type Baz = (arg: Foo) => void;"
`);
});
test('should resolve Omit with keyof', async () => {
const code = `
type Foo = {
foo: string;
};
type Bar = {
bar: number;
};
type Baz = (arg: Omit<Foo, keyof Bar>) => void;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Foo = {
foo: string;
};
type Bar = {
bar: number;
};
type Baz = (arg: Foo) => void;"
`);
});
test('should resolve Omit with keyof {}', async () => {
const code = `
type Foo = {
foo: string;
};
type baz = Omit<Foo, keyof {}>
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Foo = {
foo: string;
};
type baz = Foo;"
`);
});
test('should resolve Omit with keyof when types overlap', async () => {
const code = `
type Foo = {
foo: string;
bar: number;
};
type Baz = (arg: Omit<Foo, 'bar' | 'baz'>) => void;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Foo = {
foo: string;
bar: number;
};
type Baz = (arg: Omit<Foo, \\"bar\\">) => void;"
`);
});
test('should resolve Omit when types overlap', async () => {
const code = `
type Foo = {
foo: string;
bar: number;
};
type Bar = {
bar: number;
baz: string;
};
type Baz = (arg: Omit<Foo, keyof Bar>) => void;
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(`
"type Foo = {
foo: string;
bar: number;
};
type Bar = {
bar: number;
baz: string;
};
type Baz = (arg: Omit<Foo, \\"bar\\">) => void;"
`);
});
test('should resolve Omit keys when the object type is unresolvable', async () => {
const code = `
type Foo<T> = Omit<Bar, keyof { touchHistory: string }>
`;
const result = await applyPostTransforms(code);
expect(result).toMatchInlineSnapshot(
`"type Foo<T> = Omit<Bar, \\"touchHistory\\">;"`,
);
});
});
@@ -0,0 +1,91 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {NodePath} from '@babel/traverse';
import traverse from '@babel/traverse';
const t = require('@babel/types');
export default function alignTypeParameters(
node: BabelNodeTSType,
declarationPath: NodePath<t.TSTypeAliasDeclaration>,
path: NodePath<t.Node>,
): BabelNodeTSType {
const declarationTypeParameters = declarationPath.node.typeParameters?.params;
const nodeTypeParameters = path.node.typeParameters?.params;
if (!declarationTypeParameters || !nodeTypeParameters) {
throw new Error(
`No type parameters found for ${declarationPath.node.id.name ?? ''}`,
);
}
if (nodeTypeParameters.length > declarationTypeParameters.length) {
throw new Error(
`Encountered ${nodeTypeParameters.length} type parameters for type ${declarationPath.node.id.name ?? ''} while at maximum ${declarationTypeParameters.length} are allowed`,
);
}
const genericMapping = new Map<string, BabelNodeTSType>();
for (let i = 0; i < declarationTypeParameters.length; i++) {
const declarationTypeParameter = declarationTypeParameters[i];
let nodeTypeParameter: ?BabelNodeTSType = nodeTypeParameters[
i
] as $FlowFixMe;
if (nodeTypeParameter == null) {
nodeTypeParameter = declarationTypeParameter.default;
}
if (nodeTypeParameter == null) {
throw new Error(
`No value provided for a required type parameter ${declarationPath.node.id.name ?? ''}`,
);
}
genericMapping.set(declarationTypeParameter.name, nodeTypeParameter);
}
// handle edge case where the generic type is equal to one of the type
// parameters, i.e. `type Foo<T> = T`
if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
const mappedType = genericMapping.get(node.typeName.name);
if (mappedType) {
return mappedType;
}
}
const wrapped = t.tsTypeAliasDeclaration(
t.identifier('Wrapper'),
undefined,
node,
);
traverse(t.file(t.program([wrapped])), {
TSTypeReference(innerPath) {
const type = innerPath.node.typeName;
if (!t.isIdentifier(type)) {
return;
}
const mappedType = t.cloneDeep(genericMapping.get(type.name));
if (!mappedType) {
return;
}
innerPath.replaceWith(mappedType);
innerPath.skip();
},
});
return node;
}
@@ -0,0 +1,136 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {InlineVisitorState} from './visitorState';
type KeyofLayer = {
type: 'keyof',
};
type OmitLayer = {
type: 'omit',
};
type UnionLayer = {
type: 'union',
};
type ExtendsLayer = {
type: 'extends',
};
type ArrayLayer = {
type: 'array',
};
type TypeAliasLayer = {
type: 'typeAlias',
typeParams?: string[],
};
type TypeParameterInstantiationLayer = {
type: 'typeParameterInstantiation',
};
type UnresolvableTypeLayer = {
type: 'unresolvableType',
};
type ResolvableTypeLayer = {
type: 'resolvableType',
};
export type StackLayer =
| KeyofLayer
| OmitLayer
| UnionLayer
| ExtendsLayer
| ArrayLayer
| TypeAliasLayer
| TypeParameterInstantiationLayer
| UnresolvableTypeLayer
| ResolvableTypeLayer;
export function pushLayer(state: InlineVisitorState, layer: StackLayer) {
state.stack.push(layer);
}
export function popLayer(state: InlineVisitorState, type: StackLayer['type']) {
const top = state.stack[state.stack.length - 1];
if (!top || top.type !== type) {
throw new Error(
`Unexpected stack state. Expected ${type}, got ${top.type}`,
);
}
state.stack.pop();
}
export function isDefiningType(
state: InlineVisitorState,
alias: string,
): boolean {
return state.parentTypeAliases?.has(alias) ?? false;
}
export function insideKeyofLayer(state: InlineVisitorState): boolean {
return state.stack.some(layer => layer.type === 'keyof');
}
export function insideOmitLayer(state: InlineVisitorState): boolean {
return state.stack.some(layer => layer.type === 'omit');
}
export function insideUnionLayer(state: InlineVisitorState): boolean {
return state.stack.some(layer => layer.type === 'union');
}
export function insideExtendsLayer(state: InlineVisitorState): boolean {
return state.stack.some(layer => layer.type === 'extends');
}
export function insideArrayLayer(state: InlineVisitorState): boolean {
return state.stack.some(layer => layer.type === 'array');
}
export function insideTypeAliasLayerWithTypeParam(
state: InlineVisitorState,
parameter: string,
): boolean {
return state.stack.some(
layer =>
layer.type === 'typeAlias' &&
layer.typeParams?.includes(parameter) === true,
);
}
export function insideIndexedAccessLayer(state: InlineVisitorState): boolean {
return state.stack.some(layer => layer.type === 'indexedAccess');
}
export function insideUnresolvableTypeInstantiation(
state: InlineVisitorState,
): boolean {
const lastUnresolvableTypeIdx = state.stack.findLastIndex(
layer => layer.type === 'unresolvableType',
);
const lastResolvableTypeIdx = state.stack.findLastIndex(
layer => layer.type === 'resolvableType',
);
const lastTypeParameterInstantiationIdx = state.stack.findLastIndex(
layer => layer.type === 'typeParameterInstantiation',
);
return (
lastUnresolvableTypeIdx >= 0 &&
lastTypeParameterInstantiationIdx >= 0 &&
lastResolvableTypeIdx < lastUnresolvableTypeIdx
);
}
@@ -0,0 +1,30 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {NodePath, Visitor} from '@babel/traverse';
const t = require('@babel/types');
export type GatherTypeAliasesVisitorState = {
+aliasToPathMap: Map<string, NodePath<t.TSTypeAliasDeclaration>>,
};
/**
* Gather all type aliases in the file into a map
*/
const gatherTypeAliasesVisitor: Visitor<GatherTypeAliasesVisitorState> = {
TSTypeAliasDeclaration(path, state) {
const alias = path.node.id.name;
state.aliasToPathMap.set(alias, path);
},
};
export default gatherTypeAliasesVisitor;
@@ -0,0 +1,68 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {BaseVisitorState} from './visitorState';
import type {PluginObj} from '@babel/core';
import type {NodePath} from '@babel/traverse';
import gatherTypeAliasesVisitor from './gatherTypeAliasesVisitor';
import {canResolveBuiltinType, resolveBuiltinType} from './resolveBuiltinType';
import {resolveIntersection} from './resolveIntersection';
import {resolveTSType} from './resolveTSType';
const t = require('@babel/types');
const mergeObjects: PluginObj<BaseVisitorState> = {
visitor: {
Program: {
enter(path, state): void {
state.aliasToPathMap = new Map<
string,
NodePath<t.TSTypeAliasDeclaration>,
>();
state.nodeToAliasMap = new Map<t.Node, string>();
state.parentTypeAliases = new Set<string>();
path.traverse(gatherTypeAliasesVisitor, {
aliasToPathMap: state.aliasToPathMap,
});
},
exit(path, state): void {},
},
TSIntersectionType: {
enter() {
// Do nothing
},
exit(path, state) {
resolveIntersection(path, state);
},
},
TSTypeReference: {
enter(path, state): void {},
// Builtin-type resolution is done bottom-up
exit(path, state) {
if (!t.isIdentifier(path.node.typeName)) {
return;
}
const typeName = path.node.typeName.name;
if (canResolveBuiltinType(typeName)) {
resolveBuiltinType(path, state, resolveTSType);
return;
}
},
},
},
};
// Visitor state is only used internally, so we can safely cast to PluginObj<mixed>.
module.exports = mergeObjects as $FlowFixMe as PluginObj<mixed>;
@@ -0,0 +1,43 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {InlineVisitorState} from './visitorState';
import type {NodePath} from '@babel/traverse';
import alignTypeParameters from './alignTypeParameters';
const t = require('@babel/types');
const debug = require('debug')('build-types:transforms:inlineTypes');
export default function inlineType(
state: InlineVisitorState,
path: NodePath<t.Node>,
alias: string,
): void {
const declarationPath = state.aliasToPathMap?.get(alias);
if (!declarationPath) {
debug(`No declaration found for ${alias ?? ''}`);
return;
}
let cloned = t.cloneDeep(declarationPath.node.typeAnnotation);
// If the inlined type is generic, align the provided type parameters
// to the declaration
if (declarationPath.node.typeParameters) {
cloned = alignTypeParameters(cloned, declarationPath, path);
}
state.parentTypeAliases?.add(alias);
state.nodeToAliasMap?.set(cloned, alias);
path.replaceWith(cloned);
}
@@ -0,0 +1,256 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {InlineVisitorState} from './visitorState';
import type {PluginObj} from '@babel/core';
import type {NodePath} from '@babel/traverse';
import {
insideTypeAliasLayerWithTypeParam,
isDefiningType,
popLayer,
pushLayer,
} from './contextStack';
import gatherTypeAliasesVisitor from './gatherTypeAliasesVisitor';
import inlineType from './inlineType';
import {canResolveBuiltinType, resolveBuiltinType} from './resolveBuiltinType';
import {resolveIntersection} from './resolveIntersection';
import {resolveTypeOperator} from './resolveTypeOperator';
import {onNodeExit, shouldSkipInliningType} from './utils';
const t = require('@babel/types');
const inlineTypes: PluginObj<InlineVisitorState> = {
visitor: {
Program: {
enter(path, state): void {
if (!state.aliasToPathMap) {
state.aliasToPathMap = new Map<
string,
NodePath<t.TSTypeAliasDeclaration>,
>();
path.traverse(gatherTypeAliasesVisitor, {
aliasToPathMap: state.aliasToPathMap,
});
}
state.parentTypeAliases = new Set<string>();
state.nodeToAliasMap = new Map<t.Node, string>();
state.stack = [];
},
exit(path, state): void {
// Do nothing
},
},
TSTypeLiteral: {
enter() {
// Do nothing
},
exit(path, state) {
onNodeExit(state, path.node);
},
},
TSFunctionType: {
enter() {
// Do nothing
},
exit(path, state) {
onNodeExit(state, path.node);
},
},
TSTypeOperator: {
enter(path, state) {
if (path.node.operator === 'keyof') {
pushLayer(state, {type: 'keyof'});
}
},
exit(path, state) {
if (path.node.operator === 'keyof') {
popLayer(state, 'keyof');
}
resolveTypeOperator(path, state);
},
},
TSTypeParameter: {
enter(path, state) {
if (path.node.constraint) {
pushLayer(state, {type: 'extends'});
}
},
exit(path, state) {
if (path.node.constraint) {
popLayer(state, 'extends');
}
},
},
TSTypeParameterInstantiation: {
enter(path, state) {
pushLayer(state, {type: 'typeParameterInstantiation'});
},
exit(path, state) {
popLayer(state, 'typeParameterInstantiation');
},
},
TSTypeAliasDeclaration: {
enter(path, state): void {
state.parentTypeAliases?.add(path.node.id.name);
pushLayer(state, {
type: 'typeAlias',
typeParams: path.node.typeParameters?.params?.map(
param => param.name,
),
});
},
exit(path, state): void {
state.parentTypeAliases?.delete(path.node.id.name);
popLayer(state, 'typeAlias');
},
},
TSUnionType: {
enter(path, state): void {
pushLayer(state, {type: 'union'});
},
exit(path, state): void {
popLayer(state, 'union');
},
},
TSIntersectionType: {
enter() {
// Do nothing
},
exit(path, state) {
resolveIntersection(path, state);
},
},
// Classes are a very special case, because they can extend a super class,
// and that super class can be generic. We need to track that superclass
// either as a resolvable or unresolvable type layer.
ClassDeclaration: {
enter(path, state): void {
if (!path.node.superClass || !path.node.superTypeParameters) {
// No generic superclass, don't care
return;
}
if (
t.isIdentifier(path.node.superClass) &&
canResolveBuiltinType(path.node.superClass.name)
) {
pushLayer(state, {type: 'resolvableType'});
} else {
pushLayer(state, {type: 'unresolvableType'});
}
},
exit(path, state): void {
if (!path.node.superClass || !path.node.superTypeParameters) {
// No generic superclass, don't care
return;
}
if (
t.isIdentifier(path.node.superClass) &&
canResolveBuiltinType(path.node.superClass.name)
) {
popLayer(state, 'resolvableType');
} else {
popLayer(state, 'unresolvableType');
}
},
},
TSTypeReference: {
// Reference inlining is done top-down
enter(path, state): void {
if (
t.isTSQualifiedName(path.node.typeName) &&
!!path.node.typeParameters
) {
// A generic qualified name, we need to push an unresolvable layer
pushLayer(state, {type: 'unresolvableType'});
return;
}
if (path.node.typeName.type !== 'Identifier') {
return;
}
const typeName = path.node.typeName.name;
if (isDefiningType(state, typeName)) {
// Skipping recursive type
path.skip();
return;
}
if (typeName === 'Array' || typeName === 'ReadonlyArray') {
pushLayer(state, {type: 'array'});
return;
}
if (canResolveBuiltinType(typeName)) {
pushLayer(state, {type: 'resolvableType'});
if (typeName === 'Omit') {
pushLayer(state, {type: 'omit'});
}
// Builtin type, do nothing. Will be resolved on exit.
return;
}
if (insideTypeAliasLayerWithTypeParam(state, typeName)) {
return;
}
if (shouldSkipInliningType(state, typeName)) {
return;
}
inlineType(state, path, typeName);
},
// Builtin-type resolution is done bottom-up
exit(path, state) {
if (
t.isTSQualifiedName(path.node.typeName) &&
!!path.node.typeParameters
) {
// A generic qualified name, we need to pop the unresolvable layer
popLayer(state, 'unresolvableType');
return;
}
if (path.node.typeName.type !== 'Identifier') {
return;
}
const typeName = path.node.typeName.name;
if (typeName === 'Array' || typeName === 'ReadonlyArray') {
popLayer(state, 'array');
return;
}
if (canResolveBuiltinType(typeName)) {
if (typeName === 'Omit') {
popLayer(state, 'omit');
}
popLayer(state, 'resolvableType');
resolveBuiltinType(path, state);
return;
}
state.parentTypeAliases?.delete(typeName);
},
},
},
};
// Visitor state is only used internally, so we can safely cast to PluginObj<mixed>.
module.exports = inlineTypes as $FlowFixMe as PluginObj<mixed>;
@@ -0,0 +1,249 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {TSTypeResolver} from './resolveTSType';
import type {BaseVisitorState} from './visitorState';
import type {NodePath} from '@babel/traverse';
import {replaceWithCleanup} from './utils';
const t = require('@babel/types');
const debug = require('debug')('build-types:transforms:inlineTypes');
// TODO: Handle more builtin TS types
const builtinTypeResolvers: {
+[K: string]: (
path: NodePath<t.TSTypeReference>,
state: BaseVisitorState,
tsTypeResolver?: TSTypeResolver,
) => void,
} = {
Omit: (path, state, tsTypeResolver) => {
if (
!path.node.typeParameters ||
path.node.typeParameters.params.length !== 2
) {
throw new Error(
`Omit type must have exactly 2 type parameters. Got ${path.node.typeParameters?.params.length ?? 0}`,
);
}
const [objectType, keys] = path.node.typeParameters.params;
// Try to resolve the second type parameter to a literal or union of literals
const resolvedKeys = tsTypeResolver?.(keys, state.aliasToPathMap) ?? keys;
if (t.isTSNeverKeyword(resolvedKeys)) {
// never is just an empty union, so we can just replace the Omit with
// the object type
replaceWithCleanup(state, path, objectType);
path.skip(); // We don't want to traverse the new node
return;
}
const stringLiteralElements = (() => {
if (t.isTSLiteralType(keys) && t.isStringLiteral(keys.literal)) {
return [keys.literal.value];
}
if (t.isTSUnionType(resolvedKeys)) {
const unionElements = resolvedKeys.types;
return unionElements
.map(element =>
t.isTSLiteralType(element) && t.isStringLiteral(element.literal)
? element.literal
: null,
)
.filter(literal => literal !== null)
.map(literal => literal.value);
}
debug(`Unsupported Omit second parameter: ${keys.type}`);
return 'skip' as const;
})();
if (stringLiteralElements === 'skip') {
return;
}
// At this point, we know that the keys are a union of string literals
if (!t.isTSTypeLiteral(objectType)) {
// The parameter is not a literal, we can try to resolve it to a literal
const constBody = tsTypeResolver?.(objectType, state.aliasToPathMap);
if (!constBody || !t.isTSTypeLiteral(constBody)) {
// Resolving the parameter failed, so we cannot do anything but update
// the second type parameter to the resolved keys.
if (path.node.typeParameters) {
path.node.typeParameters.params[1] = resolvedKeys;
}
return;
}
// Resolving the parameter succeeded, so we can compare the keys to the
// members of the object type
const members = constBody.members
.map(member => {
if (t.isIdentifier(member.key)) {
return member.key.name;
}
if (t.isLiteral(member.key)) {
return member.key.value;
}
})
.filter(member => member);
const overlappingKeys = stringLiteralElements.filter(key =>
members.includes(key),
);
// If there are no overlapping keys, we can just replace the Omit with
// the object type
if (overlappingKeys.length === 0) {
replaceWithCleanup(state, path, objectType);
path.skip(); // We don't want to traverse the new node
return;
}
// If there are overlapping keys, we can leave only the relevant ones
if (path.node.typeParameters) {
path.node.typeParameters.params[1] = t.tsUnionType(
overlappingKeys.map(key => t.tsLiteralType(t.stringLiteral(key))),
);
}
return;
}
// objectType is a TSTypeLiteral, so we can just remove the omitted members
if (
// Only performing the resolution on objects with non-computed string keys.
!objectType.members.every(
member =>
(t.isTSPropertySignature(member) || t.isTSMethodSignature(member)) &&
(t.isIdentifier(member.key) || t.isLiteral(member.key)),
)
) {
debug(`Unsupported Omit first parameter: ${objectType.type}`);
return;
}
replaceWithCleanup(
state,
path,
t.tsTypeLiteral(
objectType.members.filter(member => {
const propNode = (member as $FlowFixMe as t.TSPropertySignature)
.key as $FlowFixMe as t.Identifier | t.Literal;
const propName =
propNode.type === 'Identifier' ? propNode.name : propNode.value;
return !stringLiteralElements.includes(propName);
}),
),
);
path.skip(); // We don't want to traverse the new node
},
Readonly: (path, state) => {
if (
!path.node.typeParameters ||
path.node.typeParameters.params.length !== 1
) {
throw new Error(
`Readonly type must have exactly 1 type parameter. Got ${path.node.typeParameters?.params.length ?? 0}`,
);
}
const [objectType] = path.node.typeParameters.params;
if (!t.isTSTypeLiteral(objectType)) {
// The parameter was not inlined, so we cannot do anything.
return;
}
replaceWithCleanup(
state,
path,
t.tsTypeLiteral(
(t.cloneDeep(objectType).members ?? []).map(member => {
if (
t.isTSMethodSignature(member) ||
t.isTSCallSignatureDeclaration(member) ||
t.isTSConstructSignatureDeclaration(member)
) {
return member;
}
member.readonly = true;
return member;
}),
),
);
path.skip(); // We don't want to traverse the new node
},
Partial: (path, state) => {
if (
!path.node.typeParameters ||
path.node.typeParameters.params.length !== 1
) {
throw new Error(
`Partial type must have exactly 1 type parameter. Got ${path.node.typeParameters?.params.length ?? 0}`,
);
}
const [objectType] = path.node.typeParameters.params;
if (!t.isTSTypeLiteral(objectType)) {
// The parameter was not inlined, so we cannot do anything.
return;
}
replaceWithCleanup(
state,
path,
t.tsTypeLiteral(
(t.cloneDeep(objectType).members ?? []).map(member => {
if (
t.isTSMethodSignature(member) ||
t.isTSCallSignatureDeclaration(member) ||
t.isTSConstructSignatureDeclaration(member) ||
t.isTSIndexSignature(member)
) {
return member;
}
member.optional = true;
return member;
}),
),
);
path.skip(); // We don't want to traverse the new node
},
};
export function canResolveBuiltinType(name: string): boolean {
return builtinTypeResolvers.hasOwnProperty(name);
}
export function resolveBuiltinType(
path: NodePath<t.TSTypeReference>,
state: BaseVisitorState,
tsTypeResolver?: TSTypeResolver,
): void {
const name = path.node.typeName.name;
if (name != null && builtinTypeResolvers.hasOwnProperty(name)) {
builtinTypeResolvers[name](path, state, tsTypeResolver);
} else {
debug(`Unsupported builtin type: ${name ?? 'undefined'}`);
}
}
@@ -0,0 +1,159 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {BaseVisitorState} from './visitorState';
import type {NodePath} from '@babel/traverse';
import {replaceWithCleanup} from './utils';
const t = require('@babel/types');
const debug = require('debug')('build-types:transforms:inlineTypes');
export function resolveIntersection(
path: NodePath<t.TSIntersectionType>,
state: BaseVisitorState,
): void {
const newTypes: Array<BabelNodeTSType> = [];
const requiredKeys = new Set<string>();
const mutableKeys = new Set<string>();
const combinedMembers: Record<
string,
{node: BabelNodeTSTypeAnnotation, leadingComments?: BabelNodeComment[]}[],
> = {};
for (const type of path.node.types) {
if (!t.isTSTypeLiteral(type)) {
newTypes.push(type); // Leave it as is, nothing to do here
continue;
}
const members = type.members;
if (members.length === 0) {
// Empty object, lets omit it
continue;
}
const membersObj = members.reduce(
(acc, member) => {
if (t.isTSPropertySignature(member)) {
const key = member.key;
if (t.isIdentifier(key) && member.typeAnnotation) {
acc.map[key.name] = member;
} else if (t.isStringLiteral(key) && member.typeAnnotation) {
acc.map[key.value] = member;
} else {
acc.unsupported = true;
debug(`Unsupported object literal property key: ${key.type}`);
}
} else {
acc.unsupported = true;
debug(`Unsupported object literal type key: ${member.type}`);
}
return acc;
},
{
map: {} as Record<string, BabelNodeTSPropertySignature>,
unsupported: false,
},
);
if (membersObj.unsupported) {
// We cannot combine this type with the others, so we leave it as is
newTypes.push(type);
continue;
}
for (const [key, prop] of Object.entries(membersObj.map)) {
const annotation = prop.typeAnnotation;
if (!annotation) {
continue;
}
// $FlowExpectedError[sketchy-null-bool] Missing prop is the same as false
if (!prop.optional) {
requiredKeys.add(key);
}
// $FlowExpectedError[sketchy-null-bool] Missing prop is the same as false
if (!prop.readonly) {
mutableKeys.add(key);
}
if (!combinedMembers[key]) {
combinedMembers[key] = [
{node: annotation, leadingComments: prop.leadingComments},
];
} else {
combinedMembers[key].push({
node: annotation,
leadingComments: prop.leadingComments,
});
}
}
}
// Creating a type literal from the combined keys
const combinedLiteral = t.tsTypeLiteral(
Object.entries(combinedMembers).map(([key, values]) => {
const keyNode = t.isValidIdentifier(key)
? t.identifier(key)
: t.stringLiteral(key);
const prop = t.tsPropertySignature(
keyNode,
values.length === 1
? values[0].node
: t.tsTypeAnnotation(
t.tsIntersectionType(
values.map(value => value.node.typeAnnotation),
),
),
);
prop.optional = !requiredKeys.has(key);
prop.readonly = !mutableKeys.has(key);
prop.leadingComments = values
.map(value => value.leadingComments)
.filter(Boolean)
.flat();
return prop;
}),
);
if (combinedLiteral.members.length !== 0) {
newTypes.push(combinedLiteral);
}
let newNode;
if (newTypes.length === 0) {
newNode = t.tsTypeReference(
t.identifier('Record'),
t.tsTypeParameterInstantiation([t.tsStringKeyword(), t.tsNeverKeyword()]),
);
} else if (newTypes.length === 1) {
newNode = newTypes[0];
} else {
newNode = t.tsIntersectionType(newTypes);
}
const alias = state.nodeToAliasMap?.get(path.node);
if (alias !== undefined) {
// Inheriting alias from the node we're replacing
state.nodeToAliasMap.set(newNode, alias);
state.nodeToAliasMap.delete(path.node);
}
replaceWithCleanup(state, path, newNode);
path.skip(); // We don't want to traverse the new node
}
@@ -0,0 +1,79 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {NodePath} from '@babel/traverse';
import traverse from '@babel/traverse';
const inlineTypes = require('./inlineTypesVisitor');
const t = require('@babel/types');
export type TSTypeResolver = (
node: BabelNodeTSType,
aliasToPathMap: Map<string, NodePath<t.TSTypeAliasDeclaration>>,
) => ?BabelNodeTSType;
export const resolveTSType: TSTypeResolver = (node, aliasToPathMap) => {
if (t.isTSTypeLiteral(node)) {
return node;
}
if (!t.isTSTypeLiteral(node)) {
let body: ?BabelNodeTSType = null;
const wrapperAst = t.file(
t.program([
t.exportNamedDeclaration(
t.tsTypeAliasDeclaration(
t.identifier('__WrapperAlias'),
undefined,
t.cloneDeep(node),
),
),
]),
);
traverse(wrapperAst, inlineTypes.visitor, undefined, {
aliasToPathMap: aliasToPathMap,
});
traverse(wrapperAst, {
TSTypeAliasDeclaration(innerPath) {
if (innerPath.node.id.name !== '__WrapperAlias') {
return;
}
body = innerPath.node.typeAnnotation;
innerPath.stop();
},
});
if (!body) {
return;
}
const constBody = body;
if (t.isTSTypeLiteral(constBody)) {
// Only performing the resolution on objects with non-computed string keys.
if (
!constBody.members.every(
member =>
(t.isTSPropertySignature(member) ||
t.isTSMethodSignature(member)) &&
(t.isIdentifier(member.key) || t.isLiteral(member.key)),
)
) {
return;
}
}
return constBody;
}
};
@@ -0,0 +1,83 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {BaseVisitorState} from './visitorState';
import type {NodePath} from '@babel/traverse';
import {replaceWithCleanup} from './utils';
const t = require('@babel/types');
const debug = require('debug')('build-types:transforms:inlineTypes');
const typeOperatorResolvers: {
+[K: string]: (
path: NodePath<t.TSTypeOperator>,
state: BaseVisitorState,
) => void,
} = {
keyof: (path, state) => {
const unionElements: Array<BabelNodeTSType> = [];
const typeAnnotation = path.node.typeAnnotation;
if (t.isTSTypeLiteral(typeAnnotation)) {
for (const member of typeAnnotation.members) {
if (t.isTSPropertySignature(member) || t.isTSMethodSignature(member)) {
if (t.isIdentifier(member.key)) {
unionElements.push(
t.tsLiteralType(t.stringLiteral(member.key.name)),
);
} else if (t.isStringLiteral(member.key)) {
unionElements.push(
t.tsLiteralType(t.stringLiteral(member.key.value)),
);
} else {
debug(
`Unsupported object literal property key: ${member.key.type}`,
);
return;
}
} else {
debug(`Unsupported object literal type key: ${member.type}`);
return;
}
}
} else {
debug(`Unsupported type operand for keyof: ${typeAnnotation.type}`);
return;
}
replaceWithCleanup(
state,
path,
unionElements.length === 0
? t.tsNeverKeyword()
: t.tsUnionType(unionElements),
);
path.skip(); // We don't want to traverse the new node
},
};
export function canResolveTypeOperator(name: string): boolean {
return typeOperatorResolvers.hasOwnProperty(name);
}
export function resolveTypeOperator(
path: NodePath<t.TSTypeOperator>,
state: BaseVisitorState,
): void {
const name = path.node.operator;
if (canResolveTypeOperator(name)) {
typeOperatorResolvers[name](path, state);
} else {
debug(`Unsupported type operator: ${name}`);
}
}
@@ -0,0 +1,73 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {BaseVisitorState, InlineVisitorState} from './visitorState';
import type {NodePath} from '@babel/traverse';
import {
insideArrayLayer,
insideExtendsLayer,
insideKeyofLayer,
insideOmitLayer,
insideUnionLayer,
insideUnresolvableTypeInstantiation,
} from './contextStack';
const t = require('@babel/types');
export function onNodeExit(state: BaseVisitorState, node: t.Node): void {
// We're no longer inside the node if we remove it
const alias = state.nodeToAliasMap?.get(node);
if (alias !== undefined) {
state.parentTypeAliases?.delete(alias);
}
}
export function replaceWithCleanup(
state: BaseVisitorState,
path: NodePath<t.Node>,
newNode: t.Node,
) {
if (newNode === path.node) {
// Nothing to do
return;
}
// Treating `newNode` as a template, and instantiating it
const clonedNewNode = t.cloneDeep(newNode);
// We're removing the node, so let's treat it as if we're exiting it
onNodeExit(state, path.node);
path.replaceWith(clonedNewNode);
}
export function shouldSkipInliningType(
state: InlineVisitorState,
alias: string,
): boolean {
// Type instantiations of types that are not resolvable
if (insideUnresolvableTypeInstantiation(state)) {
return true;
}
// Skipping inline of union/array types, except when inside keyof or omit
if (
(insideUnionLayer(state) ||
insideArrayLayer(state) ||
insideExtendsLayer(state)) &&
!(insideKeyofLayer(state) || insideOmitLayer(state))
) {
return true;
}
return false;
}
@@ -0,0 +1,32 @@
/**
* 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 strict-local
* @format
* @oncall react_native
*/
import type {StackLayer} from './contextStack';
import type {NodePath} from '@babel/traverse';
const t = require('@babel/types');
export type BaseVisitorState = {
aliasToPathMap: Map<string, NodePath<t.TSTypeAliasDeclaration>>,
parentTypeAliases?: Set<string>,
nodeToAliasMap: Map<t.Node, string>,
...
};
export type InlineVisitorState = {
...BaseVisitorState,
/**
* Layers of operators that the current node is inside of.
* Used to determine if we should inline a type.
*/
stack: Array<StackLayer>,
...
};