From 850d3058be13d1b8ec4e78bf9570b1ac5acb6157 Mon Sep 17 00:00:00 2001 From: Sam Zhou Date: Wed, 14 Oct 2026 18:19:33 -0700 Subject: [PATCH] Add `LegacyFunction` to preserve callback compatibility Summary: With `experimental.function_and_object_type_is_no_longer_any`, Flow resolves `Function` to the builtin class type instead of `any`. Add `LegacyFunction` as a global compat type, following `LegacyObject`, and use it for the legacy callbacks in `Alert`, `Animated`, `PushNotificationIOS`, and `Dimensions`. The TypeScript API pre-transform replaces `LegacyFunction` with `Function`, preserving the generated public API. Remove the `unclear-type` suppressions made unnecessary by the compat type. Changelog: [Internal] Differential Revision: D125351850 --- .../react-native/Libraries/Alert/Alert.js | 2 +- .../Animated/AnimatedImplementation.js | 15 ++--- .../PushNotificationIOS.js | 9 +-- .../Libraries/Utilities/Dimensions.js | 4 +- packages/react-native/flow/LegacyFunction.js | 13 ++++ .../replaceLegacyFunctionWithFunction-test.js | 62 +++++++++++++++++++ .../flow/replaceLegacyFunctionWithFunction.js | 34 ++++++++++ .../js-api/build-types/translateSourceFile.js | 1 + 8 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 packages/react-native/flow/LegacyFunction.js create mode 100644 scripts/js-api/build-types/transforms/flow/__tests__/replaceLegacyFunctionWithFunction-test.js create mode 100644 scripts/js-api/build-types/transforms/flow/replaceLegacyFunctionWithFunction.js diff --git a/packages/react-native/Libraries/Alert/Alert.js b/packages/react-native/Libraries/Alert/Alert.js index 0efac6d6beda..ee2b40a7a5d4 100644 --- a/packages/react-native/Libraries/Alert/Alert.js +++ b/packages/react-native/Libraries/Alert/Alert.js @@ -27,7 +27,7 @@ export type AlertButtonStyle = 'default' | 'cancel' | 'destructive'; export type AlertButton = { text?: string, // $FlowFixMe[unclear-type] - onPress?: ?((value?: string) => any) | ?Function, + onPress?: ?((value?: string) => any) | ?LegacyFunction, isPreferred?: boolean, style?: AlertButtonStyle, ... diff --git a/packages/react-native/Libraries/Animated/AnimatedImplementation.js b/packages/react-native/Libraries/Animated/AnimatedImplementation.js index 14cbef1fb195..af29e9433718 100644 --- a/packages/react-native/Libraries/Animated/AnimatedImplementation.js +++ b/packages/react-native/Libraries/Animated/AnimatedImplementation.js @@ -559,12 +559,9 @@ const loopImpl = function ( }; function forkEventImpl( - // $FlowFixMe[unclear-type] - event: ?AnimatedEvent | ?Function, - // $FlowFixMe[unclear-type] - listener: Function, - // $FlowFixMe[unclear-type] -): AnimatedEvent | Function { + event: ?AnimatedEvent | ?LegacyFunction, + listener: LegacyFunction, +): AnimatedEvent | LegacyFunction { if (!event) { return listener; } else if (event instanceof AnimatedEvent) { @@ -579,10 +576,8 @@ function forkEventImpl( } function unforkEventImpl( - // $FlowFixMe[unclear-type] - event: ?AnimatedEvent | ?Function, - // $FlowFixMe[unclear-type] - listener: Function, + event: ?AnimatedEvent | ?LegacyFunction, + listener: LegacyFunction, ): void { if (event && event instanceof AnimatedEvent) { event.__removeListener(listener); diff --git a/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js b/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js index 1c69fc55d83b..3ff50971b355 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -319,8 +319,7 @@ class PushNotificationIOS { * * See https://reactnative.dev/docs/pushnotificationios#getapplicationiconbadgenumber */ - // $FlowFixMe[unclear-type] - static getApplicationIconBadgeNumber(callback: Function): void { + static getApplicationIconBadgeNumber(callback: LegacyFunction): void { invariant( NativePushNotificationManagerIOS, 'PushNotificationManager is not available.', @@ -348,8 +347,7 @@ class PushNotificationIOS { * * See https://reactnative.dev/docs/pushnotificationios#getscheduledlocalnotifications */ - // $FlowFixMe[unclear-type] - static getScheduledLocalNotifications(callback: Function): void { + static getScheduledLocalNotifications(callback: LegacyFunction): void { invariant( NativePushNotificationManagerIOS, 'PushNotificationManager is not available.', @@ -365,8 +363,7 @@ class PushNotificationIOS { */ static addEventListener( type: PushNotificationEventName, - // $FlowFixMe[unclear-type] - handler: Function, + handler: LegacyFunction, ): void { invariant( type === 'notification' || diff --git a/packages/react-native/Libraries/Utilities/Dimensions.js b/packages/react-native/Libraries/Utilities/Dimensions.js index 13458dd737db..3e5dc8385cb5 100644 --- a/packages/react-native/Libraries/Utilities/Dimensions.js +++ b/packages/react-native/Libraries/Utilities/Dimensions.js @@ -111,9 +111,7 @@ class Dimensions { */ static addEventListener( type: 'change', - /* $FlowFixMe[unclear-type] Callers annotate the payload with assorted - * object shapes that any precise signature would reject. */ - handler: Function, + handler: LegacyFunction, ): EventSubscription { invariant( type === 'change', diff --git a/packages/react-native/flow/LegacyFunction.js b/packages/react-native/flow/LegacyFunction.js new file mode 100644 index 000000000000..cbf48b6cb12b --- /dev/null +++ b/packages/react-native/flow/LegacyFunction.js @@ -0,0 +1,13 @@ +/** + * 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 + * @format + */ + +// The legacy Flow `Function` type, which is `any`. The generated TypeScript API +// (scripts/js-api/build-types) translates this type to `Function`. +declare type LegacyFunction = any; diff --git a/scripts/js-api/build-types/transforms/flow/__tests__/replaceLegacyFunctionWithFunction-test.js b/scripts/js-api/build-types/transforms/flow/__tests__/replaceLegacyFunctionWithFunction-test.js new file mode 100644 index 000000000000..a71b64dc13df --- /dev/null +++ b/scripts/js-api/build-types/transforms/flow/__tests__/replaceLegacyFunctionWithFunction-test.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 + */ + +const translateSourceFile = require('../../../translateSourceFile'); +const replaceLegacyFunctionWithFunction = require('../replaceLegacyFunctionWithFunction'); +const {parse, print} = require('flow-transform'); + +const prettierOptions = {parser: 'babel'}; + +async function translate(code: string): Promise { + const parsed = await parse(code); + const result = await replaceLegacyFunctionWithFunction(parsed); + return print(result.ast, result.mutatedCode, prettierOptions); +} + +describe('replaceLegacyFunctionWithFunction', () => { + test('replaces LegacyFunction in return annotations', async () => { + expect(await translate('function foo(): LegacyFunction {}')).toBe( + 'function foo(): Function {}\n', + ); + }); + + test('replaces LegacyFunction in nullable unions', async () => { + expect( + await translate('let foo: ?((value?: string) => any) | ?LegacyFunction;'), + ).toBe('let foo: ?((value?: string) => any) | ?Function;\n'); + }); + + test('replaces LegacyFunction in type aliases', async () => { + expect(await translate('type Foo = LegacyFunction;')).toBe( + 'type Foo = Function;\n', + ); + }); + + test('preserves unrelated and qualified types', async () => { + const code = 'type Foo = Function | Other.LegacyFunction;'; + expect(await translate(code)).toBe(code + '\n'); + }); + + test('preserves Function in the generated TypeScript API', async () => { + const source = `export type Callbacks = { + onPress?: ?((value?: string) => any) | ?LegacyFunction, + listener: LegacyFunction, + }; + export function forkEvent(event: ?LegacyFunction): LegacyFunction { + return event; + }`; + const result = await translateSourceFile(source, __filename); + const original = await translateSourceFile( + source.replaceAll('LegacyFunction', 'Function'), + __filename, + ); + expect(result).toEqual(original); + }); +}); diff --git a/scripts/js-api/build-types/transforms/flow/replaceLegacyFunctionWithFunction.js b/scripts/js-api/build-types/transforms/flow/replaceLegacyFunctionWithFunction.js new file mode 100644 index 000000000000..f030ba986679 --- /dev/null +++ b/scripts/js-api/build-types/transforms/flow/replaceLegacyFunctionWithFunction.js @@ -0,0 +1,34 @@ +/** + * 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 + */ + +import type {TransformVisitor} from 'flow-transform'; +import type {ParseResult} from 'flow-transform/dist/transform/parse'; +import type {TransformASTResult} from 'flow-transform/dist/transform/transformAST'; + +const {transformAST} = require('flow-transform/dist/transform/transformAST'); + +const visitors: TransformVisitor = context => ({ + GenericTypeAnnotation(node): void { + if (node.id.type === 'Identifier' && node.id.name === 'LegacyFunction') { + context.modifyNodeInPlace(node.id, {name: 'Function'}); + } + }, +}); + +/** + * Replaces LegacyFunction with Function to preserve the TypeScript API. + */ +async function replaceLegacyFunctionWithFunction( + source: ParseResult, +): Promise { + return transformAST(source, visitors); +} + +module.exports = replaceLegacyFunctionWithFunction; diff --git a/scripts/js-api/build-types/translateSourceFile.js b/scripts/js-api/build-types/translateSourceFile.js index 0e76ca2004d5..831b99a3acd3 100644 --- a/scripts/js-api/build-types/translateSourceFile.js +++ b/scripts/js-api/build-types/translateSourceFile.js @@ -24,6 +24,7 @@ const preTransforms: Array = [ require('./transforms/flow/replaceRequiresWithImports'), require('./transforms/flow/replaceEmptyWithNever'), require('./transforms/flow/replaceStringishWithString'), + require('./transforms/flow/replaceLegacyFunctionWithFunction'), require('./transforms/flow/replaceLegacyObjectWithObject'), require('./transforms/flow/replaceNullablePropertiesWithUndefined'), require('./transforms/flow/reattachDocComments'),