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'),