From a4282a7cc3cf534f9820b19e8bb75a4064e2ba77 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 24 Mar 2025 07:21:58 -0700 Subject: [PATCH] Add pre-transform that replaces nullable types with undefined for object types (#50213) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50213 Changelog: [Internal] Reviewed By: huntie Differential Revision: D71730936 fbshipit-source-id: c126d461fda4ab9758a41421d8300dd92a5cd9af --- ...aceNullablePropertiesWithUndefined-test.js | 59 ++++++++++++++++++ .../replaceNullablePropertiesWithUndefined.js | 62 +++++++++++++++++++ .../build/build-types/translateSourceFile.js | 1 + 3 files changed, 122 insertions(+) create mode 100644 scripts/build/build-types/transforms/__tests__/replaceNullablePropertiesWithUndefined-test.js create mode 100644 scripts/build/build-types/transforms/replaceNullablePropertiesWithUndefined.js diff --git a/scripts/build/build-types/transforms/__tests__/replaceNullablePropertiesWithUndefined-test.js b/scripts/build/build-types/transforms/__tests__/replaceNullablePropertiesWithUndefined-test.js new file mode 100644 index 00000000000..745a1f39253 --- /dev/null +++ b/scripts/build/build-types/transforms/__tests__/replaceNullablePropertiesWithUndefined-test.js @@ -0,0 +1,59 @@ +/** + * 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 + */ + +const replaceNullablePropertiesWithUndefined = require('../replaceNullablePropertiesWithUndefined.js'); +const {parse, print} = require('hermes-transform'); + +const prettierOptions = {parser: 'babel'}; + +async function translate(code: string): Promise { + const parsed = await parse(code); + const result = await replaceNullablePropertiesWithUndefined(parsed); + return print(result.ast, result.mutatedCode, prettierOptions); +} + +describe('replaceNullablePropertiesWithUndefined', () => { + test('should not replace nullable with undefined outside object type definition', async () => { + const code = `function foo(arg: ?string) {}`; + const result = await translate(code); + expect(result).toMatchInlineSnapshot(` + "function foo(arg: ?string) {} + " + `); + }); + + test('should replace nullable with undefined inside object type definition', async () => { + const code = `type Foo = {bar: ?string};`; + const result = await translate(code); + expect(result).toMatchInlineSnapshot(` + "type Foo = { bar: string | undefined }; + " + `); + }); + + test('should replace nullable with undefined inside nested object type definition', async () => { + const code = `type Foo = {bar: {baz: ?string}};`; + const result = await translate(code); + expect(result).toMatchInlineSnapshot(` + "type Foo = { bar: { baz: string | undefined } }; + " + `); + }); + + test('should replace nullable with undefined in function inside object type definition', async () => { + const code = `type Foo = {bar: (?string) => void};`; + const result = await translate(code); + expect(result).toMatchInlineSnapshot(` + "type Foo = { bar: (string | undefined) => void }; + " + `); + }); +}); diff --git a/scripts/build/build-types/transforms/replaceNullablePropertiesWithUndefined.js b/scripts/build/build-types/transforms/replaceNullablePropertiesWithUndefined.js new file mode 100644 index 00000000000..7ea11d70d5c --- /dev/null +++ b/scripts/build/build-types/transforms/replaceNullablePropertiesWithUndefined.js @@ -0,0 +1,62 @@ +/** + * 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 {ESNode, TypeAnnotationType} from 'hermes-estree'; +import type {TransformVisitor} from 'hermes-transform'; +import type {ParseResult} from 'hermes-transform/dist/transform/parse'; +import type {TransformASTResult} from 'hermes-transform/dist/transform/transformAST'; + +const {transformAST} = require('hermes-transform/dist/transform/transformAST'); + +function isInsideObjectTypeProperty(node: TypeAnnotationType) { + let current: ESNode = node; + while (current) { + if (current.type === 'ObjectTypeProperty') { + return true; + } + current = current.parent; + } + return false; +} + +const visitors: TransformVisitor = context => ({ + NullableTypeAnnotation(node): void { + if (!isInsideObjectTypeProperty(node)) { + return; + } + + // $FlowExpectedError[incompatible-call] - UnionTypeAnnotation is not assignable to NullableTypeAnnotation + context.replaceNode(node, { + type: 'UnionTypeAnnotation', + types: [ + node.typeAnnotation, + { + type: 'GenericTypeAnnotation', + id: { + type: 'Identifier', + name: 'undefined', + }, + }, + ], + }); + }, +}); + +/** + * Replaces all Stringish type references with string + */ +async function replaceNullablePropertiesWithUndefined( + source: ParseResult, +): Promise { + return transformAST(source, visitors); +} + +module.exports = replaceNullablePropertiesWithUndefined; diff --git a/scripts/build/build-types/translateSourceFile.js b/scripts/build/build-types/translateSourceFile.js index 058cee1dd41..f353b70e860 100644 --- a/scripts/build/build-types/translateSourceFile.js +++ b/scripts/build/build-types/translateSourceFile.js @@ -25,6 +25,7 @@ const preTransforms: Array = [ require('./transforms/replaceRequiresWithImports'), require('./transforms/replaceEmptyWithNever'), require('./transforms/replaceStringishWithString'), + require('./transforms/replaceNullablePropertiesWithUndefined'), ]; const postTransforms: Array> = []; const prettierOptions = {parser: 'babel'};