From 6590bb3e146d074c71eaac3fc54e603db2f97265 Mon Sep 17 00:00:00 2001 From: Eli White Date: Tue, 11 Feb 2025 08:23:16 -0800 Subject: [PATCH] Initial Open Sourcing of React Native Compatibility Check (#49311) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49311 This tool enables checking the boundary between JavaScript and Native for backwards incompatible changes to protect against crashes. This is useful for: - Local Development - Over the Air updates on platforms that support it - Theoretically: Server Components with React Native Check out the Readme for more information Changelog: [General][Added] Open Sourcing React Native's Compatibility Check Reviewed By: yungsters Differential Revision: D69277991 fbshipit-source-id: 886a983d4b17609ce771cdd93b75f34bbd8417dc --- .../.gitignore | 1 + .../CONTRIBUTING.md | 36 + .../README.md | 245 +++ .../package.json | 38 + .../src/ComparisonResult.js | 239 +++ .../src/DiffResults.js | 207 +++ .../src/ErrorFormatting.js | 309 ++++ .../src/SortTypeAnnotations.js | 290 +++ .../src/TypeDiffing.js | 1427 +++++++++++++++ .../src/VersionDiffing.js | 1370 ++++++++++++++ .../src/__tests__/ErrorFormatting-test.js | 65 + .../src/__tests__/ErrorFormattingTests.js | 150 ++ .../src/__tests__/TypeDiffing-test.js | 1207 +++++++++++++ .../src/__tests__/VersionDiffing-test.js | 1603 +++++++++++++++++ .../NativeComponent.js.flow | 169 ++ .../NativeComponent.js.flow | 60 + .../NativeComponent.js.flow | 48 + .../NativeComponent.js.flow | 33 + .../NativeComponent.js.flow | 38 + .../NativeComponent.js.flow | 35 + .../NativeComponent.js.flow | 34 + .../NativeComponent.js.flow | 30 + .../NativeComponent.js.flow | 30 + .../NativeComponent.js.flow | 39 + .../NativeComponent.js.flow | 24 + .../NativeComponent.js.flow | 24 + .../NativeComponent.js.flow | 29 + .../NativeComponent.js.flow | 29 + .../NativeComponent.js.flow | 35 + .../NativeComponent.js.flow | 35 + .../NativeComponent.js.flow | 34 + .../NativeComponent.js.flow | 24 + .../NativeComponent.js.flow | 24 + .../NativeComponent.js.flow | 29 + .../native-component/NativeComponent.js.flow | 22 + .../NativeModuleBeforeAfterTypes.js.flow | 33 + .../NativeModuleBeforeAfterTypes.js.flow | 73 + .../NativeModuleBeforeAfterTypes.js.flow | 66 + .../NativeModule.js.flow | 24 + .../NativeModule.js.flow | 24 + .../NativeModule.js.flow | 24 + .../NativeModule.js.flow | 24 + .../NativeModule.js.flow | 24 + .../NativeModule.js.flow | 22 + .../NativeModule.js.flow | 22 + .../NativeModule.js.flow | 23 + .../NativeModule.js.flow | 23 + .../NativeModule.js.flow | 32 + .../NativeModule.js.flow | 32 + .../NativeModule.js.flow | 32 + .../native-module-nested/NativeModule.js.flow | 31 + .../NativeTypeDiffingTypes.js.flow | 218 +++ .../NativeModule.js.flow | 24 + .../NativeModule.js.flow | 24 + .../NativeModule.js.flow | 31 + .../NativeModule.js.flow | 31 + .../NativeModule.js.flow | 30 + .../NativeModule.js.flow | 30 + .../NativeModule.js.flow | 30 + .../NativeModule.js.flow | 31 + .../NativeModule.js.flow | 30 + .../NativeModule.js.flow | 30 + .../NativeModule.js.flow | 31 + .../NativeModule.js.flow | 31 + .../NativeModule.js.flow | 24 + .../NativeModule.js.flow | 26 + .../NativeModule.js.flow | 26 + .../NativeModule.js.flow | 26 + .../NativeModule.js.flow | 26 + .../NativeModule.js.flow | 26 + .../NativeModule.js.flow | 26 + .../native-module/NativeModule.js.flow | 24 + .../ErrorFormatting-test.js.snap | 792 ++++++++ .../src/__tests__/utilities/getTestSchema.js | 27 + .../src/convertPropToBasicTypes.js | 104 ++ .../src/index.flow.js | 12 + .../src/index.js | 54 + scripts/build/config.js | 4 + yarn.lock | 26 + 79 files changed, 10315 insertions(+) create mode 100644 packages/react-native-compatibility-check/.gitignore create mode 100644 packages/react-native-compatibility-check/CONTRIBUTING.md create mode 100644 packages/react-native-compatibility-check/README.md create mode 100644 packages/react-native-compatibility-check/package.json create mode 100644 packages/react-native-compatibility-check/src/ComparisonResult.js create mode 100644 packages/react-native-compatibility-check/src/DiffResults.js create mode 100644 packages/react-native-compatibility-check/src/ErrorFormatting.js create mode 100644 packages/react-native-compatibility-check/src/SortTypeAnnotations.js create mode 100644 packages/react-native-compatibility-check/src/TypeDiffing.js create mode 100644 packages/react-native-compatibility-check/src/VersionDiffing.js create mode 100644 packages/react-native-compatibility-check/src/__tests__/ErrorFormatting-test.js create mode 100644 packages/react-native-compatibility-check/src/__tests__/ErrorFormattingTests.js create mode 100644 packages/react-native-compatibility-check/src/__tests__/TypeDiffing-test.js create mode 100644 packages/react-native-compatibility-check/src/__tests__/VersionDiffing-test.js create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-all-types/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-all-basic-types-arrays/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-all-basic-types/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-changed/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-extra-arg/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-extra-command/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-added-optional-prop/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-added-required-prop/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-all-types/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-array-union-added/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-array-union/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-changes/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-default-change/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object-added-optional-key/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object-added-required-key/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-union-added/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-union/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component/NativeComponent.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types-removed/NativeModuleBeforeAfterTypes.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types-type-changed/NativeModuleBeforeAfterTypes.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types/NativeModuleBeforeAfterTypes.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-changed/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-optional-constant-readonly/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-optional-constant/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-required-constant-readonly/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-required-constant/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-readonly/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-alias-changed/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-alias/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-changed/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-nullable/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-optional/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-type-diffing-types/NativeTypeDiffingTypes.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-dictionary-changed/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-dictionary/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-type-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-value-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-type-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-value-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-eventemitter-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-eventemitter/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-optional-argument/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-confusing-string-literals/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-from-native-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-from-native/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-type-changes/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module/NativeModule.js.flow create mode 100644 packages/react-native-compatibility-check/src/__tests__/__snapshots__/ErrorFormatting-test.js.snap create mode 100644 packages/react-native-compatibility-check/src/__tests__/utilities/getTestSchema.js create mode 100644 packages/react-native-compatibility-check/src/convertPropToBasicTypes.js create mode 100644 packages/react-native-compatibility-check/src/index.flow.js create mode 100644 packages/react-native-compatibility-check/src/index.js diff --git a/packages/react-native-compatibility-check/.gitignore b/packages/react-native-compatibility-check/.gitignore new file mode 100644 index 00000000000..a65b41774ad --- /dev/null +++ b/packages/react-native-compatibility-check/.gitignore @@ -0,0 +1 @@ +lib diff --git a/packages/react-native-compatibility-check/CONTRIBUTING.md b/packages/react-native-compatibility-check/CONTRIBUTING.md new file mode 100644 index 00000000000..c48a6b73f01 --- /dev/null +++ b/packages/react-native-compatibility-check/CONTRIBUTING.md @@ -0,0 +1,36 @@ +This tool is essentially a type checker, and as such it can be difficult to +understand the data flow. Luckily, there are fairly extensive tests which can +aid in ramping up. + +This tool is made up of 3 primary stages: TypeDiffing, VersionDiffing, and +ErrorFormatting. + +At a high level, the schemas are passed to TypeDiffing which is the pure +typechecker. It returns all differences between the types. VersionDiffing then +interprets these results and decides if some of those changes are actually safe +in the context of React Native’s JS/Native boundary. + +For example, if you have a NativeModule method that returns a string union +`small | medium | large`. Any changes to that union would be flagged by +TypeDiffing as incompatible. However, adding a value to that union is safe +because it ensures your JS code handles more cases than native returns. Removing +a value from that union isn’t safe though because it means your JS no longer +handles something native might return which could cause an exception. + +VersionDiffing encodes the logic of what is safe and what isn’t; +property/union/enum additions and removals, changing something from optional to +required and vice versa, etc. VersionDiffing has knowledge of components and +modules. + +VersionDiffing returns a set of incompatible changes, which then gets passed to +ErrorFormatting. ErrorFormatting does as you’d expect, converting these deep +objects into nicely formatted strings. + +When contributing, some principles: + +- Keep TypeDiffing and ErrorFormatting pure. They should only know about + JavaScript types, not React Native specific concepts +- Add tests for every case you can think of. This codebase can be complex and + hard to reason about when making changes. The only way to stay sane is to be + able to rely on the tests to catch anything bad you’ve done. Do yourself and + future contributors a favor. diff --git a/packages/react-native-compatibility-check/README.md b/packages/react-native-compatibility-check/README.md new file mode 100644 index 00000000000..0e727aaf885 --- /dev/null +++ b/packages/react-native-compatibility-check/README.md @@ -0,0 +1,245 @@ +# **React Native compatibility-check** + +Status: Experimental (stage 1) + +Work In Progress. Documentation is lacking, and intended to be used by power +users at this point. + +This tool enables checking the boundary between JavaScript and Native for +backwards incompatible changes to protect against crashes. + +This is useful for: + +- Local Development +- Over the Air updates on platforms that support it +- Theoretically: Server Components with React Native + +## **Motivating Problems** + +Let’s look at some motivating examples for this project. + +> [!NOTE] +> The examples below are written with Flow, but the compatibility-check +> tool is agnostic to the types you write. The compatibility-check runs on JSON +> schema files, most commonly generated by the +> [@react-native/codegen](https://www.npmjs.com/package/@react-native/codegen) +> tool which supports both TypeScript and Flow. + +### **Adding new methods** + +You might have an Analytics Native Module in your app, and you last built the +native client a couple of days ago: + +```javascript +export interface Spec extends TurboModule { + log: (eventName: string, content: string) => void; +} +``` + +And you are working on a change to add a new method to this Native Module: + +```javascript +export interface Spec extends TurboModule { + log: (eventName: string, content: string) => void; + logError: (message: string) => void; +} +``` + +``` +NativeAnalytics.logError('Oh No! We hit a crash') +``` + +Since you are working on this, you’ve built a new native client and tested the +change on your computer and everything works. + +However, when your colleague pulls your latest changes and tries to run it, +they’ll get a crash `logError is not a function`. They need to rebuild their +native client\! + +Using this tool, you can detect this incompatibility at build time, getting an +error that looks like: + +``` +NativeAnalytics: Object added required properties, which native will not provide + -- logError +``` + +Errors like this can occur for much more nuanced reasons than adding a method. +For example: + +### **Sending native new union values** + +```javascript +export interface Spec extends TurboModule { + // You add 'system' to this union + +setColorScheme: (color: 'light' | 'dark') => void; +} +``` + +If you add a new option of `system` and add native support for that option, when +you call this method with `system` on your commit it would work but on an older +build not expecting `system` it will crash. This tool will give you the error +message: + +``` +ColorManager.setColorScheme parameter 0: Union added items, but native will not expect/support them + -- position 3 system +``` + +### **Changing an enum value sent from native** + +As another example, say you are getting the color scheme from the system as an +integer value, used in JavaScript as an enum: + +```javascript +enum TestEnum { + LIGHT = 1, + DARK = 2, + SYSTEM = 3, +} + +export interface Spec extends TurboModule { + getColorScheme: () => TestEnum; +} +``` + +And you realize you actually need native to send `-1` for System instead of 3\. + +```javascript +enum TestEnum { + LIGHT = 1, + DARK = 2, + SYSTEM = -1, +} +``` + +If you make this change and run the JavaScript on an old build, it might still +send JavaScript the value 3, which your JavaScript isn’t handling anymore\! + +This tool gives an error: + +```javascript +ColorManager: Object contained a property with a type mismatch + -- getColorScheme: has conflicting type changes + --new: ()=>Enum + --old: ()=>Enum + Function return types do not match + --new: ()=>Enum + --old: ()=>Enum + Enum types do not match + --new: Enum {LIGHT = 1, DARK = 2, SYSTEM = -1} + --old: Enum {LIGHT = 1, DARK = 2, SYSTEM = 3} + Enum contained a member with a type mismatch + -- Member SYSTEM: has conflicting changes + --new: -1 + --old: 3 + Numeric literals are not equal + --new: -1 + --old: 3 + +``` + +## **Avoiding Breaking Changes** + +You can use this tool to either detect changes locally to warn that you need to +install a new native build, or when doing OTA you might need to guarantee that +the changes in your PR are compatible with the native client they’ll be running +in. + +### **Example 1** + +In example 1, when adding logError, it needs to be optional to be safe: + +```javascript +export interface Spec extends TurboModule { + log: (eventName: string, content: string) => void; + logError?: (message: string) => void; +} +``` + +That will enforce if you are using TypeScript or Flow that you check if the +native client supports logError before calling it: + +```javascript +if (NativeAnalytics.logError) { + NativeAnalytics.logError('Oh No! We hit a crash'); +} +``` + +### **Example 2** + +When you want to add '`system'` as a value to the union, modifying the existing +union is not safe. You would need to add a new optional method that has that +change. You can clean up the old method when you know that all of the builds you +ever want to run this JavaScript on have native support. + +```javascript +export interface Spec extends TurboModule { + +setColorScheme: (color: 'light' | 'dark') => void + +setColorSchemeWithSystem?: (color: 'light' | 'dark' | 'system') => void +} +``` + +### **Example 3** + +Changing a union case is similar to Example 2, you would either need a new +method, or support the existing value and the new `-1`. + +``` +enum TestEnum { + LIGHT = 1, + DARK = 2, + SYSTEM = 3, + SYSTEM_ALSO = -1, +} +``` + +## **Installation** + +``` +yarn add @react-native/compatibility-check +``` + +## **Usage** + +To use this package, you’ll need a script that works something like this: + +This script checks the compatibility of a React Native app's schema between two +versions. It takes into account the changes made to the schema and determines +whether they are compatible or not. + +```javascript +import {compareSchemas} from '@react-native/compatibility-check'; +const util = require('util'); + +async function run(argv: Argv, STDERR: string) { + const debug = (log: mixed) => { + argv.debug && + console.info(util.inspect(log, {showHidden: false, depth: null})); + }; + + const currentSchema = + JSON.parse(/*you'll read the file generated by codegen wherever it is in your app*/); + const previousSchema = + JSON.parse(/*you'll read the schema file that you persisted from when your native app was built*/); + + const safetyResult = compareSchemas(currentSchema, previousSchema); + + const summary = safetyResult.getSummary(); + switch (summary.status) { + case 'ok': + debug('No changes in boundary'); + console.log(JSON.stringify(summary)); + break; + case 'patchable': + debug('Changes in boundary, but are compatible'); + debug(result.getDebugInfo()); + console.log(JSON.stringify(summary)); + break; + default: + debug(result.getDebugInfo()); + console.error(JSON.stringify(result.getErrors())); + throw new Error(`Incompatible changes in boundary`); + } +} +``` diff --git a/packages/react-native-compatibility-check/package.json b/packages/react-native-compatibility-check/package.json new file mode 100644 index 00000000000..d145d16ad63 --- /dev/null +++ b/packages/react-native-compatibility-check/package.json @@ -0,0 +1,38 @@ +{ + "name": "@react-native/compatibility-check", + "version": "0.0.1", + "description": "Check a React Native app's boundary between JS and Native for incompatibilities", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/facebook/react-native.git", + "directory": "packages/react-native-compatibility-check" + }, + "homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/react-native-compatibility-check#readme", + "keywords": [ + "boundary", + "crashes", + "native", + "codegen", + "tools", + "react-native" + ], + "bugs": "https://github.com/facebook/react-native/issues", + "engines": { + "node": ">=18" + }, + "exports": { + ".": "./src/index.js", + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "dependencies": { + "@react-native/codegen": "0.79.0-main" + }, + "devDependencies": { + "flow-remove-types": "^2.237.2", + "rimraf": "^3.0.2" + } +} diff --git a/packages/react-native-compatibility-check/src/ComparisonResult.js b/packages/react-native-compatibility-check/src/ComparisonResult.js new file mode 100644 index 00000000000..933ad147364 --- /dev/null +++ b/packages/react-native-compatibility-check/src/ComparisonResult.js @@ -0,0 +1,239 @@ +/** + * 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 + */ + +import type { + CompleteTypeAnnotation, + NamedShape, + NativeModuleEnumMember, +} from '@react-native/codegen/src/CodegenSchema'; + +type TypeAnnotationComparisonError = { + type: 'TypeAnnotationComparisonError', + message: string, + newerAnnotation: CompleteTypeAnnotation, + olderAnnotation: CompleteTypeAnnotation, + previousError?: TypeComparisonError, +}; +type TypeInformationComparisonError = { + type: 'TypeInformationComparisonError', + message: string, + newerType: CompleteTypeAnnotation, + olderType: CompleteTypeAnnotation, + previousError?: TypeComparisonError, +}; +type PropertyComparisonError = { + type: 'PropertyComparisonError', + message: string, + mismatchedProperties: Array<{ + property: string, + fault?: TypeComparisonError, + ... + }>, + previousError?: TypeComparisonError, +}; +type PositionalComparisonError = { + type: 'PositionalComparisonError', + message: string, + erroneousItems: Array<[number, CompleteTypeAnnotation]>, + previousError?: TypeComparisonError, +}; +type MemberComparisonError = { + type: 'MemberComparisonError', + message: string, + mismatchedMembers: Array<{ + member: string, + fault?: TypeComparisonError, + }>, + previousError?: TypeComparisonError, +}; +export type TypeComparisonError = + | TypeAnnotationComparisonError + | TypeInformationComparisonError + | PropertyComparisonError + | PositionalComparisonError + | MemberComparisonError; + +// Collects changes that may be type safe within parameters, unions, intersections, and tuples +export type PositionalComparisonResult = { + typeKind: 'stringUnion' | 'union' | 'intersection' | 'parameter' | 'tuple', + // Nested changes stores the position of the old type followed by new + // Except for union and intersection, new position === old position + nestedChanges: Array<[number, number, ComparisonResult]>, + // These properties should never occur for a tuple + addedElements?: Array<[number, CompleteTypeAnnotation]>, + removedElements?: Array<[number, CompleteTypeAnnotation]>, + ... +}; +export type FunctionComparisonResult = { + returnType?: ComparisonResult, + // The following should always have typeKind 'parameter' + parameterTypes?: PositionalComparisonResult, + ... +}; + +// Array>> + +export type PropertiesComparisonResult = { + addedProperties?: $ReadOnlyArray>, + missingProperties?: $ReadOnlyArray>, + errorProperties?: Array<{ + property: string, + fault?: TypeComparisonError, + ... + }>, + madeStrict?: Array<{ + property: string, + furtherChanges?: ComparisonResult, + ... + }>, + madeOptional?: Array<{ + property: string, + furtherChanges?: ComparisonResult, + ... + }>, + nestedPropertyChanges?: Array<[string, ComparisonResult]>, + ... +}; +export type MembersComparisonResult = { + addedMembers?: Array, + missingMembers?: Array, + errorMembers?: Array<{ + member: string, + fault?: TypeComparisonError, + }>, +}; +export type NullableComparisonResult = { + /* Four possible cases of change: + void goes to T? :: typeRefined !optionsReduced + T? goes to void :: typeRefined optionsReduced + T goes to T? :: !typeRefined !optionsReduced + T? goes to T :: !typeRefined optionsReduced + */ + typeRefined: boolean, + optionsReduced: boolean, + // interiorLog not available if either type is void + interiorLog: ?ComparisonResult, + newType: ?CompleteTypeAnnotation, + oldType: ?CompleteTypeAnnotation, + ... +}; +export type ComparisonResult = + | {status: 'matching'} + | {status: 'skipped'} + | {status: 'nullableChange', nullableLog: NullableComparisonResult} + | {status: 'properties', propertyLog: PropertiesComparisonResult} + | {status: 'members', memberLog: MembersComparisonResult} + | {status: 'functionChange', functionChangeLog: FunctionComparisonResult} + | {status: 'positionalTypeChange', changeLog: PositionalComparisonResult} + | {status: 'error', errorLog: TypeComparisonError}; + +export function isPropertyLogEmpty( + result: PropertiesComparisonResult, +): boolean { + return !( + result.addedProperties || + result.missingProperties || + result.nestedPropertyChanges || + result.madeStrict || + result.madeOptional || + result.errorProperties + ); +} + +export function isMemberLogEmpty(result: MembersComparisonResult): boolean { + return !(result.addedMembers || result.missingMembers || result.errorMembers); +} + +export function isFunctionLogEmpty(result: FunctionComparisonResult): boolean { + return !(result.returnType || result.parameterTypes); +} + +export function makeError(error: TypeComparisonError): ComparisonResult { + return { + status: 'error', + errorLog: error, + }; +} + +export function typeInformationComparisonError( + message: string, + newerType: CompleteTypeAnnotation, + olderType: CompleteTypeAnnotation, + previousError?: TypeComparisonError, +): TypeComparisonError { + return { + type: 'TypeInformationComparisonError', + message, + newerType, + olderType, + previousError, + }; +} + +export function typeAnnotationComparisonError( + message: string, + newerAnnotation: CompleteTypeAnnotation, + olderAnnotation: CompleteTypeAnnotation, + previousError?: TypeComparisonError, +): TypeComparisonError { + return { + type: 'TypeAnnotationComparisonError', + message, + newerAnnotation, + olderAnnotation, + previousError, + }; +} + +export function propertyComparisonError( + message: string, + mismatchedProperties: Array<{ + property: string, + fault?: TypeComparisonError, + ... + }>, + previousError?: TypeComparisonError, +): TypeComparisonError { + return { + type: 'PropertyComparisonError', + message, + mismatchedProperties, + previousError, + }; +} + +export function memberComparisonError( + message: string, + mismatchedMembers: Array<{ + member: string, + fault?: TypeComparisonError, + }>, + previousError?: TypeComparisonError, +): TypeComparisonError { + return { + type: 'MemberComparisonError', + message, + mismatchedMembers, + previousError, + }; +} + +export function positionalComparisonError( + message: string, + erroneousItems: Array<[number, CompleteTypeAnnotation]>, + previousError?: TypeComparisonError, +): TypeComparisonError { + return { + type: 'PositionalComparisonError', + message, + erroneousItems, + previousError, + }; +} diff --git a/packages/react-native-compatibility-check/src/DiffResults.js b/packages/react-native-compatibility-check/src/DiffResults.js new file mode 100644 index 00000000000..33eac9b980c --- /dev/null +++ b/packages/react-native-compatibility-check/src/DiffResults.js @@ -0,0 +1,207 @@ +/** + * 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 { + PropertiesComparisonResult, + TypeComparisonError, +} from './ComparisonResult'; +import type {CompleteTypeAnnotation} from '@react-native/codegen/src/CodegenSchema'; + +export type ErrorCode = + | 'addedProps' + | 'removedProps' + | 'changedParams' + | 'incompatibleTypes' + | 'requiredProps' + | 'optionalProps' + | 'nonNullableOfNull' + | 'nullableOfNonNull' + | 'removedUnionCases' + | 'addedUnionCases' + | 'addedEnumCases' + | 'removedEnumCases' + | 'removedIntersectCases' + | 'addedIntersectCases' + | 'removedModule' + | 'removedComponent'; + +export type TypeStore = { + typeName: string, + typeInformation: CompleteTypeAnnotation, + ... +}; +// Stores object properties that are added or removed which could be patchable +export type ObjectTypeChangeStore = { + typeName: string, + newType: CompleteTypeAnnotation, + oldType: CompleteTypeAnnotation, + propertyChange: PropertiesComparisonResult, +}; +export type ErrorStore = { + typeName: string, + errorCode: ErrorCode, + errorInformation: TypeComparisonError, +}; +export type FormattedErrorStore = { + message: string, + errorCode: ErrorCode, +}; +export type NativeSpecErrorStore = { + nativeSpecName: string, + omitted: boolean, + errorCode: ErrorCode, + errorInformation?: TypeComparisonError, + changeInformation?: DiffSet, +}; +export type ExportableNativeSpecErrorStore = { + nativeSpecName: string, + omitted: boolean, + errorCode: ErrorCode, + errorInformation?: TypeComparisonError, + changeInformation?: ExportableDiffSet, +}; +export type DiffSet = { + newTypes: Set, + deprecatedTypes: Set, + objectTypeChanges: Set, + incompatibleChanges: Set, +}; +type ExportableDiffSet = { + newTypes: Array, + deprecatedTypes: Array, + objectTypeChanges: Array, + incompatibleChanges: Array, +}; + +export type Framework = 'ReactNative'; +export type SchemaDiffers = { + incompatibleSpecs: ?Set, +}; +type ExportableSchemaDiffers = { + incompatibleSpecs: ?Array, +}; +export type SchemaDiffCategory = 'new' | 'deprecated' | SchemaDiffers; +type ExportableSchemaDiffCategory = + | 'new' + | 'deprecated' + | ExportableSchemaDiffers; +export type SchemaDiff = { + name: string, + framework: Framework, + status: SchemaDiffCategory, +}; +export type ExportableSchemaDiff = { + name: string, + framework: Framework, + status: ExportableSchemaDiffCategory, +}; + +export type Version = { + device: 'android' | 'ios', + number: string, + ... +}; + +export type Incompatible = { + framework: Framework, + incompatibleSpecs?: Array, +}; +export type IncompatiblityReport = { + [hasteModuleName: string]: Incompatible, +}; +export type DiffSummary = { + /** status records how the diff compares against older versions + * ok: there are no changes that impact older versions + * patchable: there are additions (or modifications) that are not suported + * in older versions but is safe with an auto-generated patch + * incompatible: there are modifications that are not safe for use with older + * versions and are not fixable with an auto-generated patchable + */ + status: 'ok' | 'patchable' | 'incompatible', + // If there are incompatible changes, provide a record of them, otherwise {} + incompatibilityReport: IncompatiblityReport, +}; + +export type FormattedIncompatible = { + framework: Framework, + incompatibleSpecs?: Array, +}; + +export type FormattedIncompatiblityReport = { + [hasteModuleName: string]: FormattedIncompatible, +}; + +export type FormattedDiffSummary = { + /** status records how the diff compares against older versions + * ok: there are no changes that impact older versions + * patchable: there are additions (or modifications) that are not suported + * in older versions but is safe with an auto-generated patch + * incompatible: there are modifications that are not safe for use with older + * versions and are not fixable with an auto-generated patchable + */ + status: 'ok' | 'patchable' | 'incompatible', + // If there are incompatible changes, provide a record of them, otherwise {} + incompatibilityReport: FormattedIncompatiblityReport, +}; + +function diffSetExporter(diffSet: DiffSet): ExportableDiffSet { + return { + newTypes: Array.from(diffSet.newTypes), + deprecatedTypes: Array.from(diffSet.deprecatedTypes), + objectTypeChanges: Array.from(diffSet.objectTypeChanges), + incompatibleChanges: Array.from(diffSet.incompatibleChanges), + }; +} + +export function nativeSpecErrorExporter( + nativeSpecError: NativeSpecErrorStore, +): ExportableNativeSpecErrorStore { + if (nativeSpecError.changeInformation) { + return { + nativeSpecName: nativeSpecError.nativeSpecName, + omitted: nativeSpecError.omitted, + errorCode: nativeSpecError.errorCode, + errorInformation: nativeSpecError.errorInformation, + changeInformation: diffSetExporter(nativeSpecError.changeInformation), + }; + } + return { + nativeSpecName: nativeSpecError.nativeSpecName, + omitted: nativeSpecError.omitted, + errorCode: nativeSpecError.errorCode, + errorInformation: nativeSpecError.errorInformation, + }; +} + +function schemaDiffCategoryExporter( + status: SchemaDiffCategory, +): ExportableSchemaDiffCategory { + switch (status) { + case 'new': + case 'deprecated': + return status; + default: + return { + incompatibleSpecs: status.incompatibleSpecs + ? Array.from(status.incompatibleSpecs).map(nativeSpecErrorExporter) + : undefined, + }; + } +} + +export function schemaDiffExporter( + schemaDiff: SchemaDiff, +): ExportableSchemaDiff { + return { + name: schemaDiff.name, + framework: schemaDiff.framework, + status: schemaDiffCategoryExporter(schemaDiff.status), + }; +} diff --git a/packages/react-native-compatibility-check/src/ErrorFormatting.js b/packages/react-native-compatibility-check/src/ErrorFormatting.js new file mode 100644 index 00000000000..f19c796661a --- /dev/null +++ b/packages/react-native-compatibility-check/src/ErrorFormatting.js @@ -0,0 +1,309 @@ +/** + * 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 {TypeComparisonError} from './ComparisonResult'; +import type { + DiffSummary, + ErrorStore, + FormattedDiffSummary, + FormattedErrorStore, + FormattedIncompatible, + FormattedIncompatiblityReport, + NativeSpecErrorStore, +} from './DiffResults'; +import type {CompleteTypeAnnotation} from '@react-native/codegen/src/CodegenSchema'; + +function indentedLineStart(indent: number): string { + return '\n' + ' '.repeat(indent); +} + +export function formatErrorMessage( + error: TypeComparisonError, + indent: number = 0, +): string { + switch (error.type) { + case 'PropertyComparisonError': + const formattedProperties = error.mismatchedProperties.map( + individualPropertyError => + indentedLineStart(indent + 1) + + '-- ' + + individualPropertyError.property + + (individualPropertyError.fault + ? ': ' + + formatErrorMessage(individualPropertyError.fault, indent + 2) + : ''), + ); + return error.message + formattedProperties.join(''); + case 'PositionalComparisonError': + const formattedPositionalChanges = error.erroneousItems.map( + ([index, type]) => + indentedLineStart(indent + 1) + + '-- position ' + + index + + ' ' + + formatTypeAnnotation(type), + ); + return error.message + formattedPositionalChanges.join(''); + case 'TypeAnnotationComparisonError': + const previousError = error.previousError; + + return ( + error.message + + indentedLineStart(indent + 1) + + '--new: ' + + formatTypeAnnotation(error.newerAnnotation) + + indentedLineStart(indent + 1) + + '--old: ' + + formatTypeAnnotation(error.olderAnnotation) + + (previousError != null + ? indentedLineStart(indent + 1) + + '' + + formatErrorMessage(previousError, indent + 2) + : '') + ); + case 'TypeInformationComparisonError': + // I'm not sure that this error type is possible with the codegen + + return ( + error.message + + indentedLineStart(indent + 1) + + '-- new: ' + + formatTypeAnnotation(error.newerType) + + indentedLineStart(indent + 1) + + '-- old: ' + + formatTypeAnnotation(error.olderType) + ); + case 'MemberComparisonError': + const formattedMembers = error.mismatchedMembers.map( + individualMemberError => + indentedLineStart(indent + 1) + + '-- Member ' + + individualMemberError.member + + (individualMemberError.fault + ? ': ' + formatErrorMessage(individualMemberError.fault, indent + 2) + : ''), + ); + return error.message + formattedMembers.join(''); + default: + (error.type: empty); + return ''; + } +} + +function formatTypeAnnotation(annotation: CompleteTypeAnnotation): string { + switch (annotation.type) { + case 'AnyTypeAnnotation': + return 'any'; + case 'ArrayTypeAnnotation': + return 'Array<' + formatTypeAnnotation(annotation.elementType) + '>'; + case 'BooleanTypeAnnotation': + return 'boolean'; + case 'EnumDeclaration': { + let shortHandType = ''; + switch (annotation.memberType) { + case 'StringTypeAnnotation': + shortHandType = 'string'; + break; + case 'NumberTypeAnnotation': + shortHandType = 'number'; + break; + default: + (annotation.memberType: empty); + throw new Error('Unexpected enum memberType'); + } + + return `Enum<${shortHandType}>` + ''; + } + case 'EnumDeclarationWithMembers': { + let shortHandType = ''; + switch (annotation.memberType) { + case 'StringTypeAnnotation': + shortHandType = 'string'; + break; + case 'NumberTypeAnnotation': + shortHandType = 'number'; + break; + default: + (annotation.memberType: empty); + throw new Error('Unexptected enum memberType'); + } + + return ( + `Enum<${shortHandType}> {` + + annotation.members + .map( + member => `${member.name} = ${formatTypeAnnotation(member.value)}`, + ) + .join(', ') + + '}' + ); + } + case 'FunctionTypeAnnotation': + return ( + '(' + + annotation.params + .map( + param => + param.name + + (param.optional ? '?' : '') + + ': ' + + formatTypeAnnotation(param.typeAnnotation), + ) + .join(', ') + + ')' + + '=>' + + formatTypeAnnotation(annotation.returnTypeAnnotation) + ); + case 'NullableTypeAnnotation': + return '?' + formatTypeAnnotation(annotation.typeAnnotation); + case 'NumberTypeAnnotation': + return 'number'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'float'; + case 'Int32TypeAnnotation': + return 'int'; + case 'NumberLiteralTypeAnnotation': + return annotation.value.toString(); + case 'ObjectTypeAnnotation': + return ( + '{' + + annotation.properties + .map( + property => + property.name + + (property.optional ? '?' : '') + + ': ' + + formatTypeAnnotation(property.typeAnnotation), + ) + .join(', ') + + '}' + ); + case 'StringLiteralTypeAnnotation': + // If the string is a number, disambiguate from a number literal by adding quotes + // Other things are obviously strings so quotes unconditionally would just add noise + return parseInt(annotation.value, 10).toString() === annotation.value || + annotation.value.includes(' ') + ? `'${annotation.value}'` + : annotation.value; + case 'StringLiteralUnionTypeAnnotation': + return ( + '(' + + annotation.types + .map(stringLit => formatTypeAnnotation(stringLit)) + .join(' | ') + + ')' + ); + case 'StringTypeAnnotation': + return 'string'; + case 'UnionTypeAnnotation': { + const shortHandType = + annotation.memberType === 'StringTypeAnnotation' + ? 'string' + : annotation.memberType === 'ObjectTypeAnnotation' + ? 'Object' + : 'number'; + return `Union<${shortHandType}>`; + } + case 'PromiseTypeAnnotation': + return 'Promise<' + formatTypeAnnotation(annotation.elementType) + '>'; + case 'EventEmitterTypeAnnotation': + return ( + 'EventEmitter<' + formatTypeAnnotation(annotation.typeAnnotation) + '>' + ); + case 'TypeAliasTypeAnnotation': + case 'ReservedTypeAnnotation': + return annotation.name; + case 'VoidTypeAnnotation': + return 'void'; + case 'MixedTypeAnnotation': + return 'mixed'; + case 'GenericObjectTypeAnnotation': + if (annotation.dictionaryValueType) { + return `{[string]: ${formatTypeAnnotation(annotation.dictionaryValueType)}`; + } + + return 'Object'; + default: + (annotation.type: empty); + return JSON.stringify(annotation); + } +} + +export function formatErrorStore(errorStore: ErrorStore): FormattedErrorStore { + return { + message: + errorStore.typeName + + ': ' + + formatErrorMessage(errorStore.errorInformation), + errorCode: errorStore.errorCode, + }; +} + +export function formatNativeSpecErrorStore( + specError: NativeSpecErrorStore, +): Array { + if (specError.errorInformation) { + return [ + { + message: + specError.nativeSpecName + + ': ' + + formatErrorMessage(specError.errorInformation), + errorCode: specError.errorCode, + }, + ]; + } + + if (specError.changeInformation?.incompatibleChanges != null) { + return Array.from(specError.changeInformation.incompatibleChanges).map( + errorStore => formatErrorStore(errorStore), + ); + } + // changeInformation does not contain incompatible changes + return []; +} + +export function formatDiffSet(summary: DiffSummary): FormattedDiffSummary { + const summaryStatus = summary.status; + if (summaryStatus === 'ok' || summaryStatus === 'patchable') { + // $FlowFixMe I don't think we can ever get in this branch + return summary; + } + const hasteModules = Object.keys(summary.incompatibilityReport); + const incompatibles = summary.incompatibilityReport; + const formattedIncompatibilities: FormattedIncompatiblityReport = {}; + hasteModules.forEach(hasteModule => { + const incompat = incompatibles[hasteModule]; + const formattedIncompat: FormattedIncompatible = { + framework: incompat.framework, + }; + if (incompat.incompatibleSpecs) { + // nested errors + formattedIncompat.incompatibleSpecs = incompat.incompatibleSpecs.reduce( + ( + formattedModuleErrors: $ReadOnlyArray, + specErrorStore, + ) => + formattedModuleErrors.concat( + formatNativeSpecErrorStore(specErrorStore), + ), + [], + ); + } + + formattedIncompatibilities[hasteModule] = formattedIncompat; + }); + return { + status: (summaryStatus: 'incompatible'), + incompatibilityReport: formattedIncompatibilities, + }; +} diff --git a/packages/react-native-compatibility-check/src/SortTypeAnnotations.js b/packages/react-native-compatibility-check/src/SortTypeAnnotations.js new file mode 100644 index 00000000000..142581169de --- /dev/null +++ b/packages/react-native-compatibility-check/src/SortTypeAnnotations.js @@ -0,0 +1,290 @@ +/** + * 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 + */ + +import type {CompleteTypeAnnotation} from '@react-native/codegen/src/CodegenSchema'; + +import invariant from 'invariant'; + +export function sortTypeAnnotations( + annotations: $ReadOnlyArray, +): Array<[number, CompleteTypeAnnotation]> { + const sortableArray = annotations.map( + (a, i): [number, CompleteTypeAnnotation] => [i, a], + ); + return sortableArray.sort(compareTypeAnnotationForSorting); +} + +const EQUALITY_MSG = 'typeA and typeB differ despite check'; +export function compareTypeAnnotationForSorting( + [originalPositionA, typeA]: [number, CompleteTypeAnnotation], + [originalPositionB, typeB]: [number, CompleteTypeAnnotation], +): number { + if (typeA.type !== typeB.type) { + if (typeA.type === 'NullableTypeAnnotation') { + return compareTypeAnnotationForSorting( + [originalPositionA, typeA.typeAnnotation], + [originalPositionB, typeB], + ); + } + if (typeB.type === 'NullableTypeAnnotation') { + return compareTypeAnnotationForSorting( + [originalPositionA, typeA], + [originalPositionB, typeB.typeAnnotation], + ); + } + return ( + typeAnnotationArbitraryOrder(typeA) - typeAnnotationArbitraryOrder(typeB) + ); + } + switch (typeA.type) { + case 'AnyTypeAnnotation': + return 0; + case 'ArrayTypeAnnotation': + invariant(typeB.type === 'ArrayTypeAnnotation', EQUALITY_MSG); + return compareTypeAnnotationForSorting( + [originalPositionA, typeA.elementType], + [originalPositionB, typeB.elementType], + ); + case 'BooleanTypeAnnotation': + return originalPositionA - originalPositionB; + case 'EnumDeclaration': + invariant(typeB.type === 'EnumDeclaration', EQUALITY_MSG); + return typeA.memberType.localeCompare(typeB.memberType); + case 'EnumDeclarationWithMembers': + invariant(typeB.type === 'EnumDeclarationWithMembers', EQUALITY_MSG); + return compareNameAnnotationArraysForSorting( + [originalPositionA, typeA.members.map(m => [m.name, m.value])], + [originalPositionB, typeB.members.map(m => [m.name, m.value])], + ); + case 'FunctionTypeAnnotation': + invariant(typeB.type === 'FunctionTypeAnnotation', EQUALITY_MSG); + const parmComparison = compareAnnotationArraysForSorting( + [originalPositionA, typeA.params.map(p => p.typeAnnotation)], + [originalPositionB, typeB.params.map(p => p.typeAnnotation)], + ); + if (parmComparison === 0) { + return compareTypeAnnotationForSorting( + [originalPositionA, typeA.returnTypeAnnotation], + [originalPositionB, typeB.returnTypeAnnotation], + ); + } + return parmComparison; + case 'EventEmitterTypeAnnotation': + invariant(typeB.type === 'EventEmitterTypeAnnotation', EQUALITY_MSG); + + return compareTypeAnnotationForSorting( + [originalPositionA, typeA.typeAnnotation], + [originalPositionB, typeB.typeAnnotation], + ); + case 'GenericObjectTypeAnnotation': + invariant(typeB.type === 'GenericObjectTypeAnnotation', EQUALITY_MSG); + + if ( + typeA.dictionaryValueType == null && + typeB.dictionaryValueType == null + ) { + return 0; + } else if ( + typeA.dictionaryValueType != null && + typeB.dictionaryValueType != null + ) { + return compareTypeAnnotationForSorting( + [originalPositionA, typeA.dictionaryValueType], + [originalPositionB, typeB.dictionaryValueType], + ); + } else { + return typeA.dictionaryValueType == null ? -1 : 1; + } + case 'NullableTypeAnnotation': + invariant(typeB.type === 'NullableTypeAnnotation', EQUALITY_MSG); + return compareTypeAnnotationForSorting( + [originalPositionA, typeA.typeAnnotation], + [originalPositionB, typeB.typeAnnotation], + ); + case 'NumberTypeAnnotation': + case 'Int32TypeAnnotation': + case 'FloatTypeAnnotation': + case 'DoubleTypeAnnotation': + return 0; + case 'NumberLiteralTypeAnnotation': + invariant(typeB.type === 'NumberLiteralTypeAnnotation', EQUALITY_MSG); + return typeA.value - typeB.value; + case 'ObjectTypeAnnotation': + invariant(typeB.type === 'ObjectTypeAnnotation', EQUALITY_MSG); + return compareNameAnnotationArraysForSorting( + [ + originalPositionA, + typeA.properties.map(p => [p.name, p.typeAnnotation]), + ], + [ + originalPositionB, + typeB.properties.map(p => [p.name, p.typeAnnotation]), + ], + ); + case 'StringTypeAnnotation': + return originalPositionA - originalPositionB; + case 'StringLiteralTypeAnnotation': + invariant(typeB.type === 'StringLiteralTypeAnnotation', EQUALITY_MSG); + return typeA.value.localeCompare(typeB.value); + case 'StringLiteralUnionTypeAnnotation': + invariant( + typeB.type === 'StringLiteralUnionTypeAnnotation', + EQUALITY_MSG, + ); + return compareAnnotationArraysForSorting( + [originalPositionA, typeA.types], + [originalPositionB, typeB.types], + ); + case 'UnionTypeAnnotation': + invariant(typeB.type === 'UnionTypeAnnotation', EQUALITY_MSG); + return 0; + case 'VoidTypeAnnotation': + return 0; + case 'ReservedTypeAnnotation': + return 0; + case 'PromiseTypeAnnotation': + invariant(typeB.type === 'PromiseTypeAnnotation', EQUALITY_MSG); + return compareTypeAnnotationForSorting( + [originalPositionA, typeA.elementType], + [originalPositionB, typeB.elementType], + ); + case 'TypeAliasTypeAnnotation': + return 0; + case 'MixedTypeAnnotation': + return 0; + default: + (typeA.type: empty); + return -1; + } +} + +function nameComparison( + [nameA]: [string, CompleteTypeAnnotation], + [nameB]: [string, CompleteTypeAnnotation], +) { + if (nameA === nameB) { + return 0; + } else if (nameA < nameB) { + return -1; + } + return 1; +} + +function compareNameAnnotationArraysForSorting( + [originalPositionA, arrayA]: [ + number, + Array<[string, CompleteTypeAnnotation]>, + ], + [originalPositionB, arrayB]: [ + number, + Array<[string, CompleteTypeAnnotation]>, + ], +) { + if (arrayA.length - arrayB.length !== 0) { + return arrayA.length - arrayB.length; + } + const nameSortedA = arrayA.sort(nameComparison); + const nameSortedB = arrayB.sort(nameComparison); + for (let i = 0; i < nameSortedA.length; i++) { + if (nameSortedA[i][0] === nameSortedB[i][0]) { + const compared = compareTypeAnnotationForSorting( + [originalPositionA, nameSortedA[i][1]], + [originalPositionB, nameSortedB[i][1]], + ); + if (compared !== 0) { + return compared; + } + continue; + } + if (nameSortedA[i][0] < nameSortedB[i][0]) { + return -1; + } + return 1; + } + return 0; +} + +function compareAnnotationArraysForSorting( + [originalPositionA, arrayA]: [number, $ReadOnlyArray], + [originalPositionB, arrayB]: [number, $ReadOnlyArray], +) { + if (arrayA.length - arrayB.length !== 0) { + return arrayA.length - arrayB.length; + } + for (let i = 0; i < arrayA.length; i++) { + const compared = compareTypeAnnotationForSorting( + [originalPositionA, arrayA[i]], + [originalPositionB, arrayB[i]], + ); + if (compared !== 0) { + return compared; + } + continue; + } + return 0; +} + +function typeAnnotationArbitraryOrder(annotation: CompleteTypeAnnotation) { + switch (annotation.type) { + case 'AnyTypeAnnotation': + return 0; + case 'ArrayTypeAnnotation': + return 1; + case 'BooleanTypeAnnotation': + return 2; + case 'FunctionTypeAnnotation': + return 3; + case 'EventEmitterTypeAnnotation': + return 4; + case 'PromiseTypeAnnotation': + return 5; + case 'GenericObjectTypeAnnotation': + return 6; + case 'NullableTypeAnnotation': + return 9; + case 'NumberTypeAnnotation': + return 10; + case 'Int32TypeAnnotation': + return 11; + case 'DoubleTypeAnnotation': + return 12; + case 'FloatTypeAnnotation': + return 13; + case 'NumberLiteralTypeAnnotation': + return 14; + case 'ObjectTypeAnnotation': + return 15; + case 'StringLiteralUnionTypeAnnotation': + return 17; + case 'StringTypeAnnotation': + return 18; + case 'StringLiteralTypeAnnotation': + return 19; + case 'VoidTypeAnnotation': + return 20; + case 'EnumDeclaration': + return 21; + case 'EnumDeclarationWithMembers': + return 22; + case 'GenericObjectTypeAnnotation': + return 25; + case 'TypeAliasTypeAnnotation': + return 26; + case 'MixedTypeAnnotation': + return 27; + case 'ReservedTypeAnnotation': + return 28; + case 'UnionTypeAnnotation': + return 30; + default: + (annotation.type: empty); + return -1; + } +} diff --git a/packages/react-native-compatibility-check/src/TypeDiffing.js b/packages/react-native-compatibility-check/src/TypeDiffing.js new file mode 100644 index 00000000000..abd3310fe1d --- /dev/null +++ b/packages/react-native-compatibility-check/src/TypeDiffing.js @@ -0,0 +1,1427 @@ +/** + * 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 { + ComparisonResult, + FunctionComparisonResult, + MembersComparisonResult, + PropertiesComparisonResult, + TypeComparisonError, +} from './ComparisonResult'; +import type { + CompleteReservedTypeAnnotation, + CompleteTypeAnnotation, + EventEmitterTypeAnnotation, + NamedShape, + NativeModuleAliasMap, + NativeModuleEnumDeclaration, + NativeModuleEnumDeclarationWithMembers, + NativeModuleEnumMap, + NativeModuleEnumMember, + NativeModuleFunctionTypeAnnotation, + NativeModuleGenericObjectTypeAnnotation, + NativeModuleObjectTypeAnnotation, + NativeModulePromiseTypeAnnotation, + NativeModuleTypeAnnotation, + NativeModuleUnionTypeAnnotation, + NullableTypeAnnotation, + NumberLiteralTypeAnnotation, + ObjectTypeAnnotation, + StringLiteralTypeAnnotation, + StringLiteralUnionTypeAnnotation, +} from '@react-native/codegen/src/CodegenSchema'; + +import { + isFunctionLogEmpty, + isMemberLogEmpty, + isPropertyLogEmpty, + makeError, + memberComparisonError, + propertyComparisonError, + typeAnnotationComparisonError, +} from './ComparisonResult'; +import { + compareTypeAnnotationForSorting, + sortTypeAnnotations, +} from './SortTypeAnnotations.js'; +import invariant from 'invariant'; + +const EQUALITY_MSG = 'previousType and afterType differ despite check'; + +// NOTE: These are module scope type lookup registries. Ideally, they should be local arguments, +// however that would require threading them through all the hierarchical calls of type +// annotation processing, which is a lot of boilerplate. Since our logic is serial, having these +// as shared global variables is acceptable. +let _newerTypesReg, _olderTypesReg, _newerEnumMap, _olderEnumMap; + +export function compareTypes( + newerType: CompleteTypeAnnotation, + olderType: ?CompleteTypeAnnotation, + newerTypesReg: ?NativeModuleAliasMap, + olderTypesReg: ?NativeModuleAliasMap, + newerEnumMap: ?NativeModuleEnumMap, + olderEnumMap: ?NativeModuleEnumMap, +): ComparisonResult { + if (!olderType) { + // No matching olderType for newerType, skip the Comparison. + return {status: 'skipped'}; + } + + _newerTypesReg = newerTypesReg; + _olderTypesReg = olderTypesReg; + _newerEnumMap = newerEnumMap; + _olderEnumMap = olderEnumMap; + + const res = compareTypeAnnotation(newerType, olderType); + + _newerTypesReg = undefined; + _olderTypesReg = undefined; + _newerEnumMap = undefined; + _olderEnumMap = undefined; + + return res; +} + +// ??T is the same as ?T so let's remove any superflous annotations +function removeNullableTypeAnnotations( + annotation: + | NativeModuleTypeAnnotation + | NullableTypeAnnotation, +): NativeModuleTypeAnnotation { + // The parser doesn't allow nested nullables + if (annotation.type === 'NullableTypeAnnotation') { + return removeNullableTypeAnnotations(annotation.typeAnnotation); + } + + return annotation; +} + +function lookupType( + name: string, + aliases: ?NativeModuleAliasMap, +): ?NativeModuleObjectTypeAnnotation { + return aliases?.[name]; +} + +function lookupEnum( + name: string, + enums: ?NativeModuleEnumMap, +): ?NativeModuleEnumDeclarationWithMembers { + return enums?.[name]; +} + +export function compareTypeAnnotation( + originalNewerAnnotation: CompleteTypeAnnotation, + originalOlderAnnotation: CompleteTypeAnnotation, +): ComparisonResult { + const newerAnnotation = originalNewerAnnotation; + const olderAnnotation = originalOlderAnnotation; + + // Consider type aliases (generic type annotations with no type parameters) to be compatible + // with the other type, if the underlying type definition is structurally the same + if (newerAnnotation.type === 'TypeAliasTypeAnnotation') { + const newerAnnotationDefinition = lookupType( + newerAnnotation.name, + _newerTypesReg, + ); + if (newerAnnotationDefinition != null) { + return compareTypeAnnotation(newerAnnotationDefinition, olderAnnotation); + } + } + + if (olderAnnotation.type === 'TypeAliasTypeAnnotation') { + const olderAnnotationDefinition = lookupType( + olderAnnotation.name, + _olderTypesReg, + ); + if (olderAnnotationDefinition != null) { + return compareTypeAnnotation(newerAnnotation, olderAnnotationDefinition); + } + } + + invariant( + newerAnnotation.type !== 'TypeAliasTypeAnnotation' && + olderAnnotation.type !== 'TypeAliasTypeAnnotation', + EQUALITY_MSG, + ); + + if (newerAnnotation.type !== olderAnnotation.type) { + if ( + newerAnnotation.type === 'NullableTypeAnnotation' || + olderAnnotation.type === 'NullableTypeAnnotation' + ) { + return compareNullableChange(newerAnnotation, olderAnnotation); + } + + return makeError( + typeAnnotationComparisonError( + 'Type annotations are not the same.', + newerAnnotation, + olderAnnotation, + ), + ); + } + + switch (newerAnnotation.type) { + case 'AnyTypeAnnotation': + case 'MixedTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'Int32TypeAnnotation': + case 'BooleanTypeAnnotation': + case 'NumberTypeAnnotation': + case 'StringTypeAnnotation': + case 'VoidTypeAnnotation': + return {status: 'matching'}; + case 'ArrayTypeAnnotation': + invariant(olderAnnotation.type === 'ArrayTypeAnnotation', EQUALITY_MSG); + return compareTypeAnnotation( + newerAnnotation.elementType, + olderAnnotation.elementType, + ); + case 'EnumDeclaration': + invariant(olderAnnotation.type === 'EnumDeclaration', EQUALITY_MSG); + return compareEnumDeclarations(newerAnnotation, olderAnnotation); + case 'EnumDeclarationWithMembers': + invariant( + olderAnnotation.type === 'EnumDeclarationWithMembers', + EQUALITY_MSG, + ); + return compareEnumDeclarationWithMembers( + newerAnnotation, + olderAnnotation, + ); + case 'FunctionTypeAnnotation': + invariant( + olderAnnotation.type === 'FunctionTypeAnnotation', + EQUALITY_MSG, + ); + return compareFunctionTypes(newerAnnotation, olderAnnotation); + case 'PromiseTypeAnnotation': + invariant(olderAnnotation.type === 'PromiseTypeAnnotation', EQUALITY_MSG); + + return comparePromiseTypes(newerAnnotation, olderAnnotation); + case 'GenericObjectTypeAnnotation': + invariant( + olderAnnotation.type === 'GenericObjectTypeAnnotation', + EQUALITY_MSG, + ); + + return compareGenericObjectTypes(newerAnnotation, olderAnnotation); + case 'NullableTypeAnnotation': + invariant( + olderAnnotation.type === 'NullableTypeAnnotation', + EQUALITY_MSG, + ); + return compareTypeAnnotation( + newerAnnotation.typeAnnotation, + olderAnnotation.typeAnnotation, + ); + case 'ObjectTypeAnnotation': + invariant(olderAnnotation.type === 'ObjectTypeAnnotation', EQUALITY_MSG); + return compareObjectTypes( + newerAnnotation.properties, + olderAnnotation.properties, + ); + case 'NumberLiteralTypeAnnotation': + invariant( + olderAnnotation.type === 'NumberLiteralTypeAnnotation', + EQUALITY_MSG, + ); + return compareNumberLiteralTypes(newerAnnotation, olderAnnotation); + case 'StringLiteralUnionTypeAnnotation': + invariant( + olderAnnotation.type === 'StringLiteralUnionTypeAnnotation', + EQUALITY_MSG, + ); + return compareStringLiteralUnionTypes(newerAnnotation, olderAnnotation); + case 'StringLiteralTypeAnnotation': + invariant( + olderAnnotation.type === 'StringLiteralTypeAnnotation', + EQUALITY_MSG, + ); + return compareStringLiteralTypes(newerAnnotation, olderAnnotation); + case 'UnionTypeAnnotation': + invariant(olderAnnotation.type === 'UnionTypeAnnotation', EQUALITY_MSG); + return compareUnionTypes(newerAnnotation, olderAnnotation); + case 'EventEmitterTypeAnnotation': + invariant( + olderAnnotation.type === 'EventEmitterTypeAnnotation', + EQUALITY_MSG, + ); + + return compareEventEmitterTypes(newerAnnotation, olderAnnotation); + case 'ReservedTypeAnnotation': + invariant( + olderAnnotation.type === 'ReservedTypeAnnotation', + EQUALITY_MSG, + ); + + return compareReservedTypeAnnotation(newerAnnotation, olderAnnotation); + default: // Flow exhaustiveness check + (newerAnnotation: empty); + throw new Error(`Unsupported type annotation: ${newerAnnotation.type}`); + } +} + +function compareObjectTypeProperty( + first: NamedShape, + second: NamedShape, +): number { + if (first.name < second.name) { + return -1; + } else if (first.name > second.name) { + return 1; + } + return 0; +} + +function compareEnumMember( + first: NativeModuleEnumMember, + second: NativeModuleEnumMember, +) { + if (first.name < second.name) { + return -1; + } else if (first.name > second.name) { + return 1; + } + return 0; +} + +function updatePropertyError( + name: string, + newType: CompleteTypeAnnotation, + oldType: CompleteTypeAnnotation, + result: PropertiesComparisonResult, +) { + return (oldError: TypeComparisonError) => { + const comparisonError = typeAnnotationComparisonError( + 'has conflicting type changes', + newType, + oldType, + oldError, + ); + const newFault = {property: name, fault: comparisonError}; + if (result.errorProperties) { + result.errorProperties.push(newFault); + } else { + result.errorProperties = [newFault]; + } + }; +} + +function updateEnumMemberError( + name: string, + newType: CompleteTypeAnnotation, + oldType: CompleteTypeAnnotation, + result: MembersComparisonResult, +) { + return (oldError: TypeComparisonError) => { + const comparisonError = typeAnnotationComparisonError( + 'has conflicting changes', + newType, + oldType, + oldError, + ); + const newFault = {member: name, fault: comparisonError}; + if (result.errorMembers) { + result.errorMembers.push(newFault); + } else { + result.errorMembers = [newFault]; + } + }; +} + +function updateNestedProperties( + name: string, + propertyChange: ComparisonResult, + result: PropertiesComparisonResult, +) { + if (result.nestedPropertyChanges) { + result.nestedPropertyChanges.push([name, propertyChange]); + } else { + result.nestedPropertyChanges = [[name, propertyChange]]; + } +} + +function updateMadeOptional( + name: string, + result: PropertiesComparisonResult, + furtherChange?: ComparisonResult, +) { + if (result.madeOptional) { + result.madeOptional.push({property: name, furtherChange}); + } else { + result.madeOptional = [{property: name, furtherChange}]; + } +} + +function updateMadeStrict( + name: string, + result: PropertiesComparisonResult, + furtherChange?: ComparisonResult, +) { + if (result.madeStrict) { + result.madeStrict.push({property: name, furtherChange}); + } else { + result.madeStrict = [{property: name, furtherChange}]; + } +} + +function checkOptionalityChanges( + name: string, + newOptionality: boolean, + oldOptionality: boolean, + result: PropertiesComparisonResult, + furtherChange?: ComparisonResult, +): PropertiesComparisonResult { + if (newOptionality === oldOptionality) { + if (furtherChange) { + updateNestedProperties(name, furtherChange, result); + } + return result; + } + if (newOptionality) { + updateMadeOptional(name, result, furtherChange); + } else { + updateMadeStrict(name, result, furtherChange); + } + return result; +} + +function comparePropertyArrays( + newerOriginal: $ReadOnlyArray>, + olderOriginal: $ReadOnlyArray>, +): PropertiesComparisonResult { + const newer = newerOriginal.slice(0); + const older = olderOriginal.slice(0); + if (newer.length === 0 && older.length === 0) { + return {}; + } + if (newer.length === 0) { + return { + missingProperties: older, + }; + } + if (older.length === 0) { + return { + addedProperties: newer, + }; + } + const newerHead = newer.pop(); + const olderHead = older.pop(); + invariant(newerHead != null && olderHead != null, 'Array is empty'); + + const newerName = newerHead.name; + const olderName = olderHead.name; + if (newerName === olderName) { + const comparedTypes = compareTypeAnnotation( + newerHead.typeAnnotation, + + olderHead.typeAnnotation, + ); + const result = comparePropertyArrays(newer, older); + switch (comparedTypes.status) { + case 'matching': + return checkOptionalityChanges( + newerName, + newerHead.optional, + olderHead.optional, + result, + ); + case 'skipped': + throw new Error( + "Internal error: returned 'skipped' for non-optional older type", + ); + case 'nullableChange': + return checkOptionalityChanges( + newerName, + !comparedTypes.nullableLog.optionsReduced, + comparedTypes.nullableLog.optionsReduced, + result, + ); + case 'members': + case 'properties': + case 'functionChange': + case 'positionalTypeChange': + return checkOptionalityChanges( + newerName, + newerHead.optional, + olderHead.optional, + result, + comparedTypes, + ); + case 'error': + updatePropertyError( + newerName, + newerHead.typeAnnotation, + olderHead.typeAnnotation, + result, + )(comparedTypes.errorLog); + return result; + default: + (comparedTypes: empty); + throw new Error('Unsupported status ' + comparedTypes.status); + } + } + // newer property must have been added based on sorting + if (newerName > olderName) { + older.push(olderHead); + const result = comparePropertyArrays(newer, older); + if (result.hasOwnProperty('addedProperties') && result.addedProperties) { + result.addedProperties = result.addedProperties.concat([newerHead]); + } else { + result.addedProperties = [newerHead]; + } + return result; + } + // older property must have been skipped based on sorting + newer.push(newerHead); + const result = comparePropertyArrays(newer, older); + if (result.hasOwnProperty('missingProperties') && result.missingProperties) { + result.missingProperties = result.missingProperties.concat([olderHead]); + } else { + result.missingProperties = [olderHead]; + } + return result; +} + +export function compareObjectTypes( + newerPropertyTypes: $ReadOnlyArray>, + olderPropertyTypes: $ReadOnlyArray>, +): ComparisonResult { + if (newerPropertyTypes.length === 0 && olderPropertyTypes.length === 0) { + return {status: 'matching'}; + } + const sortedNewerTypes = []; + newerPropertyTypes.forEach(prop => sortedNewerTypes.push(prop)); + if (sortedNewerTypes.length !== 0) { + sortedNewerTypes.sort(compareObjectTypeProperty); + } + const sortedOlderTypes = []; + olderPropertyTypes.forEach(prop => sortedOlderTypes.push(prop)); + if (sortedOlderTypes.length !== 0) { + sortedOlderTypes.sort(compareObjectTypeProperty); + } + + if (sortedNewerTypes.length === 0) { + return { + status: 'properties', + propertyLog: {missingProperties: sortedOlderTypes}, + }; + } + if (sortedOlderTypes.length === 0) { + return { + status: 'properties', + propertyLog: {addedProperties: sortedNewerTypes}, + }; + } + const result = comparePropertyArrays(sortedNewerTypes, sortedOlderTypes); + if (isPropertyLogEmpty(result)) { + return {status: 'matching'}; + } + if (result.errorProperties) { + return makeError( + propertyComparisonError( + result.errorProperties.length > 1 + ? 'Object contained properties with type mismatches' + : 'Object contained a property with a type mismatch', + result.errorProperties, + ), + ); + } + if ( + (result.addedProperties && + result.addedProperties.length > 0 && + result.addedProperties.length === newerPropertyTypes.length) || + (result.missingProperties && + result.missingProperties.length > 0 && + result.missingProperties.length === olderPropertyTypes.length) + ) { + return makeError( + typeAnnotationComparisonError( + 'Object types do not match.', + // $FlowFixMe[incompatible-call] + objectTypeAnnotation(newerPropertyTypes), + // $FlowFixMe[incompatible-call] + objectTypeAnnotation(olderPropertyTypes), + ), + ); + } + return {status: 'properties', propertyLog: result}; +} + +function objectTypeAnnotation( + properties: $ReadOnlyArray>, +): ObjectTypeAnnotation { + return { + type: 'ObjectTypeAnnotation', + properties, + baseTypes: [], + }; +} + +export function compareEnumDeclarations( + newerDeclaration: NativeModuleEnumDeclaration, + olderDeclaration: NativeModuleEnumDeclaration, +): ComparisonResult { + if (newerDeclaration.memberType !== olderDeclaration.memberType) { + return makeError( + typeAnnotationComparisonError( + 'EnumDeclaration member types are not the same', + newerDeclaration, + olderDeclaration, + ), + ); + } + + const newerAnnotationDefinition = lookupEnum( + newerDeclaration.name, + _newerEnumMap, + ); + const olderAnnotationDefinition = lookupEnum( + olderDeclaration.name, + _olderEnumMap, + ); + + invariant( + newerAnnotationDefinition != null && olderAnnotationDefinition != null, + 'Could not find enum definition', + ); + + return compareTypeAnnotation( + newerAnnotationDefinition, + olderAnnotationDefinition, + ); +} + +export function compareEnumDeclarationMemberArrays( + newer: Array, + older: Array, +): MembersComparisonResult { + if (newer.length === 0 && older.length === 0) { + return {}; + } else if (newer.length === 0) { + return {missingMembers: older}; + } else if (older.length === 0) { + return {addedMembers: newer}; + } + + const newerHead = newer.pop(); + const olderHead = older.pop(); + invariant(newerHead != null && olderHead != null, 'Array is empty'); + + const newerName = newerHead.name; + const olderName = olderHead.name; + + if (newerName === olderName) { + const comparedTypes = compareTypeAnnotation( + newerHead.value, + olderHead.value, + ); + + const result = compareEnumDeclarationMemberArrays(newer, older); + switch (comparedTypes.status) { + case 'matching': + return result; + case 'error': + updateEnumMemberError( + newerName, + newerHead.value, + olderHead.value, + result, + )(comparedTypes.errorLog); + return result; + case 'skipped': + throw new Error( + "Internal error: returned 'skipped' for non-optional older type", + ); + case 'nullableChange': + case 'properties': + case 'functionChange': + case 'positionalTypeChange': + case 'members': + break; + default: // Flow exhaustiveness check + (comparedTypes: empty); + throw new Error('Unsupported status ' + comparedTypes.status); + } + } else if (newerName > olderName) { + older.push(olderHead); + const result = compareEnumDeclarationMemberArrays(newer, older); + if (result.hasOwnProperty('addedMembers') && result.addedMembers) { + result.addedMembers.push(newerHead); + } else { + result.addedMembers = [newerHead]; + } + return result; + } else if (newerName < olderName) { + newer.push(newerHead); + const result = compareEnumDeclarationMemberArrays(newer, older); + if (result.hasOwnProperty('missingMembers') && result.missingMembers) { + result.missingMembers.push(olderHead); + } else { + result.missingMembers = [olderHead]; + } + return result; + } + + throw new Error('Internal error: should not reach here'); +} + +export function compareEnumDeclarationWithMembers( + newerDeclaration: NativeModuleEnumDeclarationWithMembers, + olderDeclaration: NativeModuleEnumDeclarationWithMembers, +): ComparisonResult { + const sortedNewerTypes = Array.from(newerDeclaration.members).sort( + compareEnumMember, + ); + const sortedOlderTypes = Array.from(olderDeclaration.members).sort( + compareEnumMember, + ); + + const result = compareEnumDeclarationMemberArrays( + sortedNewerTypes, + sortedOlderTypes, + ); + + if (isMemberLogEmpty(result)) { + return {status: 'matching'}; + } else if (result.errorMembers) { + return makeError( + typeAnnotationComparisonError( + 'Enum types do not match', + newerDeclaration, + olderDeclaration, + memberComparisonError( + result.errorMembers.length > 1 + ? 'Enum contained members with type mismatches' + : 'Enum contained a member with a type mismatch', + result.errorMembers, + ), + ), + ); + } else if ( + (result.addedMembers && + result.addedMembers.length > 0 && + result.addedMembers.length === newerDeclaration.members.length) || + (result.missingMembers && + result.missingMembers.length > 0 && + result.missingMembers.length === olderDeclaration.members.length) + ) { + return makeError( + typeAnnotationComparisonError( + 'Enum types do not match.', + newerDeclaration, + olderDeclaration, + ), + ); + } + + return {status: 'members', memberLog: result}; +} + +function compareNullableChange( + newerAnnotation: CompleteTypeAnnotation, + olderAnnotation: CompleteTypeAnnotation, +): ComparisonResult { + const newVoidRemoved = + newerAnnotation.type === 'NullableTypeAnnotation' + ? removeNullableTypeAnnotations(newerAnnotation) + : newerAnnotation; + + const oldVoidRemoved = + olderAnnotation.type === 'NullableTypeAnnotation' + ? removeNullableTypeAnnotations(olderAnnotation) + : olderAnnotation; + + const optionalNew = newVoidRemoved.type !== newerAnnotation.type; + const optionalOld = oldVoidRemoved.type !== olderAnnotation.type; + + invariant( + optionalNew !== optionalOld, + 'compareNullableChange called with both being nullable', + ); + + const optionsReduced = !optionalNew && optionalOld; + if ( + newVoidRemoved.type === 'VoidTypeAnnotation' || + oldVoidRemoved.type === 'VoidTypeAnnotation' + ) { + return { + status: 'nullableChange', + nullableLog: { + typeRefined: true, + optionsReduced, + interiorLog: null, + newType: newerAnnotation, + oldType: olderAnnotation, + }, + }; + } + const interiorLog = compareTypeAnnotation(newVoidRemoved, oldVoidRemoved); + switch (interiorLog.status) { + case 'error': + return makeError( + typeAnnotationComparisonError( + 'Type annotations are not the same.', + newerAnnotation, + olderAnnotation, + ), + ); + case 'matching': + return { + status: 'nullableChange', + nullableLog: { + typeRefined: false, + optionsReduced, + interiorLog, + newType: newerAnnotation, + oldType: olderAnnotation, + }, + }; + default: + return { + status: 'nullableChange', + nullableLog: { + typeRefined: false, + optionsReduced, + interiorLog, + newType: newerAnnotation, + oldType: olderAnnotation, + }, + }; + } +} + +export function compareUnionTypes( + newerType: NativeModuleUnionTypeAnnotation, + olderType: NativeModuleUnionTypeAnnotation, +): ComparisonResult { + if (newerType.memberType !== olderType.memberType) { + return makeError( + typeAnnotationComparisonError( + 'Union member type does not match', + newerType, + olderType, + ), + ); + } + + return {status: 'matching'}; +} + +export function comparePromiseTypes( + newerType: NativeModulePromiseTypeAnnotation, + olderType: NativeModulePromiseTypeAnnotation, +): ComparisonResult { + if (newerType.elementType == null || olderType.elementType == null) { + return makeError( + typeAnnotationComparisonError( + 'Promise has differing arguments', + newerType, + olderType, + ), + ); + } + + invariant( + newerType.elementType != null && olderType.elementType != null, + EQUALITY_MSG, + ); + + return compareTypeAnnotation(newerType.elementType, olderType.elementType); +} + +export function compareGenericObjectTypes( + newerType: NativeModuleGenericObjectTypeAnnotation, + olderType: NativeModuleGenericObjectTypeAnnotation, +): ComparisonResult { + if ( + newerType.dictionaryValueType == null && + olderType.dictionaryValueType == null + ) { + return {status: 'matching'}; + } + + if ( + newerType.dictionaryValueType != null && + olderType.dictionaryValueType != null + ) { + return compareTypeAnnotation( + newerType.dictionaryValueType, + olderType.dictionaryValueType, + ); + } + + return makeError( + typeAnnotationComparisonError( + 'Generic Object types do not have matching dictionary types', + newerType, + olderType, + ), + ); +} + +export function compareNumberLiteralTypes( + newerType: NumberLiteralTypeAnnotation, + olderType: NumberLiteralTypeAnnotation, +): ComparisonResult { + return newerType.value === olderType.value + ? {status: 'matching'} + : makeError( + typeAnnotationComparisonError( + 'Numeric literals are not equal', + newerType, + olderType, + ), + ); +} + +export function compareStringLiteralTypes( + newerType: StringLiteralTypeAnnotation, + olderType: StringLiteralTypeAnnotation, +): ComparisonResult { + return newerType.value === olderType.value + ? {status: 'matching'} + : makeError( + typeAnnotationComparisonError( + 'String literals are not equal', + newerType, + olderType, + ), + ); +} + +export function compareStringLiteralUnionTypes( + newerType: StringLiteralUnionTypeAnnotation, + olderType: StringLiteralUnionTypeAnnotation, +): ComparisonResult { + const results = compareArrayOfTypes( + true, // Fixed order + false, // Can grow/shrink at the end + newerType.types, + olderType.types, + ); + switch (results.status) { + case 'length-mismatch': + throw new Error('length-mismatch returned with length changes allowed'); + case 'type-mismatch': + return makeError( + typeAnnotationComparisonError( + `Subtype of union at position ${results.newIndex} did not match`, + newerType, + olderType, + results.error, + ), + ); + case 'subtypable-changes': + if (results.nestedChanges.length > 0) { + throw new Error( + 'Unexpected inline objects/functions in string literal union', + ); + } + if (results.addedElements.length > 0) { + return { + status: 'positionalTypeChange', + changeLog: { + typeKind: 'stringUnion', + nestedChanges: [], + addedElements: results.addedElements, + }, + }; + } + if (results.removedElements.length > 0) { + return { + status: 'positionalTypeChange', + changeLog: { + typeKind: 'stringUnion', + nestedChanges: [], + removedElements: results.removedElements, + }, + }; + } + console.log(JSON.stringify(results)); + throw new Error('string union returned unexpected set of changes'); + case 'matching': + return {status: 'matching'}; + default: + throw new Error('Unknown status'); + } +} + +export function compareFunctionTypes( + newerType: NativeModuleFunctionTypeAnnotation, + olderType: NativeModuleFunctionTypeAnnotation, +): ComparisonResult { + // Check return types + const returnTypeResult = compareTypeAnnotation( + newerType.returnTypeAnnotation, + olderType.returnTypeAnnotation, + ); + if (returnTypeResult.status === 'error') { + return makeError( + typeAnnotationComparisonError( + 'Function return types do not match', + newerType, + olderType, + returnTypeResult.errorLog, + ), + ); + } + const functionChanges: FunctionComparisonResult = {}; + if ( + returnTypeResult.status === 'properties' || + returnTypeResult.status === 'members' || + returnTypeResult.status === 'functionChange' || + returnTypeResult.status === 'positionalTypeChange' || + returnTypeResult.status === 'nullableChange' + ) { + functionChanges.returnType = returnTypeResult; + } + + // Check argument types + const argumentResults = compareArrayOfTypes( + true, // fixedOrder + true, // fixedLength + newerType.params.map(_ => _.typeAnnotation), + olderType.params.map(_ => _.typeAnnotation), + ); + switch (argumentResults.status) { + case 'length-mismatch': + return makeError( + typeAnnotationComparisonError( + 'Function types have differing length of arguments', + newerType, + olderType, + ), + ); + case 'type-mismatch': + return makeError( + typeAnnotationComparisonError( + `Parameter at index ${argumentResults.newIndex} did not match`, + newerType, + olderType, + argumentResults.error, + ), + ); + case 'subtypable-changes': + functionChanges.parameterTypes = { + typeKind: 'parameter', + nestedChanges: argumentResults.nestedChanges, + }; + break; + case 'matching': + default: + break; + } + + if (isFunctionLogEmpty(functionChanges)) { + return {status: 'matching'}; + } + return {status: 'functionChange', functionChangeLog: functionChanges}; +} + +type ArrayComparisonResult = + | {status: 'matching', ...} + | {status: 'length-mismatch', ...} + | { + status: 'subtypable-changes', + nestedChanges: Array<[number, number, ComparisonResult]>, + addedElements: Array<[number, CompleteTypeAnnotation]>, + removedElements: Array<[number, CompleteTypeAnnotation]>, + ... + } + | { + status: 'type-mismatch', + error: TypeComparisonError, + newIndex: number, + oldIndex: number, + ... + }; + +function compareArrayOfTypes( + fixedOrder: boolean, + fixedLength: boolean, + newerTypes: $ReadOnlyArray, + olderTypes: $ReadOnlyArray, +): ArrayComparisonResult { + const sameLength = newerTypes.length === olderTypes.length; + if (fixedLength && !sameLength) { + return {status: 'length-mismatch'}; + } + const nestedChanges: Array<[number, number, ComparisonResult]> = []; + const minLength = Math.min(newerTypes.length, olderTypes.length); + + if (fixedOrder) { + for (let i = 0; i < minLength; i++) { + const result = compareTypeAnnotation(newerTypes[i], olderTypes[i]); + if (result.status === 'error') { + return { + status: 'type-mismatch', + error: result.errorLog, + newIndex: i, + oldIndex: i, + }; + } + if ( + result.status === 'properties' || + result.status === 'members' || + result.status === 'functionChange' || + result.status === 'positionalTypeChange' || + result.status === 'nullableChange' + ) { + nestedChanges.push([i, i, result]); + } + } + if (nestedChanges.length === 0 && sameLength) { + return {status: 'matching'}; + } + const addedElements: Array<[number, CompleteTypeAnnotation]> = []; + const removedElements: Array<[number, CompleteTypeAnnotation]> = []; + if (newerTypes.length < olderTypes.length) { + const elements = olderTypes.slice(minLength, olderTypes.length); + for (let i = 0; i < elements.length; i++) { + removedElements.push([i + minLength + 1, elements[i]]); + } + } + if (newerTypes.length > olderTypes.length) { + const elements = newerTypes.slice(minLength, newerTypes.length); + for (let i = 0; i < elements.length; i++) { + addedElements.push([i + minLength + 1, elements[i]]); + } + } + return { + status: 'subtypable-changes', + nestedChanges, + addedElements, + removedElements, + }; + } + return compareArrayTypesOutOfOrder( + sortTypeAnnotations(newerTypes), + 0, + sortTypeAnnotations(olderTypes), + 0, + [], + [], + [], + ); +} + +type PotentialErrorOrChange = { + olderPosition: number, + newerPosition: number, + error: TypeComparisonError, + annotation: CompleteTypeAnnotation, + ... +}; + +function compareArrayTypesOutOfOrder( + newerTypes: Array<[number, CompleteTypeAnnotation]>, + newerIndex: number, + olderTypes: Array<[number, CompleteTypeAnnotation]>, + olderIndex: number, + potentiallyAddedElements: Array, + potentiallyRemovedElements: Array, + nestedChanges: Array<[number, number, ComparisonResult]>, +): ArrayComparisonResult { + const newLength = newerTypes.length; + const oldLength = olderTypes.length; + if (newerIndex === newLength || olderIndex === oldLength) { + const [errors, added, removed] = resolvePotentials( + potentiallyAddedElements, + potentiallyRemovedElements, + ); + if (errors.length !== 0) { + // Return the first error, others might be dubious + return { + status: 'type-mismatch', + error: errors[0][0], + oldIndex: errors[0][1], + newIndex: errors[0][2], + }; + } + if ( + added.length === 0 && + removed.length === 0 && + nestedChanges.length === 0 && + newerIndex === newLength && + olderIndex === oldLength + ) { + return {status: 'matching'}; + } + if (newerIndex === newLength && olderIndex === oldLength) { + return { + status: 'subtypable-changes', + nestedChanges, + addedElements: added, + removedElements: removed, + }; + } + if (newerIndex === newLength) { + return { + status: 'subtypable-changes', + nestedChanges, + addedElements: added, + removedElements: removed.concat( + olderTypes.slice(olderIndex, oldLength), + ), + }; + } + // By elimination (olderIndex === oldLength) + return { + status: 'subtypable-changes', + nestedChanges, + addedElements: added.concat(newerTypes.slice(newerIndex, newLength)), + removedElements: removed, + }; + } + const newTypePosn = newerTypes[newerIndex][0]; + const newType = newerTypes[newerIndex][1]; + const oldTypePosn = olderTypes[olderIndex][0]; + const oldType = olderTypes[olderIndex][1]; + const currentResult = compareTypeAnnotation(newType, oldType); + const sortComparison = compareTypeAnnotationForSorting( + newerTypes[newerIndex], + olderTypes[olderIndex], + ); + switch (currentResult.status) { + case 'matching': + return compareArrayTypesOutOfOrder( + newerTypes, + newerIndex + 1, + olderTypes, + olderIndex + 1, + potentiallyAddedElements, + potentiallyRemovedElements, + nestedChanges, + ); + case 'properties': + case 'functionChange': + case 'positionalTypeChange': + case 'nullableChange': + // The match was sufficiently close so move to the next items + return compareArrayTypesOutOfOrder( + newerTypes, + newerIndex + 1, + olderTypes, + olderIndex + 1, + potentiallyAddedElements, + potentiallyRemovedElements, + // I'm not sure how I should fix this FlowFixMe. It returns `undefined` + // and has existed since the day it was written. Fixing it might break things. + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + nestedChanges.concat[[oldTypePosn, newTypePosn, currentResult]], + ); + case 'error': + // This case seems impossible, but if it happens there was ceratinly an error + if (sortComparison === 0) { + return { + status: 'type-mismatch', + error: currentResult.errorLog, + newIndex: newTypePosn, + oldIndex: oldTypePosn, + }; + } + // A matching newerType should have appeared in the sorted list by now + // So either added or an error + if (sortComparison < 0) { + return compareArrayTypesOutOfOrder( + newerTypes, + newerIndex + 1, + olderTypes, + olderIndex, + potentiallyAddedElements.concat([ + { + olderPosition: oldTypePosn, + newerPosition: newTypePosn, + error: currentResult.errorLog, + annotation: newType, + }, + ]), + potentiallyRemovedElements, + nestedChanges, + ); + } + // Else a matching olderType should have appeared in the sorted arrays + // So either error or removed + return compareArrayTypesOutOfOrder( + newerTypes, + newerIndex, + olderTypes, + olderIndex + 1, + potentiallyAddedElements, + potentiallyRemovedElements.concat([ + { + olderPosition: oldTypePosn, + newerPosition: newTypePosn, + error: currentResult.errorLog, + annotation: oldType, + }, + ]), + nestedChanges, + ); + case 'skipped': + throw new Error( + 'Unexpected skipped status for array of type annotations', + ); + default: + throw new Error('Unsupported status ' + currentResult.status); + } +} + +function resolvePotentials( + potentiallyAdded: Array, + potentiallyRemoved: Array, +): [ + Array<[TypeComparisonError, number, number]>, + Array<[number, CompleteTypeAnnotation]>, + Array<[number, CompleteTypeAnnotation]>, +] { + const addedLength = potentiallyAdded.length; + const removedLength = potentiallyRemoved.length; + if (addedLength === 0 && removedLength === 0) { + return [[], [], []]; + } + if (addedLength === 0) { + return [ + [], + [], + potentiallyRemoved.map(removed => [ + removed.olderPosition, + removed.annotation, + ]), + ]; + } + if (removedLength === 0) { + return [ + [], + potentiallyAdded.map(added => [added.newerPosition, added.annotation]), + [], + ]; + } + const addedHead = potentiallyAdded[0]; + const removedHead = potentiallyRemoved[0]; + // Found added when looking for a match to removed, + // Didn't find a match for removed after skipping added + // So an add and a remove is more likely a mismatch + if (addedHead.olderPosition === removedHead.olderPosition) { + return [ + [[addedHead.error, addedHead.olderPosition, addedHead.newerPosition]], + [], + [], + ]; + } + // Found removed when looking for a match to added, didn't find after skipping + if (removedHead.newerPosition === addedHead.newerPosition) { + return [ + [ + [ + removedHead.error, + removedHead.olderPosition, + removedHead.newerPosition, + ], + ], + [], + [], + ]; + } + // Current added and current removed heads aren't the same erroneousItems + const sortedOrder = compareTypeAnnotationForSorting( + [addedHead.newerPosition, addedHead.annotation], + [removedHead.olderPosition, removedHead.annotation], + ); + // Odd but possible, step down both + if (sortedOrder === 0) { + const [errors, added, removed] = resolvePotentials( + potentiallyAdded.slice(1, addedLength), + potentiallyRemoved.slice(1, removedLength), + ); + return [ + errors, + added.concat([[addedHead.newerPosition, addedHead.annotation]]), + removed.concat([[removedHead.olderPosition, removedHead.annotation]]), + ]; + } + if (sortedOrder < 0) { + const [errors, added, removed] = resolvePotentials( + potentiallyAdded.slice(1, addedLength), + potentiallyRemoved, + ); + return [ + errors, + added.concat([[addedHead.newerPosition, addedHead.annotation]]), + removed, + ]; + } + + const [errors, added, removed] = resolvePotentials( + potentiallyAdded, + potentiallyRemoved.slice(1, removedLength), + ); + return [ + errors, + added, + removed.concat([[removedHead.olderPosition, removedHead.annotation]]), + ]; +} + +function compareEventEmitterTypes( + newerAnnotation: EventEmitterTypeAnnotation, + olderAnnotation: EventEmitterTypeAnnotation, +): ComparisonResult { + const comparison = compareTypeAnnotation( + newerAnnotation.typeAnnotation, + olderAnnotation.typeAnnotation, + ); + + if (comparison.status === 'error') { + return makeError( + typeAnnotationComparisonError( + 'EventEmitter eventTypes are not equivalent', + newerAnnotation, + olderAnnotation, + comparison.errorLog, + ), + ); + } + + return comparison; +} + +function compareReservedTypeAnnotation( + newerAnnotation: CompleteReservedTypeAnnotation, + olderAnnotation: CompleteReservedTypeAnnotation, +): ComparisonResult { + if (newerAnnotation.name !== olderAnnotation.name) { + return makeError( + typeAnnotationComparisonError( + 'Types are not equivalent', + newerAnnotation, + olderAnnotation, + ), + ); + } + + switch (newerAnnotation.name) { + case 'RootTag': + case 'ColorPrimitive': + case 'ImageSourcePrimitive': + case 'PointPrimitive': + case 'EdgeInsetsPrimitive': + case 'ImageRequestPrimitive': + case 'DimensionPrimitive': + return {status: 'matching'}; + default: + (newerAnnotation.name: empty); + throw new Error('Unknown reserved type ' + newerAnnotation.name); + } +} diff --git a/packages/react-native-compatibility-check/src/VersionDiffing.js b/packages/react-native-compatibility-check/src/VersionDiffing.js new file mode 100644 index 00000000000..93664a18f20 --- /dev/null +++ b/packages/react-native-compatibility-check/src/VersionDiffing.js @@ -0,0 +1,1370 @@ +/** + * 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 + * @format + */ + +import type { + ComparisonResult, + FunctionComparisonResult, + MembersComparisonResult, + NullableComparisonResult, + PositionalComparisonResult, + PropertiesComparisonResult, +} from './ComparisonResult'; +import type { + DiffSet, + DiffSummary, + ErrorCode, + ErrorStore, + IncompatiblityReport, + NativeSpecErrorStore, + ObjectTypeChangeStore, + SchemaDiff, +} from './DiffResults'; +import type { + CompleteTypeAnnotation, + ComponentSchema, + NamedShape, + NativeModuleSchema, + SchemaType, +} from '@react-native/codegen/src/CodegenSchema'; + +import { + memberComparisonError, + positionalComparisonError, + propertyComparisonError, + typeAnnotationComparisonError, + typeInformationComparisonError, +} from './ComparisonResult.js'; +import convertPropToBasicTypes from './convertPropToBasicTypes'; +import * as codegenTypeDiffing from './TypeDiffing'; + +type BoundaryDirection = 'toNative' | 'fromNative' | 'both'; + +type checkerType = ( + propertyChange: ?PropertiesComparisonResult, + positionalChange: ?PositionalComparisonResult, + nullableChange: ?NullableComparisonResult, + memberChange: ?MembersComparisonResult, + typeName: string, +) => Array; + +function nestedPropertiesCheck( + typeName: string, + result: ComparisonResult, + check: checkerType, + inverseCheck: checkerType, +): Array { + const nestedMap = + (mid: string, end: string) => + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by + * Flow's LTI update could not be added via codemod */ + ([propertyName, comparisonResult]) => + nestedPropertiesCheck( + typeName + mid + propertyName + end, + comparisonResult, + check, + inverseCheck, + ); + switch (result.status) { + case 'error': + case 'matching': + case 'skipped': + throw new Error( + 'Internal error: nested property change ' + result.status, + ); + case 'properties': + let finalResult = check(result.propertyLog, null, null, null, typeName); + if (result.propertyLog.nestedPropertyChanges) { + finalResult = combine( + finalResult, + result.propertyLog.nestedPropertyChanges.map(nestedMap('.', '')), + ); + } + if (result.propertyLog.madeOptional) { + const furtherNestedProps = result.propertyLog.madeOptional.filter( + optionalProp => optionalProp.furtherChanges, + ); + if (furtherNestedProps && furtherNestedProps.length > 0) { + const localNestedMap = nestedMap('.', ''); + const mappedProps = furtherNestedProps.map(optionalProp => { + if (optionalProp.furtherChanges) { + return localNestedMap([ + optionalProp.property, + optionalProp.furtherChanges, + ]); + } + throw new Error('Internal error, filter failed'); + }); + finalResult = combine(finalResult, mappedProps); + } + } + return finalResult; + case 'members': + return check(null, null, null, result.memberLog, typeName); + case 'functionChange': + let returnTypeResult: Array = []; + if (result.functionChangeLog.returnType) { + returnTypeResult = nestedPropertiesCheck( + typeName, + result.functionChangeLog.returnType, + check, + inverseCheck, + ); + } + if (result.functionChangeLog.parameterTypes) { + return combine( + returnTypeResult, + result.functionChangeLog.parameterTypes.nestedChanges.map( + ([_oldParameterNumber, newParameterNumber, comparisonResult]) => + nestedPropertiesCheck( + typeName + ' parameter ' + newParameterNumber, + comparisonResult, + inverseCheck, + check, + ), + ), + ); + } + return returnTypeResult; + case 'positionalTypeChange': + const changeLog = result.changeLog; + const currentPositionalCheck = check( + null, + changeLog, + null, + null, + typeName, + ); + return combine( + currentPositionalCheck, + changeLog.nestedChanges.map(([_oldIndex, newIndex, nestedChange]) => + nestedMap( + ' element ', + ' of ' + changeLog.typeKind, + )([newIndex.toString(), nestedChange]), + ), + ); + case 'nullableChange': + const currentCheck = check( + null, + null, + result.nullableLog, + null, + typeName, + ); + if (result.nullableLog.interiorLog) { + const interiorLog = result.nullableLog.interiorLog; + switch (interiorLog.status) { + case 'matching': + return currentCheck; + case 'properties': + case 'functionChange': + case 'positionalTypeChange': + case 'nullableChange': + return combine(currentCheck, [ + nestedPropertiesCheck(typeName, interiorLog, check, inverseCheck), + ]); + default: + throw new Error( + 'Internal error: nested with error or skipped status', + ); + } + } + return currentCheck; + default: + (result.status: empty); + return []; + } +} + +function checkOptionalityAndSetError( + typeName: string, + properties: $ReadOnlyArray>, + msg: string, + errorCode: ErrorCode, +): Array { + const requiredProperties = properties.filter( + objectTypeProperty => !objectTypeProperty.optional, + ); + if (requiredProperties.length > 0) { + return [ + { + typeName, + errorCode, + errorInformation: propertyComparisonError( + msg, + requiredProperties.map(property => ({ + property: property.name, + })), + ), + }, + ]; + } + return []; +} + +// Exported for testing +export const removedPropertiesMessage = + 'Object removed required properties expected by native'; +function checkForUnsafeRemovedProperties( + propertyChange: ?PropertiesComparisonResult, + _postionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if (propertyChange && propertyChange.missingProperties) { + return checkOptionalityAndSetError( + typeName, + propertyChange.missingProperties, + removedPropertiesMessage, + 'removedProps', + ); + } + return []; +} + +export const addedPropertiesMessage = + 'Object added required properties, which native will not provide'; +function checkForUnsafeAddedProperties( + propertyChange: ?PropertiesComparisonResult, + _positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if (propertyChange && propertyChange.addedProperties) { + return checkOptionalityAndSetError( + typeName, + propertyChange.addedProperties, + addedPropertiesMessage, + 'addedProps', + ); + } + return []; +} + +export const stricterPropertiesMessage = + 'Property made strict, but native may not provide it'; +function checkForUnSafeMadeStrictProperties( + propertyChange: ?PropertiesComparisonResult, + _positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if ( + propertyChange && + propertyChange.madeStrict && + propertyChange.madeStrict.length > 0 + ) { + const err = propertyComparisonError( + stricterPropertiesMessage, + propertyChange.madeStrict.map(property => ({ + property: property.property, + })), + ); + return [ + { + typeName, + errorCode: 'requiredProps', + errorInformation: err, + }, + ]; + } + return []; +} + +export const tooOptionalPropertiesMessage = + 'Property made optional, but native requires it'; +function checkForUnSafeMadeOptionalProperties( + propertyChange: ?PropertiesComparisonResult, + _positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if ( + propertyChange && + propertyChange.madeOptional && + propertyChange.madeOptional.length > 0 + ) { + const err = propertyComparisonError( + tooOptionalPropertiesMessage, + propertyChange.madeOptional.map(property => ({ + property: property.property, + })), + ); + return [ + { + typeName, + errorCode: 'optionalProps', + errorInformation: err, + }, + ]; + } + return []; +} + +export const removedUnionMessage = + 'Union removed items, but native may still provide them'; +function checkForUnsafeRemovedUnionItems( + _propertyChange: ?PropertiesComparisonResult, + positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if ( + positionalChange && + (positionalChange.typeKind === 'union' || + positionalChange.typeKind === 'stringUnion') && + positionalChange.removedElements && + positionalChange.removedElements.length > 0 + ) { + return [ + { + typeName, + errorCode: 'removedUnionCases', + errorInformation: positionalComparisonError( + removedUnionMessage, + positionalChange.removedElements, + ), + }, + ]; + } + return []; +} + +export const addedUnionMessage = + 'Union added items, but native will not expect/support them'; +function checkForUnsafeAddedUnionItems( + _propertyChange: ?PropertiesComparisonResult, + positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if ( + positionalChange && + (positionalChange.typeKind === 'union' || + positionalChange.typeKind === 'stringUnion') && + positionalChange.addedElements && + positionalChange.addedElements.length > 0 + ) { + return [ + { + typeName, + errorCode: 'addedUnionCases', + errorInformation: positionalComparisonError( + addedUnionMessage, + positionalChange.addedElements, + ), + }, + ]; + } + return []; +} + +export const removedEnumMessage = + 'Enum removed items, but native may still provide them'; +function checkForUnsafeRemovedEnumItems( + _propertyChange: ?PropertiesComparisonResult, + _positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if (memberChange?.missingMembers && memberChange?.missingMembers.length > 0) { + return [ + { + typeName, + errorCode: 'removedEnumCases', + errorInformation: memberComparisonError( + removedEnumMessage, + memberChange.missingMembers.map(member => ({ + member: member.name, + })), + ), + }, + ]; + } + + return []; +} + +export const addedEnumMessage = + 'Enum added items, but native will not expect/support them'; +function checkForUnsafeAddedEnumItems( + _propertyChange: ?PropertiesComparisonResult, + _positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if (memberChange?.addedMembers && memberChange?.addedMembers.length > 0) { + return [ + { + typeName, + errorCode: 'addedEnumCases', + errorInformation: memberComparisonError( + addedEnumMessage, + memberChange.addedMembers.map(member => ({ + member: member.name, + })), + ), + }, + ]; + } + + return []; +} + +export const removedIntersectionMessage = + 'Intersection removed items, but native may still require properties contained in them'; +function checkForUnsafeRemovedIntersectionItems( + _propertyChange: ?PropertiesComparisonResult, + positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if ( + positionalChange && + positionalChange.typeKind === 'intersection' && + positionalChange.removedElements && + positionalChange.removedElements.length > 0 + ) { + return [ + { + typeName, + errorCode: 'removedIntersectCases', + errorInformation: positionalComparisonError( + removedIntersectionMessage, + positionalChange.removedElements, + ), + }, + ]; + } + return []; +} + +export const addedIntersectionMessage = + 'Intersection added items, but native may not provide all required attributes'; +function checkForUnsafeAddedIntersectionItems( + _propertyChange: ?PropertiesComparisonResult, + positionalChange: ?PositionalComparisonResult, + _nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if ( + positionalChange && + positionalChange.typeKind === 'intersection' && + positionalChange.addedElements && + positionalChange.addedElements.length > 0 + ) { + return [ + { + typeName, + errorCode: 'addedIntersectCases', + errorInformation: positionalComparisonError( + addedIntersectionMessage, + positionalChange.addedElements, + ), + }, + ]; + } + return []; +} + +export const toNativeVoidChangeMessage = + 'Native may not be able to safely handle presence of type'; +export const typeNullableChangeMessage = + 'Type made nullable, but native requires it'; +function checkForUnsafeNullableToNativeChange( + _propertyChange: ?PropertiesComparisonResult, + _positionalChange: ?PositionalComparisonResult, + nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if ( + nullableChange && + !nullableChange.optionsReduced && + nullableChange.newType && + nullableChange.oldType + ) { + return [ + { + typeName, + errorCode: 'nullableOfNonNull', + errorInformation: typeAnnotationComparisonError( + nullableChange.typeRefined + ? toNativeVoidChangeMessage + : typeNullableChangeMessage, + nullableChange.newType, + nullableChange.oldType, + ), + }, + ]; + } + return []; +} + +export const fromNativeVoidChangeMessage = + 'Type set to void but native may still provide a value'; +export const typeNonNullableChangeMessage = + 'Type made non-nullable, but native might provide null still'; +function checkForUnsafeNullableFromNativeChange( + _propertyChange: ?PropertiesComparisonResult, + _positionalChange: ?PositionalComparisonResult, + nullableChange: ?NullableComparisonResult, + _memberChange: ?MembersComparisonResult, + typeName: string, +): Array { + if ( + nullableChange && + nullableChange.optionsReduced && + nullableChange.newType && + nullableChange.oldType + ) { + return [ + { + typeName, + errorCode: 'nonNullableOfNull', + errorInformation: typeAnnotationComparisonError( + nullableChange.typeRefined + ? fromNativeVoidChangeMessage + : typeNonNullableChangeMessage, + nullableChange.newType, + nullableChange.oldType, + ), + }, + ]; + } + return []; +} + +function chainPropertiesChecks(checks: Array): checkerType { + return ( + propertyChange: ?PropertiesComparisonResult, + positionalChange: ?PositionalComparisonResult, + nullableChange: ?NullableComparisonResult, + memberChange: ?MembersComparisonResult, + typeName: string, + ) => + checks.reduce( + (errorStore, checker) => + errorStore.concat( + checker( + propertyChange, + positionalChange, + nullableChange, + memberChange, + typeName, + ), + ), + [], + ); +} + +function combine( + singleton: Array, + arrayOf: Array>, +) { + if (arrayOf.length > 0) { + return arrayOf.reduce( + (finalErrorArray, current) => finalErrorArray.concat(current), + singleton, + ); + } + return singleton; +} + +function compareFunctionTypesInContext( + typeName: string, + functionLog: FunctionComparisonResult, + check: checkerType, + inversecheck: checkerType, + result: Array, +) { + if (functionLog.returnType) { + result = combine(result, [ + nestedPropertiesCheck( + typeName, + functionLog.returnType, + check, + inversecheck, + ), + ]); + } + if ( + functionLog.parameterTypes && + functionLog.parameterTypes.nestedChanges.length > 0 + ) { + result = combine( + result, + functionLog.parameterTypes.nestedChanges.map( + ([_oldPropertyNum, newPropertyNum, comparisonResult]) => + nestedPropertiesCheck( + typeName + ' parameter ' + newPropertyNum, + comparisonResult, + inversecheck, + check, + ), + ), + ); + } + return result; +} + +// Deleting optional properties is safe and +// Making required properties optional is unsafe. +// Adding to unions and enums is unsafe, as is removing from intersections +// Changing nullable is similar to changing a union +const checksForTypesFlowingToNative: checkerType = chainPropertiesChecks([ + checkForUnsafeRemovedProperties, + checkForUnSafeMadeOptionalProperties, + checkForUnsafeAddedUnionItems, + checkForUnsafeAddedEnumItems, + checkForUnsafeRemovedIntersectionItems, + checkForUnsafeNullableToNativeChange, +]); +// Adding optional properties is safe, otherwise incompatible +// Making optional properties required is unsafe. +// Removing from unions and enums is unsafe, as is adding to intersections +// Changing nullable is similar to changing a union +const checksForTypesFlowingFromNative: checkerType = chainPropertiesChecks([ + checkForUnsafeAddedProperties, + checkForUnSafeMadeStrictProperties, + checkForUnsafeRemovedUnionItems, + checkForUnsafeRemovedEnumItems, + checkForUnsafeAddedIntersectionItems, + checkForUnsafeNullableFromNativeChange, +]); +export function assessComparisonResult( + newTypes: Set<{ + typeName: string, + typeInformation: CompleteTypeAnnotation, + ... + }>, + deprecatedTypes: Set<{ + typeName: string, + typeInformation: CompleteTypeAnnotation, + ... + }>, + incompatibleChanges: Set, + objectTypeChanges: Set, +): ( + typeName: string, + newType: CompleteTypeAnnotation, + oldType: ?CompleteTypeAnnotation, + difference: ComparisonResult, + oldDirection: BoundaryDirection, +) => void { + return ( + typeName: string, + newType: CompleteTypeAnnotation, + oldType: ?CompleteTypeAnnotation, + difference: ComparisonResult, + oldDirection: BoundaryDirection, + ) => { + switch (difference.status) { + case 'matching': + break; + case 'skipped': + newTypes.add({ + typeName, + typeInformation: newType, + }); + break; + case 'members': + { + const memberChange = difference.memberLog; + + const toNativeErrorResult = checksForTypesFlowingToNative( + null, + null, + null, + memberChange, + typeName, + ); + + const fromNativeErrorResult = checksForTypesFlowingFromNative( + null, + null, + null, + memberChange, + typeName, + ); + + switch (oldDirection) { + case 'toNative': + toNativeErrorResult.forEach(error => + incompatibleChanges.add(error), + ); + break; + case 'fromNative': + fromNativeErrorResult.forEach(error => + incompatibleChanges.add(error), + ); + break; + case 'both': + toNativeErrorResult.forEach(error => + incompatibleChanges.add(error), + ); + fromNativeErrorResult.forEach(error => + incompatibleChanges.add(error), + ); + break; + } + } + break; + case 'properties': + const propertyChange = difference.propertyLog; + const unsafeForToNative = nestedPropertiesCheck( + typeName, + difference, + checksForTypesFlowingToNative, + checksForTypesFlowingFromNative, + ); + const unsafeForFromNative = nestedPropertiesCheck( + typeName, + difference, + checksForTypesFlowingFromNative, + checksForTypesFlowingToNative, + ); + + switch (oldDirection) { + case 'toNative': + unsafeForToNative.forEach(error => incompatibleChanges.add(error)); + break; + case 'fromNative': + unsafeForFromNative.forEach(error => + incompatibleChanges.add(error), + ); + break; + case 'both': + unsafeForToNative.forEach(error => incompatibleChanges.add(error)); + unsafeForFromNative.forEach(error => + incompatibleChanges.add(error), + ); + break; + } + + if (!oldType) { + throw new Error('Internal error: properties change with no old type'); + } + objectTypeChanges.add({ + typeName, + newType, + oldType, + propertyChange, + }); + break; + case 'error': + incompatibleChanges.add({ + typeName, + errorCode: 'incompatibleTypes', + errorInformation: difference.errorLog, + }); + break; + case 'functionChange': + const functionLog = difference.functionChangeLog; + let propertyErrors: Array = []; + switch (oldDirection) { + case 'toNative': + propertyErrors = compareFunctionTypesInContext( + typeName, + functionLog, + checksForTypesFlowingToNative, + checksForTypesFlowingFromNative, + propertyErrors, + ); + break; + case 'fromNative': + propertyErrors = compareFunctionTypesInContext( + typeName, + functionLog, + checksForTypesFlowingFromNative, + checksForTypesFlowingToNative, + propertyErrors, + ); + break; + case 'both': + propertyErrors = compareFunctionTypesInContext( + typeName, + functionLog, + checksForTypesFlowingToNative, + checksForTypesFlowingFromNative, + propertyErrors, + ); + propertyErrors = compareFunctionTypesInContext( + typeName, + functionLog, + checksForTypesFlowingFromNative, + checksForTypesFlowingToNative, + propertyErrors, + ); + break; + default: + (oldDirection: empty); + throw new Error( + 'Unsupported native boundary direction ' + oldDirection, + ); + } + propertyErrors.forEach(error => incompatibleChanges.add(error)); + break; + case 'positionalTypeChange': + const changeLog = difference.changeLog; + if ( + changeLog.nestedChanges.length > 0 || + changeLog.addedElements || + changeLog.removedElements + ) { + const changes = changeLog.nestedChanges; + const toNativeBase = checksForTypesFlowingToNative( + null, + changeLog, + null, + null, + typeName, + ); + const toNativeResult = combine( + toNativeBase, + changes.map(([_oldIndex, newIndex, comparisonResult]) => + nestedPropertiesCheck( + `${typeName} element ${newIndex} of ${changeLog.typeKind}`, + comparisonResult, + checksForTypesFlowingToNative, + checksForTypesFlowingFromNative, + ), + ), + ); + const fromNativeBase = checksForTypesFlowingFromNative( + null, + changeLog, + null, + null, + typeName, + ); + const fromNativeResult = combine( + fromNativeBase, + changes.map(([_oldIndex, newIndex, comparisonResult]) => + nestedPropertiesCheck( + `${typeName} element ${newIndex} of ${changeLog.typeKind}`, + comparisonResult, + checksForTypesFlowingFromNative, + checksForTypesFlowingToNative, + ), + ), + ); + switch (oldDirection) { + case 'toNative': + toNativeResult.forEach(error => incompatibleChanges.add(error)); + break; + case 'fromNative': + fromNativeResult.forEach(error => incompatibleChanges.add(error)); + break; + case 'both': + toNativeResult.forEach(error => incompatibleChanges.add(error)); + fromNativeResult.forEach(error => incompatibleChanges.add(error)); + break; + } + } + break; + case 'nullableChange': + if (!oldType) { + throw new Error( + 'Internal error: old type null or undefined, after nullableChange', + ); + } + switch (oldDirection) { + case 'toNative': + checkForUnsafeNullableToNativeChange( + null, + null, + difference.nullableLog, + null, + typeName, + ).forEach(error => incompatibleChanges.add(error)); + break; + case 'fromNative': + checkForUnsafeNullableFromNativeChange( + null, + null, + difference.nullableLog, + null, + typeName, + ).forEach(error => incompatibleChanges.add(error)); + break; + case 'both': + const err = typeInformationComparisonError( + 'Type may not change nullability, due to flowing to and from native', + newType, + oldType, + ); + incompatibleChanges.add({ + typeName, + errorCode: 'incompatibleTypes', + errorInformation: err, + }); + break; + default: + (oldDirection: empty); + throw new Error('Unknown direction : ' + oldDirection); + } + /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.68 was deployed. To + * see the error delete this comment and run Flow. */ + if (difference.interiorLog) { + const log = difference.interiorLog; + assessComparisonResult( + newTypes, + deprecatedTypes, + incompatibleChanges, + objectTypeChanges, + )(typeName, newType, oldType, log, oldDirection); + } + break; + default: + (difference.status: empty); + throw new Error('Unsupported status: ' + difference.status); + } + }; +} + +function buildNativeModulesDiff( + newerNativeModule: NativeModuleSchema, + olderNativeModule: NativeModuleSchema, +): Set { + const moduleErrors = new Set(); + + const nativeModuleName = newerNativeModule.moduleName; + if (olderNativeModule.moduleName !== newerNativeModule.moduleName) { + // old was removed + moduleErrors.add({ + nativeSpecName: olderNativeModule.moduleName, + omitted: true, + errorCode: 'removedModule', + }); + } + + const newTypes = new Set<{ + typeInformation: CompleteTypeAnnotation, + typeName: string, + ... + }>(); + const deprecatedTypes = new Set<{ + typeInformation: CompleteTypeAnnotation, + typeName: string, + ... + }>(); + const incompatibleChanges = new Set(); + const objectTypeChanges = new Set(); + const localAssessComparison = assessComparisonResult( + newTypes, + deprecatedTypes, + incompatibleChanges, + objectTypeChanges, + ); + + const newType = { + type: 'ObjectTypeAnnotation', + properties: [ + ...newerNativeModule.spec.methods, + ...newerNativeModule.spec.eventEmitters, + ], + }; + const oldType = { + type: 'ObjectTypeAnnotation', + properties: [ + ...olderNativeModule.spec.methods, + ...olderNativeModule.spec.eventEmitters, + ], + }; + + const difference = codegenTypeDiffing.compareTypes( + newType, + olderNativeModule.moduleName === newerNativeModule.moduleName + ? oldType + : null, + newerNativeModule.aliasMap, + olderNativeModule.aliasMap, + newerNativeModule.enumMap, + olderNativeModule.enumMap, + ); + + localAssessComparison( + nativeModuleName, + newType, + oldType, + difference, + // Since we are explicitly checking the native module, we know it is starting as fromNative + 'fromNative', + ); + + const typeUpdate = { + newTypes, + deprecatedTypes, + incompatibleChanges, + objectTypeChanges, + }; + if (hasCodegenUpdatesTypes(typeUpdate)) { + moduleErrors.add({ + nativeSpecName: nativeModuleName, + omitted: false, + errorCode: 'incompatibleTypes', + changeInformation: typeUpdate, + }); + } + + return moduleErrors; +} + +function buildNativeComponentsDiff( + newerNativeSchema: ComponentSchema, + olderNativeSchema: ComponentSchema, +): Set { + const componentErrors = new Set(); + + Object.entries(newerNativeSchema.components).forEach( + ([newerComponentName, newerComponent]) => { + const olderComponent = olderNativeSchema.components[newerComponentName]; + + const newTypes = new Set<{ + typeInformation: CompleteTypeAnnotation, + typeName: string, + ... + }>(); + const deprecatedTypes = new Set<{ + typeInformation: CompleteTypeAnnotation, + typeName: string, + ... + }>(); + const incompatibleChanges = new Set(); + const objectTypeChanges = new Set(); + const localAssessComparison = assessComparisonResult( + newTypes, + deprecatedTypes, + incompatibleChanges, + objectTypeChanges, + ); + + /* Commands */ + // We are intentionally allowing new commands to be added + // even though they could result in an OTA issue if the command + // is called immediately and the native side throws on unrecognized + // commands. There is no way to do feature detection of commands + // today to protect against that. + // We are choosing to allow this change to be made since we previously + // had no protection for commands at all. + // See https://fb.workplace.com/groups/615693552291894/posts/1905124013348835 + // for more information. + newerComponent.commands.forEach(command => { + const oldCommand = olderComponent.commands?.find( + olderCommand => olderCommand.name === command.name, + ); + + const newCommands = { + type: 'ObjectTypeAnnotation', + properties: [command], + }; + const oldCommands = + oldCommand != null + ? { + type: 'ObjectTypeAnnotation', + properties: [oldCommand], + } + : null; + + const difference = codegenTypeDiffing.compareTypes( + newCommands, + oldCommands, + {}, + {}, + {}, + {}, + ); + + localAssessComparison( + newerComponentName, + newCommands, + oldCommands, + difference, + // Since we are explicitly checking the native module, we know it is starting as fromNative + 'fromNative', + ); + }); + + olderComponent.commands?.forEach(command => { + const newCommand = newerComponent.commands.find( + newerCommand => newerCommand.name === command.name, + ); + + if (newCommand == null) { + deprecatedTypes.add({ + typeName: command.name, + typeInformation: { + type: 'ObjectTypeAnnotation', + properties: [command], + }, + }); + } + }); + /* End Commands */ + + // We have to do this to remove the .defaults from the props and get it into + // standard JavaScript shapes. + const newConvertedProps = { + type: 'ObjectTypeAnnotation', + properties: newerComponent.props.map(prop => ({ + name: prop.name, + optional: prop.optional, + typeAnnotation: convertPropToBasicTypes(prop.typeAnnotation), + })), + }; + const oldConvertedProps = { + type: 'ObjectTypeAnnotation', + properties: olderComponent.props.map(prop => ({ + name: prop.name, + optional: prop.optional, + typeAnnotation: convertPropToBasicTypes(prop.typeAnnotation), + })), + }; + + const propDifference = codegenTypeDiffing.compareTypes( + newConvertedProps, + oldConvertedProps, + {}, + {}, + {}, + {}, + ); + + localAssessComparison( + newerComponentName, + newConvertedProps, + oldConvertedProps, + propDifference, + 'toNative', + ); + + const typeUpdate = { + newTypes, + deprecatedTypes, + incompatibleChanges, + objectTypeChanges, + }; + if (hasCodegenUpdatesTypes(typeUpdate)) { + componentErrors.add({ + nativeSpecName: newerComponentName, + omitted: false, + errorCode: 'incompatibleTypes', + changeInformation: typeUpdate, + }); + } + }, + ); + + Object.keys(olderNativeSchema.components).forEach(olderComponentName => { + const newerComponent = newerNativeSchema.components[olderComponentName]; + + if (newerComponent == null) { + // Component is missing in new schema + componentErrors.add({ + nativeSpecName: olderComponentName, + omitted: true, + errorCode: 'removedComponent', + }); + } + }); + + return componentErrors; +} + +export function hasUpdatesTypes(diff: DiffSet): boolean { + return ( + diff.newTypes.size > 0 || + diff.deprecatedTypes.size > 0 || + diff.objectTypeChanges.size > 0 || + diff.incompatibleChanges.size > 0 + ); +} + +export function hasCodegenUpdatesTypes(diff: DiffSet): boolean { + return ( + diff.newTypes.size > 0 || + diff.deprecatedTypes.size > 0 || + diff.objectTypeChanges.size > 0 || + diff.incompatibleChanges.size > 0 + ); +} + +export function buildSchemaDiff( + newerSchemaSet: SchemaType, + olderSchemaSet: SchemaType, +): Set { + const diff: Set = new Set(); + const newerSchema = newerSchemaSet.modules; + const olderSchema = olderSchemaSet.modules; + + Object.keys(newerSchema).forEach(hasteModuleName => { + const schemaEntry = newerSchema[hasteModuleName]; + const olderSchemaEntry = olderSchema[hasteModuleName]; + const framework = 'ReactNative'; + + if (schemaEntry.type === 'Component') { + if (olderSchemaEntry?.type === 'Component') { + const incompatibleComponents = buildNativeComponentsDiff( + schemaEntry, + olderSchemaEntry, + ); + const hasIncompatibleComponents = incompatibleComponents?.size > 0; + if (hasIncompatibleComponents) { + diff.add({ + name: hasteModuleName, + framework: framework, + status: { + incompatibleSpecs: incompatibleComponents, + }, + }); + } + } + } + + if (schemaEntry.type === 'NativeModule') { + if (olderSchemaEntry?.type === 'NativeModule') { + // Both native modules + const incompatibleModules = buildNativeModulesDiff( + schemaEntry, + olderSchemaEntry, + ); + + const hasIncompatibleModules = + incompatibleModules != null && incompatibleModules.size; + + if (hasIncompatibleModules) { + diff.add({ + name: hasteModuleName, + framework: framework, + status: { + incompatibleSpecs: incompatibleModules, + }, + }); + } + } + } + + if (olderSchemaEntry == null) { + diff.add({ + name: hasteModuleName, + framework: framework, + status: 'new', + }); + } + }); + + Object.keys(olderSchema).forEach(hasteModuleName => { + const newSchemaEntry = newerSchema[hasteModuleName]; + const oldSchemaEntry = olderSchema[hasteModuleName]; + const framework = 'ReactNative'; + + if (oldSchemaEntry != null && newSchemaEntry == null) { + diff.add({ + name: hasteModuleName, + framework: framework, + status: 'deprecated', + }); + } + }); + + return diff; +} + +function summarizeSchemaDiff(diff: SchemaDiff): DiffSummary { + switch (diff.status) { + case 'new': + return {status: 'patchable', incompatibilityReport: {}}; + case 'deprecated': + // TODO add check that a component can be removed + return {status: 'ok', incompatibilityReport: {}}; + default: + // differs case + const differs = diff.status; + if (!differs.incompatibleSpecs) { + return {status: 'patchable', incompatibilityReport: {}}; + } else { + const incompatibleObject: IncompatiblityReport = {}; + if (differs.incompatibleSpecs) { + const withErrors = Array.from(differs.incompatibleSpecs).filter( + specError => + specError.errorInformation || + (specError.changeInformation && + specError.changeInformation.incompatibleChanges.size > 0), + ); + if (withErrors.length > 0) { + if (incompatibleObject[diff.name]) { + incompatibleObject[diff.name].incompatibleSpecs = withErrors; + } else { + incompatibleObject[diff.name] = { + framework: diff.framework, + incompatibleSpecs: withErrors, + }; + } + } + } + const incompatibleUnchanged = + Object.keys(incompatibleObject).length === 0; + return { + status: incompatibleUnchanged ? 'ok' : 'incompatible', + incompatibilityReport: incompatibleObject, + }; + } + } +} + +function combineSummaries( + finalSummary: DiffSummary, + setSummary: DiffSummary, +): DiffSummary { + switch (setSummary.status) { + case 'ok': + return finalSummary; + case 'patchable': + if (finalSummary.status === 'ok') { + return setSummary; + } else { + return finalSummary; + } + default: + switch (finalSummary.status) { + case 'ok': + case 'patchable': + return setSummary; + default: + Object.keys(setSummary.incompatibilityReport).forEach( + differingSchemaName => + (finalSummary.incompatibilityReport[differingSchemaName] = + setSummary.incompatibilityReport[differingSchemaName]), + ); + return finalSummary; + } + } +} + +export function summarizeDiffSet(diffs: Set): DiffSummary { + if (diffs.size === 0) { + return {status: 'ok', incompatibilityReport: {}}; + } + const summary: Array = []; + diffs.forEach(schemaDiff => summary.push(summarizeSchemaDiff(schemaDiff))); + return summary.reduce(combineSummaries, summary[0]); +} diff --git a/packages/react-native-compatibility-check/src/__tests__/ErrorFormatting-test.js b/packages/react-native-compatibility-check/src/__tests__/ErrorFormatting-test.js new file mode 100644 index 00000000000..dd9de5f5bb3 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/ErrorFormatting-test.js @@ -0,0 +1,65 @@ +/** + * 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 + * @format + * @oncall react_native + */ + +import {formatDiffSet} from '../ErrorFormatting.js'; +import {buildSchemaDiff, summarizeDiffSet} from '../VersionDiffing.js'; +import {incompatibleChanges, okayChanges} from './ErrorFormattingTests.js'; +import {getTestSchema} from './utilities/getTestSchema.js'; + +describe('codegen formattedSummarizeDiffSet', () => { + okayChanges.forEach(([after, before]) => { + it(`allows a diff from ${before} to ${after}`, () => { + const beforeObj = getTestSchema( + __dirname, + '__fixtures__', + before + '.js.flow', + ); + + const afterObj = getTestSchema( + __dirname, + '__fixtures__', + after + '.js.flow', + ); + + const result = formatDiffSet( + summarizeDiffSet(buildSchemaDiff(afterObj, beforeObj)), + ); + + expect(result.status).toEqual('ok'); + + expect(result).toMatchSnapshot(); + }); + }); + + incompatibleChanges.forEach(([after, before]) => { + it(`reports a diff from ${before} to ${after}`, () => { + const beforeObj = getTestSchema( + __dirname, + '__fixtures__', + before + '.js.flow', + ); + + const afterObj = getTestSchema( + __dirname, + '__fixtures__', + after + '.js.flow', + ); + + const result = formatDiffSet( + summarizeDiffSet(buildSchemaDiff(afterObj, beforeObj)), + ); + + expect(result.status).toEqual('incompatible'); + + expect(result).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/react-native-compatibility-check/src/__tests__/ErrorFormattingTests.js b/packages/react-native-compatibility-check/src/__tests__/ErrorFormattingTests.js new file mode 100644 index 00000000000..a4a97b720d4 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/ErrorFormattingTests.js @@ -0,0 +1,150 @@ +/** + * 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 + */ + +export const okayChanges = [ + [ + 'native-module-before-after-types-removed/NativeModuleBeforeAfterTypes', + 'native-module-before-after-types/NativeModuleBeforeAfterTypes', + ], + [ + 'native-module-get-constants/NativeModule', + 'native-module-get-constants-added-optional-constant/NativeModule', + ], + [ + 'native-module-with-enum-from-native-changes/NativeModule', + 'native-module-with-enum-from-native/NativeModule', + ], + [ + 'native-module-with-union/NativeModule', + 'native-module-with-union-changes/NativeModule', + ], + [ + 'native-component-with-command/NativeComponent', + 'native-component-with-command/NativeComponent', + ], + [ + 'native-component-with-props/NativeComponent', + 'native-component-with-props-default-change/NativeComponent', + ], +]; + +export const incompatibleChanges = [ + [ + 'native-module-before-after-types/NativeModuleBeforeAfterTypes', + 'native-module-before-after-types-removed/NativeModuleBeforeAfterTypes', + ], + [ + 'native-module-before-after-types/NativeModuleBeforeAfterTypes', + 'native-module-before-after-types-type-changed/NativeModuleBeforeAfterTypes', + ], + ['native-module/NativeModule', 'native-module-changed/NativeModule'], + [ + 'native-module-nested/NativeModule', + 'native-module-nested-changed/NativeModule', + ], + [ + 'native-module-nested-optional/NativeModule', + 'native-module-nested/NativeModule', + ], + [ + 'native-module-nested-nullable/NativeModule', + 'native-module-nested-optional/NativeModule', + ], + [ + 'native-module-nested-optional/NativeModule', + 'native-module-nested-changed/NativeModule', + ], + [ + 'native-module-nested-alias/NativeModule', + 'native-module-nested-alias-changed/NativeModule', + ], + [ + 'native-module-with-dictionary/NativeModule', + 'native-module-with-dictionary-changed/NativeModule', + ], + [ + 'native-module-with-enum-changes/NativeModule', + 'native-module-with-enum/NativeModule', + ], + [ + 'native-module-with-enum/NativeModule', + 'native-module-with-enum-type-changes/NativeModule', + ], + [ + 'native-module-with-enum/NativeModule', + 'native-module-with-enum-value-changes/NativeModule', + ], + + [ + 'native-module-with-enum-from-native/NativeModule', + 'native-module-with-enum-from-native-changes/NativeModule', + ], + [ + 'native-module-with-enum-from-native/NativeModule', + 'native-module-with-enum-from-native-type-changes/NativeModule', + ], + [ + 'native-module-with-enum-from-native/NativeModule', + 'native-module-with-enum-from-native-value-changes/NativeModule', + ], + [ + 'native-module-with-eventemitter/NativeModule', + 'native-module-with-eventemitter-changes/NativeModule', + ], + [ + 'native-module/NativeModule', + 'native-module-with-optional-argument/NativeModule', + ], + [ + 'native-module-with-union/NativeModule', + 'native-module-with-union-type-changes/NativeModule', + ], + [ + 'native-module-with-union-changes/NativeModule', + 'native-module-with-union/NativeModule', + ], + [ + 'native-module-with-union-from-native/NativeModule', + 'native-module-with-union-from-native-changes/NativeModule', + ], + [ + 'native-module-with-union/NativeModule', + 'native-module-with-union-confusing-string-literals/NativeModule', + ], + [ + 'native-component-with-command/NativeComponent', + 'native-component-with-command-changed/NativeComponent', + ], + [ + 'native-component-with-command-all-basic-types-arrays/NativeComponent', + 'native-component-with-command-all-basic-types/NativeComponent', + ], + [ + 'native-component-with-command-extra-arg/NativeComponent', + 'native-component-with-command/NativeComponent', + ], + [ + 'native-component-with-props/NativeComponent', + 'native-component-with-props-changes/NativeComponent', + ], + [ + 'native-component-with-props/NativeComponent', + 'native-component-with-props-added-required-prop/NativeComponent', + ], + [ + 'native-component-with-props-union-added/NativeComponent', + 'native-component-with-props-union/NativeComponent', + ], + [ + 'native-component-with-props-array-union-added/NativeComponent', + 'native-component-with-props-array-union/NativeComponent', + ], +]; diff --git a/packages/react-native-compatibility-check/src/__tests__/TypeDiffing-test.js b/packages/react-native-compatibility-check/src/__tests__/TypeDiffing-test.js new file mode 100644 index 00000000000..55e0a114fcb --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/TypeDiffing-test.js @@ -0,0 +1,1207 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +import convertPropToBasicTypes from '../convertPropToBasicTypes'; +import {compareTypes} from '../TypeDiffing.js'; +import {getTestSchema} from './utilities/getTestSchema.js'; + +function getModule(...filenameComponents: Array) { + const schema = getTestSchema( + __dirname, + '__fixtures__', + ...filenameComponents, + ); + const hasteModule = filenameComponents[1].split('.')[0]; + const module = schema.modules[hasteModule]; + const aliasMap = module.aliasMap; + const enumMap = module.enumMap; + const methods = module.spec.methods; + const emitters = module.spec.eventEmitters; + + function methodLookup(methodName: string) { + return methods.find(method => method.name === methodName).typeAnnotation; + } + + function methodParamLookup(methodName: string) { + return methodLookup(methodName).params[0].typeAnnotation; + } + + function emitterLookup(emitterName: string) { + return emitters.find(emitter => emitter.name === emitterName) + .typeAnnotation; + } + + return [aliasMap, methodParamLookup, methodLookup, emitterLookup, enumMap]; +} + +function getComponent(...filenameComponents: Array) { + const schema = getTestSchema( + __dirname, + '__fixtures__', + ...filenameComponents, + ); + const hasteModule = filenameComponents[1].split('.')[0]; + const component = schema.modules[hasteModule]; + const firstComponent = + component.components[Object.keys(component.components)[0]]; + + const {commands} = firstComponent; + + function commandLookup(commandName: string) { + return commands.find(command => command.name === commandName) + .typeAnnotation; + } + + function propLookup(propName: string) { + // This conversion is done in VersionDiffing, so we need to emulate it here. + return convertPropToBasicTypes( + firstComponent.props.find(prop => prop.name === propName).typeAnnotation, + ); + } + + return [propLookup, commandLookup]; +} + +const [ + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesMethodParamLookup, + nativeModuleBeforeAfterTypesMethodLookup, +] = getModule( + 'native-module-before-after-types', + 'NativeModuleBeforeAfterTypes.js.flow', +); + +const [ + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesMethodParamLookup, + nativeTypeDiffingTypesMethodLookup, + _, + nativeTypeDiffingTypesEnums, +] = getModule( + 'native-module-type-diffing-types', + 'NativeTypeDiffingTypes.js.flow', +); + +/* eslint-disable no-unused-vars */ +const [ + nativeModuleWithEventEmitterAliases, + _nativeModuleWithEventEmitterMethodParamLookup, + _nativeModuleWithEventEmitterMethodLookup, + nativeModuleWithEventEmitterEmitterLookup, +] = getModule('native-module-with-eventemitter', 'NativeModule.js.flow'); + +const [ + nativeModuleWithEventEmitterChangesAliases, + _nativeModuleWithEventEmitterChangesMethodParamLookup, + _nativeModuleWithEventEmitterChangesMethodLookup, + nativeModuleWithEventEmitterChangesEmitterLookup, +] = getModule( + 'native-module-with-eventemitter-changes', + 'NativeModule.js.flow', +); +/* eslint-enable no-unused-vars */ + +const [ + nativeComponentAllTypesPropLookup, + nativeComponentAllTypesCommandLookup, +] = getComponent('native-component-all-types', 'NativeComponent.js.flow'); + +expect.extend({ + toBeAnError(received) { + expect(received.status).toBe('error'); + return { + pass: true, + }; + }, + + toHaveErrorWithMessage(received, argument) { + expect(received.hasOwnProperty('errorLog')).toBe(true); + expect(received.errorLog.message).toContain(argument); + return { + message: () => '', + pass: true, + }; + }, +}); + +describe('components', () => { + describe('props', () => { + it('errors between number types', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('double'), + nativeComponentAllTypesPropLookup('float'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Type annotations are not the same.'); + }); + + it('does not error between same types', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('double'), + nativeComponentAllTypesPropLookup('double'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('mixed can not become another type', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('int'), + nativeComponentAllTypesPropLookup('mixed'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Type annotations are not the same.'); + }); + + it('int matches WithDefault int', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('int'), + nativeComponentAllTypesPropLookup('optionalInt'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('string matches WithDefault string', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('string'), + nativeComponentAllTypesPropLookup('optionalString'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('optional string with different defaults match', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('optionalString'), + nativeComponentAllTypesPropLookup('optionalStringDiffDefault'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('string not compatible with string union', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('string'), + nativeComponentAllTypesPropLookup('stringUnion'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Type annotations are not the same.'); + }); + + it('string union not compatible with int union', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('stringUnion'), + nativeComponentAllTypesPropLookup('intUnion'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Type annotations are not the same.'); + }); + + it('nested object with changed value not compatible', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('nestedObjectChanged'), + nativeComponentAllTypesPropLookup('nestedObject'), + {}, + {}, + ), + ).toHaveErrorWithMessage( + 'Object contained a property with a type mismatch', + ); + }); + + it('array with a string union to an extra option not compatible', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('arrayStringUnionExtra'), + nativeComponentAllTypesPropLookup('arrayStringUnion'), + {}, + {}, + ), + ).toEqual( + expect.objectContaining({ + status: 'positionalTypeChange', + changeLog: expect.objectContaining({ + typeKind: 'stringUnion', + addedElements: expect.arrayContaining([expect.any(Array)]), + }), + }), + ); + }); + + it('array of an object to an extra key not compatible', () => { + expect( + compareTypes( + nativeComponentAllTypesPropLookup('arrayNestedObjectChange'), + nativeComponentAllTypesPropLookup('arrayNestedObject'), + {}, + {}, + ), + ).toHaveErrorWithMessage( + 'Object contained a property with a type mismatch', + ); + }); + }); + describe('commands', () => { + it('errors on type of parameter changing', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodInt'), + nativeComponentAllTypesCommandLookup('methodString'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Parameter at index 0 did not match'); + }); + + it('errors on parameter changing between number types', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodFloat'), + nativeComponentAllTypesCommandLookup('methodInt'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Parameter at index 0 did not match'); + }); + + it('errors on parameter changing from int to RootTag (opaque int)', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodRootTag'), + nativeComponentAllTypesCommandLookup('methodInt'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Parameter at index 0 did not match'); + }); + + it('does not error when unchanged with string', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodString'), + nativeComponentAllTypesCommandLookup('methodString'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('does not error when unchanged with RootTag', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodRootTag'), + nativeComponentAllTypesCommandLookup('methodRootTag'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('errors on additional argument', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodIntString'), + nativeComponentAllTypesCommandLookup('methodInt'), + {}, + {}, + ), + ).toHaveErrorWithMessage( + 'Function types have differing length of arguments', + ); + }); + + it('errors on argument removed', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodInt'), + nativeComponentAllTypesCommandLookup('methodIntString'), + {}, + {}, + ), + ).toHaveErrorWithMessage( + 'Function types have differing length of arguments', + ); + }); + + it('errors on argument compared to array of argument', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodInt'), + nativeComponentAllTypesCommandLookup('methodIntArray'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Parameter at index 0 did not match'); + }); + + it('errors on arrays of different types', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodIntArray'), + nativeComponentAllTypesCommandLookup('methodFloatArray'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Parameter at index 0 did not match'); + }); + + it('does not error when becoming an alias', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodBool'), + nativeComponentAllTypesCommandLookup('methodBoolAlias'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('does not error when becoming inlined', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodString'), + nativeComponentAllTypesCommandLookup('methodStringAlias'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('does not error (but it should) on arrays of two different objects', () => { + // This doesn't fail because we don't store object alias info in the schema + // These are just arrays of mixed. Someday someone should fix this. NativeModules support this. + // More info in https://github.com/facebook/react-native/pull/48476 + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodCustomObjArray'), + nativeComponentAllTypesCommandLookup('methodCustomObj2Array'), + {}, + {}, + ).status, + ).toBe('matching'); + }); + + it('errors on arrays of mixed vs array of standard type', () => { + expect( + compareTypes( + nativeComponentAllTypesCommandLookup('methodIntArray'), + nativeComponentAllTypesCommandLookup('methodCustomObjArray'), + {}, + {}, + ), + ).toHaveErrorWithMessage('Parameter at index 0 did not match'); + }); + }); +}); + +describe('compareTypes', () => { + it('matches the exact same type', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesAliases.SimpleObject, + nativeModuleBeforeAfterTypesAliases.SimpleObject, + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('skips null types', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesAliases.SimpleObject, + null, + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).status, + ).toBe('skipped'); + }); + + it('matches on types with the same generic parameters', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesAliases.BeforeMatchingGeneric, + nativeModuleBeforeAfterTypesAliases.AfterMatchingGeneric, + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('matches the exact same EventEmitter', () => { + expect( + compareTypes( + nativeModuleWithEventEmitterEmitterLookup('onClick'), + nativeModuleWithEventEmitterEmitterLookup('onClick'), + nativeModuleWithEventEmitterAliases, + nativeModuleWithEventEmitterAliases, + ).status, + ).toBe('matching'); + }); + + it('fails on mismatching EventEmitter parameters', () => { + expect( + compareTypes( + nativeModuleWithEventEmitterEmitterLookup('onClick'), + nativeModuleWithEventEmitterChangesEmitterLookup('onClick'), + nativeModuleWithEventEmitterAliases, + nativeModuleWithEventEmitterChangesAliases, + ), + ).toHaveErrorWithMessage('EventEmitter eventTypes are not equivalent'); + }); +}); + +describe('compareTypes objects', () => { + it('matches objects with same properties', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithProps, + nativeTypeDiffingTypesAliases.ObjectTypeWithProps, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('matches objects with unsafe objects', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodLookup('genericObjectType'), + nativeTypeDiffingTypesMethodLookup('unsafeObjectType'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('reports objects with added properties', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithProps, + nativeTypeDiffingTypesAliases.ObjectTypeLessProps, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'properties', + propertyLog: expect.objectContaining({ + addedProperties: expect.any(Object), + }), + }), + ); + }); + + it('reports objects with removed properties', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeLessProps, + nativeTypeDiffingTypesAliases.ObjectTypeWithProps, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'properties', + propertyLog: expect.objectContaining({ + missingProperties: expect.any(Object), + }), + }), + ); + }); + + it('matches objects with nested objects', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithNesting, + nativeTypeDiffingTypesAliases.ObjectTypeWithNesting, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('reports objects with changed properties', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithNesting, + nativeTypeDiffingTypesAliases.ObjectTypeWithChanges, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toHaveErrorWithMessage( + 'Object contained properties with type mismatches', + ); + }); + + it('reports objects with changed nested properties', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithNesting, + nativeTypeDiffingTypesAliases.ObjectTypeWithNestedChanges, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toHaveErrorWithMessage( + 'Object contained a property with a type mismatch', + ); + }); + + it('reports objects with completely different properties', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithProps, + nativeTypeDiffingTypesAliases.ObjectTypeWithDifferentProps, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toHaveErrorWithMessage('Object types do not match.'); + }); + + it('matches objects with optional properties, no changes', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithTwoOptionals, + nativeTypeDiffingTypesAliases.ObjectTypeWithTwoOptionals, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('reports objects with properties made strict', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithProps, + nativeTypeDiffingTypesAliases.ObjectTypeWithTwoOptionals, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'properties', + propertyLog: expect.objectContaining({ + madeStrict: expect.any(Object), + }), + }), + ); + }); + + it('reports objects with properties made optional', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithTwoOptionals, + nativeTypeDiffingTypesAliases.ObjectTypeWithProps, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'properties', + propertyLog: expect.objectContaining({ + madeOptional: expect.any(Object), + }), + }), + ); + }); + + it('reports objects with properties made strict, and nested changes', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithNesting, + nativeTypeDiffingTypesAliases.ObjectTypeWithOptionalNestedChange, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'properties', + propertyLog: expect.objectContaining({ + madeStrict: expect.objectContaining({ + '0': expect.objectContaining({ + furtherChange: expect.objectContaining({status: 'properties'}), + }), + }), + }), + }), + ); + }); + + it('reports objects with properties made optional, and nested changes', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeWithOptionalNestedChange, + nativeTypeDiffingTypesAliases.ObjectTypeWithNesting, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'properties', + propertyLog: expect.objectContaining({ + madeOptional: expect.objectContaining({ + '0': expect.objectContaining({ + furtherChange: expect.objectContaining({status: 'properties'}), + }), + }), + }), + }), + ); + }); + + it('matches structurally same objects', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeLiteral1, + nativeTypeDiffingTypesAliases.ObjectTypeLiteral2, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('matches objects with their type aliases', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeLiteral1, + nativeTypeDiffingTypesMethodParamLookup('objectTypeLiteral1TypeAlias'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('matches objects with type aliases of structurally same objects', () => { + expect( + compareTypes( + nativeTypeDiffingTypesAliases.ObjectTypeLiteral1, + nativeTypeDiffingTypesMethodParamLookup('objectTypeLiteral2TypeAlias'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('objectTypeLiteral1TypeAlias'), + nativeTypeDiffingTypesAliases.ObjectTypeLiteral2, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('matches two type aliases of structurally same objects', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('objectTypeLiteral1TypeAlias'), + nativeTypeDiffingTypesMethodParamLookup('objectTypeLiteral2TypeAlias'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); +}); + +describe('compareTypes primitives', () => { + it('matches primitive types that are the same', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodParamLookup('booleanType'), + nativeModuleBeforeAfterTypesMethodParamLookup('booleanType'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('fails on primitive types that are not the same', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodParamLookup('booleanType'), + nativeModuleBeforeAfterTypesMethodParamLookup('stringType'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ), + ).toHaveErrorWithMessage('Type annotations are not the same'); + }); +}); + +describe('compareTypes functions', () => { + it('matches functions on primitive types that are the same', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodLookup('simpleFunction'), + nativeModuleBeforeAfterTypesMethodLookup('simpleFunction'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('fails on functions with differing return types', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodLookup('simpleFunction'), + nativeModuleBeforeAfterTypesMethodLookup('simpleFunction2'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ), + ).toHaveErrorWithMessage('Function return types do not match'); + }); + + it('fails on functions with differing parameter types', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodLookup('simpleFunction'), + nativeModuleBeforeAfterTypesMethodLookup('simpleFunction4'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ), + ).toHaveErrorWithMessage('Parameter at index 0 did not match'); + }); +}); + +describe('compareTypes unions', () => { + it('matches unions that are the same', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodParamLookup('simpleUnion'), + nativeModuleBeforeAfterTypesMethodParamLookup('simpleUnion'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).status, + ).toBe('matching'); + }); + it('matches unions that are the same alias', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodLookup('simpleUnion'), + nativeModuleBeforeAfterTypesMethodLookup('simpleUnion'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).status, + ).toBe('matching'); + }); + it('fails on differing unions with same length, string -> number', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodParamLookup('simpleUnion'), + nativeModuleBeforeAfterTypesMethodParamLookup('simpleUnion2'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ), + ).toBeAnError(); + }); + it('fails on differing unions with same length and same alias name', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodLookup('simpleUnion'), + nativeModuleBeforeAfterTypesMethodLookup('simpleUnion2'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ), + ).toBeAnError(); + }); + it('reports on unions with differing types: string -> object', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodParamLookup('simpleUnion2'), + nativeModuleBeforeAfterTypesMethodParamLookup('simpleUnion3'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ), + ).toHaveErrorWithMessage('Union member type does not match'); + }); + it('reports on unions with differing lengths same alias', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodLookup('simpleUnion'), + nativeModuleBeforeAfterTypesMethodLookup('simpleUnionLonger'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).functionChangeLog.parameterTypes.nestedChanges[0][2], + ).toEqual( + expect.objectContaining({ + status: 'positionalTypeChange', + changeLog: expect.objectContaining({ + typeKind: 'stringUnion', + removedElements: expect.arrayContaining([expect.any(Array)]), + }), + }), + ); + }); +}); + +describe('compareTypes on arrays', () => { + it('arrays that are the same, with primitive types', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodParamLookup('simpleArray'), + nativeModuleBeforeAfterTypesMethodParamLookup('simpleArray'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('arrays that have changed, with primitive types', () => { + expect( + compareTypes( + nativeModuleBeforeAfterTypesMethodParamLookup('simpleArray'), + nativeModuleBeforeAfterTypesMethodParamLookup('simpleArrayChange'), + nativeModuleBeforeAfterTypesAliases, + nativeModuleBeforeAfterTypesAliases, + ), + ).toHaveErrorWithMessage('Type annotations are not the same'); + }); +}); + +describe('compareTypes on string literals', () => { + it('matches literals that are the same', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('stringLiteral0'), + nativeTypeDiffingTypesMethodParamLookup('stringLiteral0'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('fails on literals that are not the same', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('stringLiteral0'), + nativeTypeDiffingTypesMethodParamLookup('stringLiteral1'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toHaveErrorWithMessage('String literals are not equal'); + }); +}); + +describe('compareTypes on numeric literals', () => { + it('matches literals that are the same', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('numericLiteral0'), + nativeTypeDiffingTypesMethodParamLookup('numericLiteral0'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('fails on literals that are not the same', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('numericLiteral0'), + nativeTypeDiffingTypesMethodParamLookup('numericLiteral1'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toHaveErrorWithMessage('Numeric literals are not equal'); + }); +}); + +describe('compareTypes on generic types', () => { + it('matches generic types that are the same', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('referenceType'), + nativeTypeDiffingTypesMethodParamLookup('referenceType'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('fails when different types are referenced', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('referenceType'), + nativeTypeDiffingTypesMethodParamLookup('referenceType3'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'properties', + propertyLog: expect.objectContaining({ + missingProperties: expect.any(Object), + }), + }), + ); + }); + + it('matches when structurally same types are referenced', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('referenceType'), + nativeTypeDiffingTypesMethodParamLookup('referenceType2'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); +}); + +describe('compareTypes on string literal unions', () => { + it('matches unions that are the same', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('stringLiteralUnion0'), + nativeTypeDiffingTypesMethodParamLookup('stringLiteralUnion0'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('reports on unions with different lengths', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('stringLiteralUnion0'), + nativeTypeDiffingTypesMethodParamLookup('stringLiteralUnion1'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'positionalTypeChange', + changeLog: expect.objectContaining({ + typeKind: 'stringUnion', + removedElements: expect.arrayContaining([expect.any(Array)]), + }), + }), + ); + }); + + it('fails on unions with differing elements', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('stringLiteralUnion0'), + nativeTypeDiffingTypesMethodParamLookup('stringLiteralUnion2'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toHaveErrorWithMessage('Subtype of union at position 1 did not match'); + }); +}); + +describe('compareTypes on nullables', () => { + it('matches nullables that are the same', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('nullableType'), + nativeTypeDiffingTypesMethodParamLookup('nullableType'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ).status, + ).toBe('matching'); + }); + + it('reports nullables that have changed to strict', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('nonNullableType'), + nativeTypeDiffingTypesMethodParamLookup('nullableType'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'nullableChange', + nullableLog: expect.objectContaining({ + optionsReduced: true, + interiorLog: expect.objectContaining({status: 'matching'}), + }), + }), + ); + }); + + it('reports nonnullables that have changed to nullable', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('nullableType'), + nativeTypeDiffingTypesMethodParamLookup('nonNullableType'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'nullableChange', + nullableLog: expect.objectContaining({ + optionsReduced: false, + interiorLog: expect.objectContaining({status: 'matching'}), + }), + }), + ); + }); + + it('reports nullable change, and interior change', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('objectTypeWithProps'), + nativeTypeDiffingTypesMethodParamLookup('optionalTypeLessProps'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + ), + ).toEqual( + expect.objectContaining({ + status: 'nullableChange', + nullableLog: expect.objectContaining({ + optionsReduced: true, + interiorLog: expect.objectContaining({status: 'properties'}), + }), + }), + ); + }); +}); + +describe('compareTypes on enums', () => { + it('matches enums that are the same', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('enum'), + nativeTypeDiffingTypesMethodParamLookup('enum'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesEnums, + nativeTypeDiffingTypesEnums, + ).status, + ).toBe('matching'); + }); + + it('reports enums that have changed types', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('enum'), + nativeTypeDiffingTypesMethodParamLookup('enumWithTypeChange'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesEnums, + nativeTypeDiffingTypesEnums, + ), + ).toHaveErrorWithMessage('EnumDeclaration member types are not the same'); + }); + + it('reports enums that have changed values', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('enum'), + nativeTypeDiffingTypesMethodParamLookup('enumWithValueChange'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesEnums, + nativeTypeDiffingTypesEnums, + ), + ).toEqual( + expect.objectContaining({ + errorLog: expect.objectContaining({ + message: 'Enum types do not match', + previousError: expect.objectContaining({ + message: 'Enum contained a member with a type mismatch', + mismatchedMembers: expect.arrayContaining([ + expect.objectContaining({ + member: 'D', + }), + ]), + }), + }), + }), + ); + }); + + it('reports enums that have removed items', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('enumWithRemoval'), + nativeTypeDiffingTypesMethodParamLookup('enum'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesEnums, + nativeTypeDiffingTypesEnums, + ), + ).toEqual( + expect.objectContaining({ + status: 'members', + memberLog: expect.objectContaining({ + missingMembers: expect.arrayContaining([ + expect.objectContaining({ + name: 'D', + value: expect.objectContaining({ + value: 4, + }), + }), + ]), + }), + }), + ); + }); + + it('reports enums that have added items', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('enum'), + nativeTypeDiffingTypesMethodParamLookup('enumWithRemoval'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesEnums, + nativeTypeDiffingTypesEnums, + ), + ).toEqual( + expect.objectContaining({ + status: 'members', + memberLog: expect.objectContaining({ + addedMembers: expect.arrayContaining([ + expect.objectContaining({ + name: 'D', + value: expect.objectContaining({ + value: 4, + }), + }), + ]), + }), + }), + ); + }); + + it('matches enums that are the same but sorted differently', () => { + expect( + compareTypes( + nativeTypeDiffingTypesMethodParamLookup('enum'), + nativeTypeDiffingTypesMethodParamLookup('enumUnsorted'), + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesAliases, + nativeTypeDiffingTypesEnums, + nativeTypeDiffingTypesEnums, + ).status, + ).toBe('matching'); + }); +}); diff --git a/packages/react-native-compatibility-check/src/__tests__/VersionDiffing-test.js b/packages/react-native-compatibility-check/src/__tests__/VersionDiffing-test.js new file mode 100644 index 00000000000..361a70e6f9c --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/VersionDiffing-test.js @@ -0,0 +1,1603 @@ +/** + * 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 + * @format + * @oncall react_native + */ + +import type {SchemaDiff, TypeStore} from '../DiffResults'; + +import {schemaDiffExporter} from '../DiffResults.js'; +import { + addedEnumMessage, + addedPropertiesMessage, + addedUnionMessage, + buildSchemaDiff, + hasUpdatesTypes, + removedEnumMessage, + removedPropertiesMessage, + removedUnionMessage, + summarizeDiffSet, + tooOptionalPropertiesMessage, + typeNullableChangeMessage, +} from '../VersionDiffing.js'; +import {getTestSchema} from './utilities/getTestSchema.js'; + +const schemaBeforeAfterTypes = makeRN( + 'native-module-before-after-types/NativeModuleBeforeAfterTypes', +); + +const schemaBeforeAfterTypesTypesRemoved = makeRN( + 'native-module-before-after-types-removed/NativeModuleBeforeAfterTypes', +); + +const schemaWithRN = makeRN('native-module/NativeModule'); +const schemaWithRNChanged = makeRN('native-module-changed/NativeModule'); + +const schemaGetConstants = makeRN('native-module-get-constants/NativeModule'); + +const schemaGetConstantsAddedRequiredConstant = makeRN( + 'native-module-get-constants-added-required-constant/NativeModule', +); + +const schemaGetConstantsAddedOptionalConstant = makeRN( + 'native-module-get-constants-added-optional-constant/NativeModule', +); + +const schemaGetConstantsReadOnly = makeRN( + 'native-module-get-constants-readonly/NativeModule', +); + +const schemaGetConstantsAddedRequiredConstantReadOnly = makeRN( + 'native-module-get-constants-added-required-constant-readonly/NativeModule', +); + +const schemaGetConstantsAddedOptionalConstantReadOnly = makeRN( + 'native-module-get-constants-added-optional-constant-readonly/NativeModule', +); + +const schemaBeforeAfterTypesTypeChanged = makeRN( + 'native-module-before-after-types-type-changed/NativeModuleBeforeAfterTypes', +); + +function makeRN(name: string) { + return getTestSchema(__dirname, '__fixtures__', name + '.js.flow'); +} + +const schemaWithRnNestedObject = makeRN('native-module-nested/NativeModule'); +const schemaWithRnNestedChanged = makeRN( + 'native-module-nested-changed/NativeModule', +); +const schemaWithRnNestedOptions = makeRN( + 'native-module-nested-optional/NativeModule', +); +const schemaWithRnNestedNullable = makeRN( + 'native-module-nested-nullable/NativeModule', +); +const schemaWithEnum = makeRN('native-module-with-enum/NativeModule'); +const schemaWithEnumChanges = makeRN( + 'native-module-with-enum-changes/NativeModule', +); +const schemaWithEnumTypeChanges = makeRN( + 'native-module-with-enum-type-changes/NativeModule', +); +const schemaWithEnumValueChanges = makeRN( + 'native-module-with-enum-value-changes/NativeModule', +); + +const schemaWithEnumFromNative = makeRN( + 'native-module-with-enum-from-native/NativeModule', +); +const schemaWithEnumFromNativeChanges = makeRN( + 'native-module-with-enum-from-native-changes/NativeModule', +); +const schemaWithEnumFromNativeTypeChanges = makeRN( + 'native-module-with-enum-from-native-type-changes/NativeModule', +); +const schemaWithEnumFromNativeValueChanges = makeRN( + 'native-module-with-enum-from-native-value-changes/NativeModule', +); + +const schemaWithEventEmitter = makeRN( + 'native-module-with-eventemitter/NativeModule', +); +const schemaWithEventEmitterChanges = makeRN( + 'native-module-with-eventemitter-changes/NativeModule', +); +const schemaWithOptionalArgument = makeRN( + 'native-module-with-optional-argument/NativeModule', +); +const schemaWithUnion = makeRN('native-module-with-union/NativeModule'); +const schemaWithUnionChanges = makeRN( + 'native-module-with-union-changes/NativeModule', +); +const schemaWithUnionFromNative = makeRN( + 'native-module-with-union-from-native/NativeModule', +); +const schemaWithUnionFromNativeChanges = makeRN( + 'native-module-with-union-from-native-changes/NativeModule', +); + +const schemaWithRNNativeComponent = makeRN('native-component/NativeComponent'); +const schemaNativeComponentWithCommand = makeRN( + 'native-component-with-command/NativeComponent', +); +const schemaNativeComponentWithCommandExtraArg = makeRN( + 'native-component-with-command-extra-arg/NativeComponent', +); + +const schemaNativeComponentWithCommandExtraCommand = makeRN( + 'native-component-with-command-extra-command/NativeComponent', +); + +const schemaNativeComponentWithProps = makeRN( + 'native-component-with-props/NativeComponent', +); + +const schemaNativeComponentWithPropsAddedOptionalProp = makeRN( + 'native-component-with-props-added-optional-prop/NativeComponent', +); +const schemaNativeComponentWithPropsAddedRequiredProp = makeRN( + 'native-component-with-props-added-required-prop/NativeComponent', +); + +const schemaNativeComponentWithPropsChanges = makeRN( + 'native-component-with-props-changes/NativeComponent', +); + +const schemaNativeComponentWithPropsDefaultChange = makeRN( + 'native-component-with-props-default-change/NativeComponent', +); + +const schemaNativeComponentWithPropsNestedObject = makeRN( + 'native-component-with-props-nested-object/NativeComponent', +); + +const schemaNativeComponentWithPropsNestedObjectAddedOptionalKey = makeRN( + 'native-component-with-props-nested-object-added-optional-key/NativeComponent', +); + +const schemaNativeComponentWithPropsNestedObjectAddedRequiredKey = makeRN( + 'native-component-with-props-nested-object-added-required-key/NativeComponent', +); + +const schemaNativeComponentWithPropsUnion = makeRN( + 'native-component-with-props-union/NativeComponent', +); + +const schemaNativeComponentWithPropsUnionAdded = makeRN( + 'native-component-with-props-union-added/NativeComponent', +); + +const schemaNativeComponentWithPropsArrayUnion = makeRN( + 'native-component-with-props-array-union/NativeComponent', +); + +const schemaNativeComponentWithPropsArrayUnionAdded = makeRN( + 'native-component-with-props-array-union-added/NativeComponent', +); + +describe('hasUpdates', () => { + it('is given empty sets', () => { + expect( + hasUpdatesTypes({ + newTypes: new Set(), + deprecatedTypes: new Set(), + objectTypeChanges: new Set(), + incompatibleChanges: new Set(), + }), + ).toBeFalsy(); + }); + const set = new Set(); + set.add({ + typeName: 'name', + typeInformation: { + type: 'AnyTypeAnnotation', + }, + filename: 'name', + }); + it('is given nonempty newTypes', () => { + expect( + hasUpdatesTypes({ + newTypes: set, + deprecatedTypes: new Set(), + objectTypeChanges: new Set(), + incompatibleChanges: new Set(), + }), + ).toBeTruthy(); + }); + it('is given nonempty deprecatedTypes', () => { + expect( + hasUpdatesTypes({ + newTypes: new Set(), + deprecatedTypes: set, + objectTypeChanges: new Set(), + incompatibleChanges: new Set(), + }), + ).toBeTruthy(); + }); +}); + +describe('buildSchemaDiff', () => { + it('same file, no changes', () => { + expect( + buildSchemaDiff(schemaBeforeAfterTypes, schemaBeforeAfterTypes), + ).toEqual(new Set()); + }); + it('same RN file, no changes', () => { + expect(buildSchemaDiff(schemaWithRN, schemaWithRN)).toEqual(new Set()); + }); + it('two RN files, with incompatible Spec changes', () => { + expect( + schemaDiffExporter( + Array.from(buildSchemaDiff(schemaWithRN, schemaWithRNChanged))[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.anything(), + ]), + }), + }), + ]), + }), + }), + ); + }); + it('RN file with nested object properties, no changes', () => { + expect( + buildSchemaDiff(schemaWithRnNestedObject, schemaWithRnNestedObject), + ).toEqual(new Set()); + }); + it('RN file with nested object properties, added', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithRnNestedChanged, schemaWithRnNestedObject), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + objectTypeChanges: expect.any(Object), + incompatibleChanges: expect.not.arrayContaining([ + expect.anything(), + ]), + }), + }), + ]), + }), + }), + ); + }); + it('RN file with nested object properties, removed', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithRnNestedObject, schemaWithRnNestedChanged), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.objectContaining({ + '0': expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: removedPropertiesMessage, + }), + }), + }), + }), + }), + ]), + }), + }), + ); + }); + it('RN file with nested object properties, optional removed', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithRnNestedOptions, schemaWithRnNestedObject), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.objectContaining({ + '0': expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: tooOptionalPropertiesMessage, + }), + }), + }), + }), + }), + ]), + }), + }), + ); + }); + it('RN file with nested object properties, nullable added', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaWithRnNestedNullable, + schemaWithRnNestedOptions, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.objectContaining({ + '0': expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: typeNullableChangeMessage, + }), + }), + }), + }), + }), + ]), + }), + }), + ); + }); + it('RN file with nested object properties on function, made optional', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithRnNestedOptions, schemaWithRnNestedChanged), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.objectContaining({ + '0': expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: tooOptionalPropertiesMessage, + }), + }), + }), + }), + }), + ]), + }), + }), + ); + }); + + describe('enums', () => { + describe('toNative', () => { + it('RN with enum types, same module', () => { + expect(buildSchemaDiff(schemaWithEnum, schemaWithEnum)).toEqual( + new Set(), + ); + }); + it('RN with enum types, and removal', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithEnum, schemaWithEnumChanges), + )[0], + ), + ).toEqual( + expect.objectContaining({ + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + newTypes: expect.not.arrayContaining([expect.anything()]), + deprecatedTypes: expect.not.arrayContaining([ + expect.anything(), + ]), + incompatibleChanges: expect.not.arrayContaining([ + expect.anything(), + ]), + objectTypeChanges: expect.any(Object), + }), + }), + ]), + }), + }), + ); + }); + it('RN with enum types, and addition', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithEnumChanges, schemaWithEnum), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorCode: 'addedEnumCases', + errorInformation: expect.objectContaining({ + message: addedEnumMessage, + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + it('RN with enum types, and type changes', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithEnum, schemaWithEnumTypeChanges), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + previousError: expect.objectContaining({ + previousError: expect.objectContaining({ + message: + 'EnumDeclaration member types are not the same', + }), + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + it('RN with enum types, and value changes', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithEnum, schemaWithEnumValueChanges), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + previousError: expect.objectContaining({ + previousError: expect.objectContaining({ + message: 'Enum types do not match', + previousError: expect.objectContaining({ + message: + 'Enum contained a member with a type mismatch', + mismatchedMembers: expect.arrayContaining( + [ + expect.objectContaining({ + member: 'B', + }), + ], + ), + }), + }), + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + }); + + describe('fromNative', () => { + it('RN with enum types, same module', () => { + expect( + buildSchemaDiff(schemaWithEnumFromNative, schemaWithEnumFromNative), + ).toEqual(new Set()); + }); + it('RN with enum types, and removal', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaWithEnumFromNative, + schemaWithEnumFromNativeChanges, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorCode: 'removedEnumCases', + errorInformation: expect.objectContaining({ + message: removedEnumMessage, + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + it('RN with enum types, and addition', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaWithEnumFromNativeChanges, + schemaWithEnumFromNative, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + newTypes: expect.not.arrayContaining([expect.anything()]), + deprecatedTypes: expect.not.arrayContaining([ + expect.anything(), + ]), + incompatibleChanges: expect.not.arrayContaining([ + expect.anything(), + ]), + objectTypeChanges: expect.any(Object), + }), + }), + ]), + }), + }), + ); + }); + it('RN with enum types, and type changes', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaWithEnumFromNative, + schemaWithEnumFromNativeTypeChanges, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + previousError: expect.objectContaining({ + previousError: expect.objectContaining({ + mismatchedProperties: + expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + previousError: + expect.objectContaining({ + message: + 'EnumDeclaration member types are not the same', + }), + }), + }), + ]), + }), + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + it('RN with enum types, and value changes', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaWithEnumFromNative, + schemaWithEnumFromNativeValueChanges, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + previousError: expect.objectContaining({ + previousError: expect.objectContaining({ + mismatchedProperties: + expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + previousError: + expect.objectContaining({ + message: + 'Enum types do not match', + previousError: + expect.objectContaining({ + message: + 'Enum contained a member with a type mismatch', + mismatchedMembers: + expect.arrayContaining([ + expect.objectContaining( + { + member: 'B', + }, + ), + ]), + }), + }), + }), + }), + ]), + }), + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + }); + }); + + it('RN with eventEmitters, same module', () => { + expect( + buildSchemaDiff(schemaWithEventEmitter, schemaWithEventEmitter), + ).toEqual(new Set()); + }); + + it('RN with eventEmitters, and eventType changes', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaWithEventEmitter, + schemaWithEventEmitterChanges, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: + 'Object contained properties with type mismatches', + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + message: 'has conflicting type changes', + previousError: expect.objectContaining({ + message: + 'EventEmitter eventTypes are not equivalent', + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + + it('RN with adding an optional argument to function', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithOptionalArgument, schemaWithRN), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: + 'Object contained a property with a type mismatch', + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + message: 'has conflicting type changes', + previousError: expect.objectContaining({ + message: + 'Function types have differing length of arguments', + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + + it('RN with removing an optional argument to function', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithRN, schemaWithOptionalArgument), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: + 'Object contained a property with a type mismatch', + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + message: 'has conflicting type changes', + previousError: expect.objectContaining({ + message: + 'Function types have differing length of arguments', + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + + it('RN with union types, same module', () => { + expect(buildSchemaDiff(schemaWithUnion, schemaWithUnion)).toEqual( + new Set(), + ); + }); + + it('RN with union types, and addition', () => { + expect( + schemaDiffExporter( + Array.from(buildSchemaDiff(schemaWithUnionChanges, schemaWithUnion))[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: addedUnionMessage, + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + it('RN with union types, and removal', () => { + expect( + schemaDiffExporter( + Array.from(buildSchemaDiff(schemaWithUnion, schemaWithUnionChanges))[0], + ), + ).toEqual( + expect.objectContaining({ + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + newTypes: expect.not.arrayContaining([expect.anything()]), + deprecatedTypes: expect.not.arrayContaining([ + expect.anything(), + ]), + incompatibleChanges: expect.not.arrayContaining([ + expect.anything(), + ]), + objectTypeChanges: expect.any(Object), + }), + }), + ]), + }), + }), + ); + }); + it('RN with union types from native, and addition', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaWithUnionFromNativeChanges, + schemaWithUnionFromNative, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + newTypes: expect.not.arrayContaining([expect.anything()]), + deprecatedTypes: expect.not.arrayContaining([ + expect.anything(), + ]), + incompatibleChanges: expect.not.arrayContaining([ + expect.anything(), + ]), + objectTypeChanges: expect.any(Object), + }), + }), + ]), + }), + }), + ); + }); + it('RN with union types from native, and removal', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaWithUnionFromNative, + schemaWithUnionFromNativeChanges, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: removedUnionMessage, + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + + describe('NativeComponent', () => { + it('module to component; deprecated + new', () => { + expect( + Array.from(buildSchemaDiff(schemaWithRNNativeComponent, schemaWithRN)), + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'NativeComponent', + status: 'new', + }), + expect.objectContaining({ + name: 'NativeModule', + status: 'deprecated', + }), + ]), + ); + }); + + it('component changed to module; deprecated + new', () => { + expect( + Array.from(buildSchemaDiff(schemaWithRN, schemaWithRNNativeComponent)), + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'NativeModule', + status: 'new', + }), + expect.objectContaining({ + name: 'NativeComponent', + status: 'deprecated', + }), + ]), + ); + }); + + it('same RN component, no changes', () => { + expect( + buildSchemaDiff( + schemaWithRNNativeComponent, + schemaWithRNNativeComponent, + ), + ).toEqual(new Set()); + }); + + describe('commands', () => { + it('Command with additional arg', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaNativeComponentWithCommandExtraArg, + schemaNativeComponentWithCommand, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + previousError: expect.objectContaining({ + message: + 'Function types have differing length of arguments', + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + + it('Command with arg removed', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaNativeComponentWithCommand, + schemaNativeComponentWithCommandExtraArg, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + mismatchedProperties: expect.arrayContaining([ + expect.objectContaining({ + fault: expect.objectContaining({ + previousError: expect.objectContaining({ + message: + 'Function types have differing length of arguments', + }), + }), + }), + ]), + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + + it('1 command to 2 commands is allowed', () => { + // This could technically result in an OTA issue if the command + // is called immediately and the native side throws on unrecognized + // commands. However, there is no way to do feature detection + // of commands today to protect against that. + // We are choosing to allow this change to be made since we previously + // had no protection for commands at all. + // See https://fb.workplace.com/groups/615693552291894/posts/1905124013348835 + // for more information. + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithCommandExtraCommand, + schemaNativeComponentWithCommand, + ), + ).status, + ).toEqual('ok'); + }); + + it('2 commands to 1 command is allowed', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithCommand, + schemaNativeComponentWithCommandExtraCommand, + ), + ).status, + ).toEqual('ok'); + }); + + it('0 commands to 1 command is allowed', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithCommand, + schemaWithRNNativeComponent, + ), + ).status, + ).toEqual('ok'); + }); + + it('1 command to 0 commands is allowed', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaWithRNNativeComponent, + schemaNativeComponentWithCommand, + ), + ).status, + ).toEqual('ok'); + }); + }); + + describe('props', () => { + it('prop with changed value', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaNativeComponentWithPropsChanges, + schemaNativeComponentWithProps, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: + 'Object contained a property with a type mismatch', + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + + it('prop with changed default value should be okay', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithPropsDefaultChange, + schemaNativeComponentWithProps, + ), + ).status, + ).toBe('ok'); + }); + + it('props with optional prop added should be okay', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithPropsAddedOptionalProp, + schemaNativeComponentWithProps, + ), + ).status, + ).toBe('ok'); + }); + + it('props with optional prop removed should be okay', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithProps, + schemaNativeComponentWithPropsAddedOptionalProp, + ), + ).status, + ).toBe('ok'); + }); + + it('props with required prop added should be okay', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithPropsAddedRequiredProp, + schemaNativeComponentWithProps, + ), + ).status, + ).toBe('ok'); + }); + + it('props with required prop removed should fail', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaNativeComponentWithProps, + schemaNativeComponentWithPropsAddedRequiredProp, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.objectContaining({ + '0': expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: removedPropertiesMessage, + }), + }), + }), + }), + }), + ]), + }), + }), + ); + }); + + it('props with nested object, adding an optional key should be okay', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithPropsNestedObjectAddedOptionalKey, + schemaNativeComponentWithPropsNestedObject, + ), + ).status, + ).toBe('ok'); + }); + + it('props with nested object, removing an optional key should be okay', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithPropsNestedObject, + schemaNativeComponentWithPropsNestedObjectAddedOptionalKey, + ), + ).status, + ).toBe('ok'); + }); + + it('props with nested object, adding a required key should be okay', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithPropsNestedObjectAddedRequiredKey, + schemaNativeComponentWithPropsNestedObject, + ), + ).status, + ).toBe('ok'); + }); + + it('props with union and added option should fail', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaNativeComponentWithPropsUnionAdded, + schemaNativeComponentWithPropsUnion, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.objectContaining({ + '0': expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: addedUnionMessage, + }), + }), + }), + }), + }), + ]), + }), + }), + ); + }); + + it('props with union and removed option should be ok', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithPropsUnion, + schemaNativeComponentWithPropsUnionAdded, + ), + ).status, + ).toBe('ok'); + }); + + it('props with array union and added option should fail', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff( + schemaNativeComponentWithPropsArrayUnionAdded, + schemaNativeComponentWithPropsArrayUnion, + ), + )[0], + ), + ).toEqual( + expect.objectContaining({ + framework: 'ReactNative', + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.objectContaining({ + '0': expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: addedUnionMessage, + }), + }), + }), + }), + }), + ]), + }), + }), + ); + }); + + it('props with array union and removed option should be ok', () => { + // This is risky due to these being stored from component props codegen + // in C++ ints via bitshift which might change the resulting + // int value for the same inputs. + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaNativeComponentWithPropsArrayUnion, + schemaNativeComponentWithPropsArrayUnionAdded, + ), + ).status, + ).toBe('ok'); + }); + }); + }); + + describe('RN new codegen', () => { + describe('Native Module', () => { + it('removed', () => { + expect( + summarizeDiffSet( + buildSchemaDiff(schemaWithRNNativeComponent, schemaWithRN), + ).status, + ).toEqual('patchable'); + }); + it('added', () => { + expect( + schemaDiffExporter( + Array.from( + buildSchemaDiff(schemaWithRN, schemaWithRNNativeComponent), + )[0], + ), + ).toEqual( + expect.objectContaining({ + name: 'NativeModule', + status: 'new', + }), + ); + }); + }); + }); +}); + +describe('RN NativeModule getConstants type diffing', () => { + it('should not allow required constants to be added to NativeModules', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsAddedRequiredConstant, // new + schemaGetConstants, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('incompatible'); + expect(schemaDiffExporter(Array.from(schemaDiff)[0])).toEqual( + expect.objectContaining({ + status: expect.objectContaining({ + incompatibleSpecs: expect.arrayContaining([ + expect.objectContaining({ + changeInformation: expect.objectContaining({ + incompatibleChanges: expect.arrayContaining([ + expect.objectContaining({ + errorInformation: expect.objectContaining({ + message: addedPropertiesMessage, + }), + }), + ]), + }), + }), + ]), + }), + }), + ); + }); + + it('should allow optional constants to be added to NativeModules', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsAddedOptionalConstant, // new + schemaGetConstants, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('ok'); + }); + + it('should allow removal of required constants from NativeModules', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstants, // new + schemaGetConstantsAddedRequiredConstant, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('ok'); + }); + + it('should allow removal of optional constants from NativeModules', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstants, // new + schemaGetConstantsAddedOptionalConstant, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('ok'); + }); + + it('should not allow promotion of optional constants to required constants', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsAddedRequiredConstant, // new + schemaGetConstantsAddedOptionalConstant, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('incompatible'); + }); + + it('should allow required constants to become optional constants', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsAddedOptionalConstant, // new + schemaGetConstantsAddedRequiredConstant, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('ok'); + }); +}); + +describe('RN NativeModule getConstants $ReadOnly type diffing', () => { + it('should not allow required constants to be added to NativeModules', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsAddedRequiredConstantReadOnly, // new + schemaGetConstantsReadOnly, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('incompatible'); + }); + + it('should allow optional constants to be added to NativeModules', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsAddedOptionalConstantReadOnly, // new + schemaGetConstantsReadOnly, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('ok'); + }); + + it('should allow removal of required constants from NativeModules', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsReadOnly, // new + schemaGetConstantsAddedRequiredConstantReadOnly, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('ok'); + }); + + it('should allow removal of optional constants from NativeModules', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsReadOnly, // new + schemaGetConstantsAddedOptionalConstantReadOnly, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('ok'); + }); + + it('should not allow promotion of optional constants to required constants', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsAddedRequiredConstantReadOnly, // new + schemaGetConstantsAddedOptionalConstantReadOnly, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('incompatible'); + }); + + it('should allow required constants to become optional constants', () => { + const schemaDiff = buildSchemaDiff( + schemaGetConstantsAddedOptionalConstantReadOnly, // new + schemaGetConstantsAddedRequiredConstantReadOnly, // old + ); + + expect(summarizeDiffSet(schemaDiff).status).toBe('ok'); + }); +}); + +describe('summarizeDiffSet', () => { + const set = new Set(); + it('is given empty set', () => { + expect(summarizeDiffSet(set).status).toBe('ok'); + }); + it('is given set with one new', () => { + set.add({ + name: 'SampleName', + framework: 'ReactNative', + status: 'new', + }); + expect(summarizeDiffSet(set).status).toBe('patchable'); + }); + it('is given complex set', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaBeforeAfterTypes, + schemaBeforeAfterTypesTypesRemoved, + ), + ).status, + ).toBe('incompatible'); + }); + it('is given a diff that has incompatible changes in the boundary', () => { + expect( + summarizeDiffSet( + buildSchemaDiff( + schemaBeforeAfterTypes, + schemaBeforeAfterTypesTypeChanged, + ), + ).status, + ).toBe('incompatible'); + }); +}); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-all-types/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-all-types/NativeComponent.js.flow new file mode 100644 index 00000000000..ad22b238464 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-all-types/NativeComponent.js.flow @@ -0,0 +1,169 @@ +/** + * 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 {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {ColorValue} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type { + EdgeInsetsValue, + PointValue, +} from 'react-native/Libraries/StyleSheet/StyleSheetTypes'; +import type {RootTag} from 'react-native/Libraries/TurboModule/RCTExport'; +import type { + Double, + Float, + Int32, + UnsafeMixed, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type NativeProps = $ReadOnly<{ + ...ViewProps, + bool: boolean, + string: string, + double: Double, + float: Float, + int: Int32, + mixed: UnsafeMixed, + + optionalBool?: WithDefault, + optionalString?: WithDefault, + optionalStringDiffDefault?: WithDefault, + optionalDouble?: WithDefault, + optionalFloat?: WithDefault, + optionalInt?: WithDefault, + + reserverdEdgeInsets: EdgeInsetsValue, + reservedPointValue: PointValue, + + stringUnion?: WithDefault<'foo' | 'bar', 'bar'>, + intUnion?: WithDefault<1 | 2, 1>, + + nestedObject: $ReadOnly<{ + string: string, + color: ColorValue, + }>, + + nestedObjectChanged: $ReadOnly<{ + string: boolean, + color: ColorValue, + }>, + + arrayStringUnion?: WithDefault<$ReadOnlyArray<'foo' | 'bar'>, 'foo'>, + arrayStringUnionExtra?: WithDefault< + $ReadOnlyArray<'foo' | 'bar' | 'baz'>, + 'bar', + >, + + arrayNestedObject: $ReadOnlyArray< + $ReadOnly<{ + string: string, + color: ColorValue, + }>, + >, + + arrayNestedObjectChange: $ReadOnlyArray< + $ReadOnly<{ + string: boolean, + color: ColorValue, + }>, + >, +}>; + +type NativeType = HostComponent; + +type Int = Int32; +type Str = string; +type Bool = boolean; +type Fl = Float; +type Dbl = Double; +type CustomObj = {foo: string, bar: number}; +type CustomObj2 = {a: Int32, b: string}; + +interface NativeCommands { + +methodInt: (viewRef: React.ElementRef, a: Int32) => void; + +methodIntString: ( + viewRef: React.ElementRef, + a: Int32, + b: string, + ) => void; + +methodString: (viewRef: React.ElementRef, a: string) => void; + +methodBool: (viewRef: React.ElementRef, a: boolean) => void; + +methodFloat: (viewRef: React.ElementRef, a: Float) => void; + +methodDouble: (viewRef: React.ElementRef, a: Double) => void; + + +methodIntAlias: (viewRef: React.ElementRef, a: Int) => void; + +methodStringAlias: (viewRef: React.ElementRef, a: Str) => void; + +methodBoolAlias: (viewRef: React.ElementRef, a: Bool) => void; + +methodFloatAlias: (viewRef: React.ElementRef, a: Fl) => void; + +methodDoubleAlias: (viewRef: React.ElementRef, a: Dbl) => void; + + +methodIntArray: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodStringArray: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodBoolArray: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodFloatArray: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodDoubleArray: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodCustomObjArray: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodCustomObj2Array: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + + +methodRootTag: (viewRef: React.ElementRef, a: RootTag) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: [ + 'methodInt', + 'methodIntString', + 'methodString', + 'methodBool', + 'methodFloat', + 'methodDouble', + 'methodIntAlias', + 'methodStringAlias', + 'methodBoolAlias', + 'methodFloatAlias', + 'methodDoubleAlias', + 'methodIntArray', + 'methodStringArray', + 'methodBoolArray', + 'methodFloatArray', + 'methodDoubleArray', + 'methodCustomObjArray', + 'methodCustomObj2Array', + 'methodRootTag', + ], +}); + +export default codegenNativeComponent( + 'NativeComponent', +) as NativeType; diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-all-basic-types-arrays/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-all-basic-types-arrays/NativeComponent.js.flow new file mode 100644 index 00000000000..1c55d96e879 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-all-basic-types-arrays/NativeComponent.js.flow @@ -0,0 +1,60 @@ +/** + * 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 {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Double, + Float, + Int32, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type NativeProps = $ReadOnly<{ + ...ViewProps, +}>; + +type NativeType = HostComponent; + +interface NativeCommands { + +methodInt: (viewRef: React.ElementRef, a: Array) => void; + +methodString: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodBool: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodFloat: ( + viewRef: React.ElementRef, + a: Array, + ) => void; + +methodDouble: ( + viewRef: React.ElementRef, + a: Array, + ) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: [ + 'methodInt', + 'methodString', + 'methodBool', + 'methodFloat', + 'methodDouble', + ], +}); + +export default codegenNativeComponent( + 'NativeComponent', +) as NativeType; diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-all-basic-types/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-all-basic-types/NativeComponent.js.flow new file mode 100644 index 00000000000..eac4e790272 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-all-basic-types/NativeComponent.js.flow @@ -0,0 +1,48 @@ +/** + * 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 {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Double, + Float, + Int32, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type NativeProps = $ReadOnly<{ + ...ViewProps, +}>; + +type NativeType = HostComponent; + +interface NativeCommands { + +methodInt: (viewRef: React.ElementRef, a: Int32) => void; + +methodString: (viewRef: React.ElementRef, a: string) => void; + +methodBool: (viewRef: React.ElementRef, a: boolean) => void; + +methodFloat: (viewRef: React.ElementRef, a: Float) => void; + +methodDouble: (viewRef: React.ElementRef, a: Double) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: [ + 'methodInt', + 'methodString', + 'methodBool', + 'methodFloat', + 'methodDouble', + ], +}); + +export default codegenNativeComponent( + 'NativeComponent', +) as NativeType; diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-changed/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-changed/NativeComponent.js.flow new file mode 100644 index 00000000000..a0c2bf47be8 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-changed/NativeComponent.js.flow @@ -0,0 +1,33 @@ +/** + * 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 {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; + +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type NativeProps = $ReadOnly<{ + ...ViewProps, +}>; + +type NativeType = HostComponent; + +interface NativeCommands { + +methodInt: (viewRef: React.ElementRef, a: string) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['methodInt'], +}); + +export default codegenNativeComponent( + 'NativeComponent', +) as NativeType; diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-extra-arg/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-extra-arg/NativeComponent.js.flow new file mode 100644 index 00000000000..4a52639b60d --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-extra-arg/NativeComponent.js.flow @@ -0,0 +1,38 @@ +/** + * 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 {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {Int32} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type NativeProps = $ReadOnly<{ + ...ViewProps, +}>; + +type NativeType = HostComponent; + +interface NativeCommands { + +methodInt: ( + viewRef: React.ElementRef, + a: Int32, + b: string, + ) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['methodInt'], +}); + +export default codegenNativeComponent( + 'NativeComponent', +) as NativeType; diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-extra-command/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-extra-command/NativeComponent.js.flow new file mode 100644 index 00000000000..8159b7a528f --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command-extra-command/NativeComponent.js.flow @@ -0,0 +1,35 @@ +/** + * 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 {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {Int32} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type NativeProps = $ReadOnly<{ + ...ViewProps, +}>; + +type NativeType = HostComponent; + +interface NativeCommands { + +methodInt: (viewRef: React.ElementRef, a: Int32) => void; + +methodString: (viewRef: React.ElementRef, a: string) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['methodInt', 'methodString'], +}); + +export default codegenNativeComponent( + 'NativeComponent', +) as NativeType; diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command/NativeComponent.js.flow new file mode 100644 index 00000000000..38e623fb18d --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-command/NativeComponent.js.flow @@ -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 {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {Int32} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type NativeProps = $ReadOnly<{ + ...ViewProps, +}>; + +type NativeType = HostComponent; + +interface NativeCommands { + +methodInt: (viewRef: React.ElementRef, a: Int32) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['methodInt'], +}); + +export default codegenNativeComponent( + 'NativeComponent', +) as NativeType; diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-added-optional-prop/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-added-optional-prop/NativeComponent.js.flow new file mode 100644 index 00000000000..6e52c2d4a76 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-added-optional-prop/NativeComponent.js.flow @@ -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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + text: string, + backgroundColor?: WithDefault, + height?: WithDefault, + width?: WithDefault, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-added-required-prop/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-added-required-prop/NativeComponent.js.flow new file mode 100644 index 00000000000..3b4b3f8956b --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-added-required-prop/NativeComponent.js.flow @@ -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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + text: string, + backgroundColor?: WithDefault, + height?: WithDefault, + width: Int32, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-all-types/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-all-types/NativeComponent.js.flow new file mode 100644 index 00000000000..47f800cdaad --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-all-types/NativeComponent.js.flow @@ -0,0 +1,39 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Double, + Float, + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + bool: boolean, + string: string, + double: Double, + float: Float, + int: Int32, + stringUnion: WithDefault<'foo' | 'bar', 'bar'>, + intUnion: WithDefault<1 | 2, 1>, + mixed: mixed, + + backgroundColor?: WithDefault, + height?: WithDefault, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-array-union-added/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-array-union-added/NativeComponent.js.flow new file mode 100644 index 00000000000..b713bfa1c1f --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-array-union-added/NativeComponent.js.flow @@ -0,0 +1,24 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {WithDefault} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + sizes?: WithDefault<$ReadOnlyArray<'small' | 'large' | 'huge'>, 'small'>, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-array-union/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-array-union/NativeComponent.js.flow new file mode 100644 index 00000000000..da55640b3d2 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-array-union/NativeComponent.js.flow @@ -0,0 +1,24 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {WithDefault} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + sizes?: WithDefault<$ReadOnlyArray<'small' | 'large'>, 'small'>, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-changes/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-changes/NativeComponent.js.flow new file mode 100644 index 00000000000..a816e4a42d6 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-changes/NativeComponent.js.flow @@ -0,0 +1,29 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + text: Int32, + backgroundColor?: WithDefault, + height?: WithDefault, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-default-change/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-default-change/NativeComponent.js.flow new file mode 100644 index 00000000000..f7768a1ab00 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-default-change/NativeComponent.js.flow @@ -0,0 +1,29 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + text: string, + backgroundColor?: WithDefault, + height?: WithDefault, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object-added-optional-key/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object-added-optional-key/NativeComponent.js.flow new file mode 100644 index 00000000000..412d465a48c --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object-added-optional-key/NativeComponent.js.flow @@ -0,0 +1,35 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Float, + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + text: string, + backgroundColor?: WithDefault, + height?: WithDefault, + pins: $ReadOnly<{ + x: Float, + y: Float, + color?: WithDefault, + }>, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object-added-required-key/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object-added-required-key/NativeComponent.js.flow new file mode 100644 index 00000000000..31b5bf6aeb6 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object-added-required-key/NativeComponent.js.flow @@ -0,0 +1,35 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Float, + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + text: string, + backgroundColor?: WithDefault, + height?: WithDefault, + pins: $ReadOnly<{ + x: Float, + y: Float, + color: string, + }>, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object/NativeComponent.js.flow new file mode 100644 index 00000000000..1c423bbae7e --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-nested-object/NativeComponent.js.flow @@ -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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Float, + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + text: string, + backgroundColor?: WithDefault, + height?: WithDefault, + pins: $ReadOnly<{ + x: Float, + y: Float, + }>, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-union-added/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-union-added/NativeComponent.js.flow new file mode 100644 index 00000000000..2a4fcf90d8f --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-union-added/NativeComponent.js.flow @@ -0,0 +1,24 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {WithDefault} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + size?: WithDefault<'small' | 'large' | 'huge', 'small'>, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-union/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-union/NativeComponent.js.flow new file mode 100644 index 00000000000..ce3187bd455 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props-union/NativeComponent.js.flow @@ -0,0 +1,24 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type {WithDefault} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + size?: WithDefault<'small' | 'large', 'small'>, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props/NativeComponent.js.flow new file mode 100644 index 00000000000..d8f9bd8777e --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component-with-props/NativeComponent.js.flow @@ -0,0 +1,29 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { + Int32, + WithDefault, +} from 'react-native/Libraries/Types/CodegenTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, + text: string, + backgroundColor?: WithDefault, + height?: WithDefault, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component/NativeComponent.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component/NativeComponent.js.flow new file mode 100644 index 00000000000..56b3fba9ee6 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-component/NativeComponent.js.flow @@ -0,0 +1,22 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from 'react-native'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export type Props = $ReadOnly<{ + ...ViewProps, +}>; + +export default (codegenNativeComponent( + 'NativeComponent', +): HostComponent); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types-removed/NativeModuleBeforeAfterTypes.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types-removed/NativeModuleBeforeAfterTypes.js.flow new file mode 100644 index 00000000000..93fbf957584 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types-removed/NativeModuleBeforeAfterTypes.js.flow @@ -0,0 +1,33 @@ +/** + * 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 + */ + +/* eslint-disable no-unused-vars */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type SimpleObject = {||}; + +type BeforeMismatchGeneric = {||}; + +type BeforeMatchingGeneric = {||}; +type AfterMatchingGeneric = {||}; + +export interface Spec extends TurboModule { + +simpleObject: (a: SimpleObject) => void; + +beforeMismatchGeneric: (a: BeforeMismatchGeneric) => void; + + +beforeMatchingGeneric: (a: BeforeMatchingGeneric) => void; + +afterMatchingGeneric: (a: AfterMatchingGeneric) => void; +} + +(TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types-type-changed/NativeModuleBeforeAfterTypes.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types-type-changed/NativeModuleBeforeAfterTypes.js.flow new file mode 100644 index 00000000000..e2b9af63ab9 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types-type-changed/NativeModuleBeforeAfterTypes.js.flow @@ -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 + */ + +/* eslint-disable no-unused-vars */ +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type SimpleObject = {||}; + +type BeforeMismatchGeneric = {||}; +type AfterMismatchGeneric = {||}; + +type BeforeMatchingGeneric = {||}; +type AfterMatchingGeneric = {||}; + +type BooleanType = boolean; +type StringType = string; + +type SimpleFunction = (a: string) => string; +type SimpleFunction2 = (a: string) => number; +type SimpleFunction3 = (foo: string) => string; +type SimpleFunction4 = (a: number) => string; + +type SimpleUnion = 'str1' | 'str2'; +type SimpleUnionLonger = 'str1' | 'str2' | 'str3'; +type SimpleUnion2 = 4 | 5; +type SimpleUnion3 = {} | {key: 'value'}; +type SimpleUnion4 = BooleanType | StringType | SimpleObject; + +type FunctionReferencingUnion = (a: SimpleUnion) => void; +type FunctionReferencingUnion2 = (a: SimpleUnion2) => void; + +type SimpleArray = Array; +// changed from Array to Array +type SimpleArrayChange = Array; + +export interface Spec extends TurboModule { + +exampleFunction: (a: SimpleObject) => void; + +simpleObject: (a: SimpleObject) => void; + +beforeMismatchGeneric: (a: BeforeMismatchGeneric) => void; + +afterMismatchGeneric: (a: AfterMismatchGeneric) => void; + + +beforeMatchingGeneric: (a: BeforeMatchingGeneric) => void; + +afterMatchingGeneric: (a: AfterMatchingGeneric) => void; + + +booleanType: (a: BooleanType) => void; + +stringType: (a: StringType) => void; + + +simpleFunction: (a: string) => string; + +simpleFunction2: (a: string) => number; + +simpleFunction3: (foo: string) => string; + +simpleFunction4: (a: number) => string; + + +simpleUnion: (a: SimpleUnion) => void; + +simpleUnionLonger: (a: SimpleUnionLonger) => void; + +simpleUnion2: (a: SimpleUnion2) => void; + +simpleUnion3: (a: SimpleUnion3) => void; + +simpleUnion4: (a: SimpleUnion4) => void; + + +simpleArray: (a: SimpleArray) => void; + +simpleArrayChange: (a: SimpleArrayChange) => void; + } + + (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', + ): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types/NativeModuleBeforeAfterTypes.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types/NativeModuleBeforeAfterTypes.js.flow new file mode 100644 index 00000000000..e5ba221a889 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-before-after-types/NativeModuleBeforeAfterTypes.js.flow @@ -0,0 +1,66 @@ +/** + * 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 + */ + +/* eslint-disable no-unused-vars */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type SimpleObject = {}; + +type BeforeMismatchGeneric = {}; +type AfterMismatchGeneric = {}; + +type BeforeMatchingGeneric = {}; +type AfterMatchingGeneric = {}; + +type BooleanType = boolean; +type StringType = string; + +type SimpleUnion = 'str1' | 'str2'; +type SimpleUnionLonger = 'str1' | 'str2' | 'str3'; +type SimpleUnion2 = 4 | 5; +type SimpleUnion3 = {} | {key: 'value'}; +type SimpleUnion4 = BooleanType | StringType | SimpleObject; + +type SimpleArray = Array; +type SimpleArrayChange = Array; + + +export interface Spec extends TurboModule { + +exampleFunction: (a: SimpleObject) => void; + +simpleObject: (a: SimpleObject) => void; + +beforeMismatchGeneric: (a: BeforeMismatchGeneric) => void; + +afterMismatchGeneric: (a: AfterMismatchGeneric) => void; + + +beforeMatchingGeneric: (a: BeforeMatchingGeneric) => void; + +afterMatchingGeneric: (a: AfterMatchingGeneric) => void; + + +booleanType: (a: BooleanType) => void; + +stringType: (a: StringType) => void; + + +simpleFunction: (a: string) => string; + +simpleFunction2: (a: string) => number; + +simpleFunction3: (foo: string) => string; + +simpleFunction4: (a: number) => string; + + +simpleUnion: (a: SimpleUnion) => void; + +simpleUnionLonger: (a: SimpleUnionLonger) => void; + +simpleUnion2: (a: SimpleUnion2) => void; + +simpleUnion3: (a: SimpleUnion3) => void; + +simpleUnion4: (a: SimpleUnion4) => void; + + +simpleArray: (a: SimpleArray) => void; + +simpleArrayChange: (a: SimpleArrayChange) => void; +} + +(TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-changed/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-changed/NativeModule.js.flow new file mode 100644 index 00000000000..a7800a22f51 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-changed/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +exampleConstant: string, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-optional-constant-readonly/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-optional-constant-readonly/NativeModule.js.flow new file mode 100644 index 00000000000..32b27c558c5 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-optional-constant-readonly/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => $ReadOnly<{ + constant?: string, + }>; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-optional-constant/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-optional-constant/NativeModule.js.flow new file mode 100644 index 00000000000..82885474f32 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-optional-constant/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +constant?: string, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-required-constant-readonly/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-required-constant-readonly/NativeModule.js.flow new file mode 100644 index 00000000000..a1ae7696fe1 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-required-constant-readonly/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => $ReadOnly<{ + constant: string, + }>; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-required-constant/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-required-constant/NativeModule.js.flow new file mode 100644 index 00000000000..ee6abb89868 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-added-required-constant/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +constant: string, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-readonly/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-readonly/NativeModule.js.flow new file mode 100644 index 00000000000..878d2267f2d --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants-readonly/NativeModule.js.flow @@ -0,0 +1,22 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => $ReadOnly<{}>; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants/NativeModule.js.flow new file mode 100644 index 00000000000..633e17bca56 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-get-constants/NativeModule.js.flow @@ -0,0 +1,22 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => {}; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-alias-changed/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-alias-changed/NativeModule.js.flow new file mode 100644 index 00000000000..8a665abdf2d --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-alias-changed/NativeModule.js.flow @@ -0,0 +1,23 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type MyType = () => Promise; + +export interface Spec extends TurboModule { + +exampleFunction: (a: MyType, b: number) => void; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-alias/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-alias/NativeModule.js.flow new file mode 100644 index 00000000000..79bf39c71bb --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-alias/NativeModule.js.flow @@ -0,0 +1,23 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type MyType = () => Promise; + +export interface Spec extends TurboModule { + +exampleFunction: (a: MyType, b: number) => void; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-changed/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-changed/NativeModule.js.flow new file mode 100644 index 00000000000..2ee8a14bbc3 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-changed/NativeModule.js.flow @@ -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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: ( + a: { + a1: string, + a2: number, + a3: string, + ... + }, + b: number, + ) => void; + +getConstants: () => { + +exampleConstant: string, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-nullable/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-nullable/NativeModule.js.flow new file mode 100644 index 00000000000..4762d412f49 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-nullable/NativeModule.js.flow @@ -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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: ( + a: ?{ + a1: string, + a2?: number, + a3: string, + ... + }, + b: number, + ) => void; + +getConstants: () => { + +exampleConstant: string, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-optional/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-optional/NativeModule.js.flow new file mode 100644 index 00000000000..eb7e82106bd --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested-optional/NativeModule.js.flow @@ -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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: ( + a: { + a1: string, + a2?: number, + a3: string, + ... + }, + b: number, + ) => void; + +getConstants: () => { + +exampleConstant: string, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested/NativeModule.js.flow new file mode 100644 index 00000000000..6cf8cdf2f81 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-nested/NativeModule.js.flow @@ -0,0 +1,31 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: ( + a: { + a1: string, + a2: number, + ... + }, + b: number, + ) => void; + +getConstants: () => { + +exampleConstant: string, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-type-diffing-types/NativeTypeDiffingTypes.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-type-diffing-types/NativeTypeDiffingTypes.js.flow new file mode 100644 index 00000000000..a215dd4bf21 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-type-diffing-types/NativeTypeDiffingTypes.js.flow @@ -0,0 +1,218 @@ +/** + * 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 + */ + +/* eslint-disable no-unused-vars */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type StringLiteral0 = 'foo'; +type StringLiteral1 = 'bar'; + +type NumericLiteral0 = 1; +type NumericLiteral1 = 2; + +type StringLiteralUnion0 = 'a' | 'b' | 'c'; +type StringLiteralUnion1 = 'a' | 'b' | 'c' | 'd'; +type StringLiteralUnion2 = 'a' | 'x' | 'c'; + +type Tuple0 = [string, number]; +type Tuple1 = [string, number, number]; +type Tuple2 = [number, number]; + +type ReferencedType = {...}; +type ReferencedType2 = {...}; +type ReferencedType3 = {prop0: string, ...}; + +type ReferenceType = ReferencedType; +type ReferenceType2 = ReferencedType2; +type ReferenceType3 = ReferencedType3; +type ReferenceGeneric = ReferencedType; +type ReferenceGeneric2 = ReferencedType; + +type ObjectTypeWithProps = { + prop1: string, + prop2: number, + prop3: boolean, + ... +}; +type ObjectTypeLessProps = { + prop1: string, + prop3: boolean, + ... +}; + +type ObjectTypeWithDifferentProps = { + prop0: string, + prop4: number, + prop5: boolean, + ... +}; + +type ObjectTypeWithNesting = { + prop1: { + subprop1: string, + subprop2: number, + ... + }, + prop2: number, + ... +}; +type ObjectTypeWithChanges = { + prop1: number, + prop2: { + subprop1: string, + subprp2: number, + ... + }, + ... +}; +type ObjectTypeWithNestedChanges = { + prop1: { + subprop1: string, + subprop2: string, + ... + }, + prop2: number, + ... +}; +type ObjectTypeWithTwoOptionals = { + prop1?: string, + prop2: number, + prop3?: boolean, + ... +}; +type ObjectTypeWithOptionalNestedChange = { + prop1?: { + subprop1: string, + subprop2: number, + subprop3: string, + ... + }, + prop2: number, + ... +}; + +type NonNullableType = Object; +type NullableType = ?Object; + +type GenericObjectType = Object; +type UnsafeObjectType = UnsafeObject; + +type OptionalTypeLessProps = ?{ + prop1: string, + prop3: boolean, + ... +}; + +type ObjectTypeLiteral1 = { + prop1: number, + prop2: { + subprop1: string, + subprp2: number, + }, +}; + +type ObjectTypeLiteral2 = { + prop1: number, + prop2: { + subprop1: string, + subprp2: number, + }, +}; + +type ObjectTypeLiteral1TypeAlias = ObjectTypeLiteral1; +type ObjectTypeLiteral2TypeAlias = ObjectTypeLiteral2; + +type MapWithKey = {[a: string]: ?number}; +type MapWithKey2 = {[a: string]: ?string}; + +enum Enum { + A = 1, + B = 2, + C = 3, + D = 4, +} +enum EnumWithTypeChange { + A = '1', + B = '2', + C = '3', + D = '4', +} +enum EnumWithRemoval { + A = 1, + B = 2, + C = 3, +} +enum EnumWithValueChange { + A = 1, + B = 2, + C = 3, + D = 14, +} + +enum EnumUnsorted { + C = 3, + D = 4, + B = 2, + A = 1, +} + +export interface Spec extends TurboModule { + +stringLiteral0: (a: StringLiteral0) => void; + +stringLiteral1: (a: StringLiteral1) => void; + + +numericLiteral0: (a: NumericLiteral0) => void; + +numericLiteral1: (a: NumericLiteral1) => void; + + +stringLiteralUnion0: (a: StringLiteralUnion0) => void; + +stringLiteralUnion1: (a: StringLiteralUnion1) => void; + +stringLiteralUnion2: (a: StringLiteralUnion2) => void; + + +referenceType: (a: ReferenceType) => void; + +referenceType2: (a: ReferenceType2) => void; + +referenceType3: (a: ReferenceType3) => void; + + +referenceGeneric: (a: ReferenceGeneric) => void; + +referenceGeneric2: (a: ReferenceGeneric2) => void; + + +objectTypeWithProps: (a: ObjectTypeWithProps) => void; + +objectTypeLessProps: (a: ObjectTypeLessProps) => void; + +objectTypeWithDifferentProps: (a: ObjectTypeWithDifferentProps) => void; + +objectTypeWithNesting: (a: ObjectTypeWithNesting) => void; + +objectTypeWithChanges: (a: ObjectTypeWithChanges) => void; + +objectTypeWithNestedChanges: (a: ObjectTypeWithNestedChanges) => void; + +objectTypeWithTwoOptionals: (a: ObjectTypeWithTwoOptionals) => void; + +objectTypeWithOptionalNestedChange: (a: ObjectTypeWithOptionalNestedChange) => void; + +nonNullableType: (a: NonNullableType) => void; + +nullableType: (a: NullableType) => void; + +genericObjectType: (a: GenericObjectType) => void; + +unsafeObjectType: (a: UnsafeObjectType) => void; + +optionalTypeLessProps: (a: OptionalTypeLessProps) => void; + +objectTypeLiteral1: (a: ObjectTypeLiteral1) => void; + +objectTypeLiteral2: (a: ObjectTypeLiteral2) => void; + +objectTypeLiteral1TypeAlias: (a: ObjectTypeLiteral1TypeAlias) => void; + +objectTypeLiteral2TypeAlias: (a: ObjectTypeLiteral2TypeAlias) => void; + +genericObjectType: (a: Object) => void; + + +mapWithKey: (a: MapWithKey) => void; + +mapWithKey2: (a: MapWithKey2) => void; + + +enum: (a: Enum) => void; + +enumWithTypeChange: (a: EnumWithTypeChange) => void; + +enumWithRemoval: (a: EnumWithRemoval) => void; + +enumWithValueChange: (a: EnumWithValueChange) => void; + +enumUnsorted: (a: EnumUnsorted) => void; +} + +(TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-dictionary-changed/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-dictionary-changed/NativeModule.js.flow new file mode 100644 index 00000000000..90274b8f2ce --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-dictionary-changed/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: {[key: string]: string}, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-dictionary/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-dictionary/NativeModule.js.flow new file mode 100644 index 00000000000..fdef3b52353 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-dictionary/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: {[key: string]: number}, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-changes/NativeModule.js.flow new file mode 100644 index 00000000000..06ad4f8b5a7 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-changes/NativeModule.js.flow @@ -0,0 +1,31 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +enum TestEnum { + A = 1, + B = 2, + C = 3, + D = 4, +} + +export interface Spec extends TurboModule { + +exampleFunction: (a: TestEnum, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-changes/NativeModule.js.flow new file mode 100644 index 00000000000..1998ecb7d6c --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-changes/NativeModule.js.flow @@ -0,0 +1,31 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +enum TestEnum { + A = 1, + B = 2, + C = 3, + D = 4, +} + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +exampleConstant: TestEnum, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-type-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-type-changes/NativeModule.js.flow new file mode 100644 index 00000000000..4c980e0fe51 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-type-changes/NativeModule.js.flow @@ -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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +enum TestEnum { + A = '1', + B = '2', + C = '3', +} + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +exampleConstant: TestEnum, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-value-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-value-changes/NativeModule.js.flow new file mode 100644 index 00000000000..7bb2d076f27 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native-value-changes/NativeModule.js.flow @@ -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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +enum TestEnum { + A = 1, + B = 13, + C = 3, +} + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +exampleConstant: TestEnum, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native/NativeModule.js.flow new file mode 100644 index 00000000000..31c57e02ee2 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-from-native/NativeModule.js.flow @@ -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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +enum TestEnum { + A = 1, + B = 2, + C = 3, +} + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +exampleConstant: TestEnum, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-type-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-type-changes/NativeModule.js.flow new file mode 100644 index 00000000000..e0559fee938 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-type-changes/NativeModule.js.flow @@ -0,0 +1,31 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +enum TestEnum { + A = '1', + B = '2', + C = '3', + D = '4', +} + +export interface Spec extends TurboModule { + +exampleFunction: (a: TestEnum, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-value-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-value-changes/NativeModule.js.flow new file mode 100644 index 00000000000..4c488ecc59e --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum-value-changes/NativeModule.js.flow @@ -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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +enum TestEnum { + A = 1, + B = 13, + C = 3, +} + +export interface Spec extends TurboModule { + +exampleFunction: (a: TestEnum, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum/NativeModule.js.flow new file mode 100644 index 00000000000..2514a8aa379 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-enum/NativeModule.js.flow @@ -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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +enum TestEnum { + A = 1, + B = 2, + C = 3, +} + +export interface Spec extends TurboModule { + +exampleFunction: (a: TestEnum, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-eventemitter-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-eventemitter-changes/NativeModule.js.flow new file mode 100644 index 00000000000..a17ac145459 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-eventemitter-changes/NativeModule.js.flow @@ -0,0 +1,31 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type ObjectStruct = { + a: number, + b: string, + c?: ?string, +}; + +export interface Spec extends TurboModule { + onPress: EventEmitter; + onClick: EventEmitter; + onChange: EventEmitter; + onSubmit: EventEmitter; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-eventemitter/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-eventemitter/NativeModule.js.flow new file mode 100644 index 00000000000..d4b83987e11 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-eventemitter/NativeModule.js.flow @@ -0,0 +1,31 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type ObjectStruct = { + a: number, + b: string, + c?: ?string, +}; + +export interface Spec extends TurboModule { + onPress: EventEmitter; + onClick: EventEmitter; + onChange: EventEmitter; + onSubmit: EventEmitter; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-optional-argument/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-optional-argument/NativeModule.js.flow new file mode 100644 index 00000000000..0ffe3ce3a1e --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-optional-argument/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number, c?: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-changes/NativeModule.js.flow new file mode 100644 index 00000000000..b86c7841180 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-changes/NativeModule.js.flow @@ -0,0 +1,26 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type testUnion = 'a' | 'b' | 'c' | 'd'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: testUnion, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-confusing-string-literals/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-confusing-string-literals/NativeModule.js.flow new file mode 100644 index 00000000000..aff7f31947c --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-confusing-string-literals/NativeModule.js.flow @@ -0,0 +1,26 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type testUnion = 'a' | '0' | '1' | 'a long string'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: testUnion, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-from-native-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-from-native-changes/NativeModule.js.flow new file mode 100644 index 00000000000..0a10a45bb16 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-from-native-changes/NativeModule.js.flow @@ -0,0 +1,26 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type testUnion = 'a' | 'b' | 'c' | 'd'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +exampleConstant: testUnion, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-from-native/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-from-native/NativeModule.js.flow new file mode 100644 index 00000000000..53f5816c44e --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-from-native/NativeModule.js.flow @@ -0,0 +1,26 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type testUnion = 'a' | 'b' | 'c'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +exampleConstant: testUnion, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-type-changes/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-type-changes/NativeModule.js.flow new file mode 100644 index 00000000000..6cbbd9bcb4f --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union-type-changes/NativeModule.js.flow @@ -0,0 +1,26 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type testUnion = 1 | 2 | 3; + +export interface Spec extends TurboModule { + +exampleFunction: (a: testUnion, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union/NativeModule.js.flow new file mode 100644 index 00000000000..0015b1e1082 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module-with-union/NativeModule.js.flow @@ -0,0 +1,26 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type testUnion = 'a' | 'b' | 'c'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: testUnion, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module/NativeModule.js.flow b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module/NativeModule.js.flow new file mode 100644 index 00000000000..eba04a1f8b6 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__fixtures__/native-module/NativeModule.js.flow @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +exampleFunction: (a: string, b: number) => void; + +getConstants: () => { + +exampleConstant: number, + }; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeModuleTest', +): Spec); diff --git a/packages/react-native-compatibility-check/src/__tests__/__snapshots__/ErrorFormatting-test.js.snap b/packages/react-native-compatibility-check/src/__tests__/__snapshots__/ErrorFormatting-test.js.snap new file mode 100644 index 00000000000..05f5b3a3bc4 --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/__snapshots__/ErrorFormatting-test.js.snap @@ -0,0 +1,792 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`codegen formattedSummarizeDiffSet allows a diff from native-component-with-command/NativeComponent to native-component-with-command/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object {}, + "status": "ok", +} +`; + +exports[`codegen formattedSummarizeDiffSet allows a diff from native-component-with-props-default-change/NativeComponent to native-component-with-props/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object {}, + "status": "ok", +} +`; + +exports[`codegen formattedSummarizeDiffSet allows a diff from native-module-before-after-types/NativeModuleBeforeAfterTypes to native-module-before-after-types-removed/NativeModuleBeforeAfterTypes 1`] = ` +Object { + "incompatibilityReport": Object {}, + "status": "ok", +} +`; + +exports[`codegen formattedSummarizeDiffSet allows a diff from native-module-get-constants-added-optional-constant/NativeModule to native-module-get-constants/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object {}, + "status": "ok", +} +`; + +exports[`codegen formattedSummarizeDiffSet allows a diff from native-module-with-enum-from-native/NativeModule to native-module-with-enum-from-native-changes/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object {}, + "status": "ok", +} +`; + +exports[`codegen formattedSummarizeDiffSet allows a diff from native-module-with-union-changes/NativeModule to native-module-with-union/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object {}, + "status": "ok", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-component-with-command/NativeComponent to native-component-with-command-extra-arg/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeComponent": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeComponent: Object contained a property with a type mismatch + -- methodInt: has conflicting type changes + --new: (a: int, b: string)=>void + --old: (a: int)=>void + Function types have differing length of arguments + --new: (a: int, b: string)=>void + --old: (a: int)=>void", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-component-with-command-all-basic-types/NativeComponent to native-component-with-command-all-basic-types-arrays/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeComponent": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeComponent: Object contained a property with a type mismatch + -- methodInt: has conflicting type changes + --new: (a: Array)=>void + --old: (a: int)=>void + Parameter at index 0 did not match + --new: (a: Array)=>void + --old: (a: int)=>void + Type annotations are not the same. + --new: Array + --old: int", + }, + Object { + "errorCode": "incompatibleTypes", + "message": "NativeComponent: Object contained a property with a type mismatch + -- methodString: has conflicting type changes + --new: (a: Array)=>void + --old: (a: string)=>void + Parameter at index 0 did not match + --new: (a: Array)=>void + --old: (a: string)=>void + Type annotations are not the same. + --new: Array + --old: string", + }, + Object { + "errorCode": "incompatibleTypes", + "message": "NativeComponent: Object contained a property with a type mismatch + -- methodBool: has conflicting type changes + --new: (a: Array)=>void + --old: (a: boolean)=>void + Parameter at index 0 did not match + --new: (a: Array)=>void + --old: (a: boolean)=>void + Type annotations are not the same. + --new: Array + --old: boolean", + }, + Object { + "errorCode": "incompatibleTypes", + "message": "NativeComponent: Object contained a property with a type mismatch + -- methodFloat: has conflicting type changes + --new: (a: Array)=>void + --old: (a: float)=>void + Parameter at index 0 did not match + --new: (a: Array)=>void + --old: (a: float)=>void + Type annotations are not the same. + --new: Array + --old: float", + }, + Object { + "errorCode": "incompatibleTypes", + "message": "NativeComponent: Object contained a property with a type mismatch + -- methodDouble: has conflicting type changes + --new: (a: Array)=>void + --old: (a: double)=>void + Parameter at index 0 did not match + --new: (a: Array)=>void + --old: (a: double)=>void + Type annotations are not the same. + --new: Array + --old: double", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-component-with-command-changed/NativeComponent to native-component-with-command/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeComponent": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeComponent: Object contained a property with a type mismatch + -- methodInt: has conflicting type changes + --new: (a: int)=>void + --old: (a: string)=>void + Parameter at index 0 did not match + --new: (a: int)=>void + --old: (a: string)=>void + Type annotations are not the same. + --new: int + --old: string", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-component-with-props-added-required-prop/NativeComponent to native-component-with-props/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeComponent": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "removedProps", + "message": "NativeComponent: Object removed required properties expected by native + -- width", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-component-with-props-array-union/NativeComponent to native-component-with-props-array-union-added/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeComponent": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "addedUnionCases", + "message": "NativeComponent.sizes: Union added items, but native will not expect/support them + -- position 3 huge", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-component-with-props-changes/NativeComponent to native-component-with-props/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeComponent": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeComponent: Object contained a property with a type mismatch + -- text: has conflicting type changes + --new: string + --old: int + Type annotations are not the same. + --new: string + --old: int", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-component-with-props-union/NativeComponent to native-component-with-props-union-added/NativeComponent 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeComponent": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "addedUnionCases", + "message": "NativeComponent.size: Union added items, but native will not expect/support them + -- position 3 huge", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-before-after-types-removed/NativeModuleBeforeAfterTypes to native-module-before-after-types/NativeModuleBeforeAfterTypes 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModuleBeforeAfterTypes": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "addedProps", + "message": "NativeModuleTest: Object added required properties, which native will not provide + -- afterMismatchGeneric + -- booleanType + -- exampleFunction + -- simpleArray + -- simpleArrayChange + -- simpleFunction + -- simpleFunction2 + -- simpleFunction3 + -- simpleFunction4 + -- simpleUnion + -- simpleUnion2 + -- simpleUnion3 + -- simpleUnion4 + -- simpleUnionLonger + -- stringType", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-before-after-types-type-changed/NativeModuleBeforeAfterTypes to native-module-before-after-types/NativeModuleBeforeAfterTypes 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModuleBeforeAfterTypes": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- simpleArrayChange: has conflicting type changes + --new: (a: Array)=>void + --old: (a: Array)=>void + Parameter at index 0 did not match + --new: (a: Array)=>void + --old: (a: Array)=>void + Type annotations are not the same. + --new: number + --old: string", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-changed/NativeModule to native-module/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- getConstants: has conflicting type changes + --new: ()=>{exampleConstant: number} + --old: ()=>{exampleConstant: string} + Function return types do not match + --new: ()=>{exampleConstant: number} + --old: ()=>{exampleConstant: string} + Object contained a property with a type mismatch + -- exampleConstant: has conflicting type changes + --new: number + --old: string + Type annotations are not the same. + --new: number + --old: string", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-nested/NativeModule to native-module-nested-optional/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "optionalProps", + "message": "NativeModuleTest.exampleFunction parameter 0: Property made optional, but native requires it + -- a2", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-nested-alias-changed/NativeModule to native-module-nested-alias/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- exampleFunction: has conflicting type changes + --new: (a: ()=>Promise, b: number)=>void + --old: (a: ()=>Promise, b: number)=>void + Parameter at index 0 did not match + --new: (a: ()=>Promise, b: number)=>void + --old: (a: ()=>Promise, b: number)=>void + Function return types do not match + --new: ()=>Promise + --old: ()=>Promise + Type annotations are not the same. + --new: string + --old: number", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-nested-changed/NativeModule to native-module-nested/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "removedProps", + "message": "NativeModuleTest.exampleFunction parameter 0: Object removed required properties expected by native + -- a3", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-nested-changed/NativeModule to native-module-nested-optional/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "optionalProps", + "message": "NativeModuleTest.exampleFunction parameter 0: Property made optional, but native requires it + -- a2", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-nested-optional/NativeModule to native-module-nested-nullable/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "nullableOfNonNull", + "message": "NativeModuleTest.exampleFunction parameter 0: Type made nullable, but native requires it + --new: ?{a1: string, a2?: number, a3: string} + --old: {a1: string, a2?: number, a3: string}", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-dictionary-changed/NativeModule to native-module-with-dictionary/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- exampleFunction: has conflicting type changes + --new: (a: {[string]: number, b: number)=>void + --old: (a: {[string]: string, b: number)=>void + Parameter at index 0 did not match + --new: (a: {[string]: number, b: number)=>void + --old: (a: {[string]: string, b: number)=>void + Type annotations are not the same. + --new: number + --old: string", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-enum/NativeModule to native-module-with-enum-changes/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "addedEnumCases", + "message": "NativeModuleTest.exampleFunction parameter 0: Enum added items, but native will not expect/support them + -- Member D", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-enum-from-native-changes/NativeModule to native-module-with-enum-from-native/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "removedEnumCases", + "message": "NativeModuleTest.getConstants.exampleConstant: Enum removed items, but native may still provide them + -- Member D", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-enum-from-native-type-changes/NativeModule to native-module-with-enum-from-native/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- getConstants: has conflicting type changes + --new: ()=>{exampleConstant: Enum} + --old: ()=>{exampleConstant: Enum} + Function return types do not match + --new: ()=>{exampleConstant: Enum} + --old: ()=>{exampleConstant: Enum} + Object contained a property with a type mismatch + -- exampleConstant: has conflicting type changes + --new: Enum + --old: Enum + EnumDeclaration member types are not the same + --new: Enum + --old: Enum", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-enum-from-native-value-changes/NativeModule to native-module-with-enum-from-native/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- getConstants: has conflicting type changes + --new: ()=>{exampleConstant: Enum} + --old: ()=>{exampleConstant: Enum} + Function return types do not match + --new: ()=>{exampleConstant: Enum} + --old: ()=>{exampleConstant: Enum} + Object contained a property with a type mismatch + -- exampleConstant: has conflicting type changes + --new: Enum + --old: Enum + Enum types do not match + --new: Enum {A = 1, B = 2, C = 3} + --old: Enum {A = 1, B = 13, C = 3} + Enum contained a member with a type mismatch + -- Member B: has conflicting changes + --new: 2 + --old: 13 + Numeric literals are not equal + --new: 2 + --old: 13", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-enum-type-changes/NativeModule to native-module-with-enum/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- exampleFunction: has conflicting type changes + --new: (a: Enum, b: number)=>void + --old: (a: Enum, b: number)=>void + Parameter at index 0 did not match + --new: (a: Enum, b: number)=>void + --old: (a: Enum, b: number)=>void + EnumDeclaration member types are not the same + --new: Enum + --old: Enum", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-enum-value-changes/NativeModule to native-module-with-enum/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- exampleFunction: has conflicting type changes + --new: (a: Enum, b: number)=>void + --old: (a: Enum, b: number)=>void + Parameter at index 0 did not match + --new: (a: Enum, b: number)=>void + --old: (a: Enum, b: number)=>void + Enum types do not match + --new: Enum {A = 1, B = 2, C = 3} + --old: Enum {A = 1, B = 13, C = 3} + Enum contained a member with a type mismatch + -- Member B: has conflicting changes + --new: 2 + --old: 13 + Numeric literals are not equal + --new: 2 + --old: 13", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-eventemitter-changes/NativeModule to native-module-with-eventemitter/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained properties with type mismatches + -- onChange: has conflicting type changes + --new: EventEmitter + --old: EventEmitter> + EventEmitter eventTypes are not equivalent + --new: EventEmitter + --old: EventEmitter> + Type annotations are not the same. + --new: {a: number, b: string, c?: ?string} + --old: Array + -- onClick: has conflicting type changes + --new: EventEmitter + --old: EventEmitter + EventEmitter eventTypes are not equivalent + --new: EventEmitter + --old: EventEmitter + Type annotations are not the same. + --new: string + --old: number + -- onPress: has conflicting type changes + --new: EventEmitter + --old: EventEmitter + EventEmitter eventTypes are not equivalent + --new: EventEmitter + --old: EventEmitter + Type annotations are not the same. + --new: void + --old: string + -- onSubmit: has conflicting type changes + --new: EventEmitter> + --old: EventEmitter + EventEmitter eventTypes are not equivalent + --new: EventEmitter> + --old: EventEmitter + Type annotations are not the same. + --new: Array + --old: {a: number, b: string, c?: ?string}", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-optional-argument/NativeModule to native-module/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- exampleFunction: has conflicting type changes + --new: (a: string, b: number)=>void + --old: (a: string, b: number, c?: number)=>void + Function types have differing length of arguments + --new: (a: string, b: number)=>void + --old: (a: string, b: number, c?: number)=>void", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-union/NativeModule to native-module-with-union-changes/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "addedUnionCases", + "message": "NativeModuleTest.exampleFunction parameter 0: Union added items, but native will not expect/support them + -- position 4 d", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-union-confusing-string-literals/NativeModule to native-module-with-union/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- exampleFunction: has conflicting type changes + --new: (a: (a | b | c), b: number)=>void + --old: (a: (a | '0' | '1' | 'a long string'), b: number)=>void + Parameter at index 0 did not match + --new: (a: (a | b | c), b: number)=>void + --old: (a: (a | '0' | '1' | 'a long string'), b: number)=>void + Subtype of union at position 1 did not match + --new: (a | b | c) + --old: (a | '0' | '1' | 'a long string') + String literals are not equal + --new: b + --old: '0'", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-union-from-native-changes/NativeModule to native-module-with-union-from-native/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "removedUnionCases", + "message": "NativeModuleTest.getConstants.exampleConstant: Union removed items, but native may still provide them + -- position 4 d", + }, + ], + }, + }, + "status": "incompatible", +} +`; + +exports[`codegen formattedSummarizeDiffSet reports a diff from native-module-with-union-type-changes/NativeModule to native-module-with-union/NativeModule 1`] = ` +Object { + "incompatibilityReport": Object { + "NativeModule": Object { + "framework": "ReactNative", + "incompatibleSpecs": Array [ + Object { + "errorCode": "incompatibleTypes", + "message": "NativeModuleTest: Object contained a property with a type mismatch + -- exampleFunction: has conflicting type changes + --new: (a: (a | b | c), b: number)=>void + --old: (a: Union, b: number)=>void + Parameter at index 0 did not match + --new: (a: (a | b | c), b: number)=>void + --old: (a: Union, b: number)=>void + Type annotations are not the same. + --new: (a | b | c) + --old: Union", + }, + ], + }, + }, + "status": "incompatible", +} +`; diff --git a/packages/react-native-compatibility-check/src/__tests__/utilities/getTestSchema.js b/packages/react-native-compatibility-check/src/__tests__/utilities/getTestSchema.js new file mode 100644 index 00000000000..5b008a683da --- /dev/null +++ b/packages/react-native-compatibility-check/src/__tests__/utilities/getTestSchema.js @@ -0,0 +1,27 @@ +/** + * 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. + * + * @format + * @flow strict-local + * @oncall react_native + */ + +import type {SchemaType} from '@react-native/codegen/src/CodegenSchema'; + +import {FlowParser} from '@react-native/codegen/src/parsers/flow/parser'; +import path from 'path'; + +const flowParser = new FlowParser(); + +export function getTestSchema( + ...filenameComponents: Array +): SchemaType { + const filename = path.join(...filenameComponents); + + const schema = flowParser.parseFile(filename); + + return schema; +} diff --git a/packages/react-native-compatibility-check/src/convertPropToBasicTypes.js b/packages/react-native-compatibility-check/src/convertPropToBasicTypes.js new file mode 100644 index 00000000000..1fdd80f73d1 --- /dev/null +++ b/packages/react-native-compatibility-check/src/convertPropToBasicTypes.js @@ -0,0 +1,104 @@ +/** + * 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 { + ArrayTypeAnnotation, + CompleteTypeAnnotation, + ComponentArrayTypeAnnotation, + ObjectTypeAnnotation, + PropTypeAnnotation, +} from '@react-native/codegen/src/CodegenSchema'; + +export default function convertPropToBasicTypes( + inputType: PropTypeAnnotation | ComponentArrayTypeAnnotation['elementType'], +): CompleteTypeAnnotation { + let resultingType: CompleteTypeAnnotation; + + switch (inputType.type) { + case 'BooleanTypeAnnotation': + resultingType = { + type: 'BooleanTypeAnnotation', + }; + break; + case 'StringTypeAnnotation': + resultingType = { + type: 'StringTypeAnnotation', + }; + break; + case 'DoubleTypeAnnotation': + resultingType = { + type: 'DoubleTypeAnnotation', + }; + break; + + case 'FloatTypeAnnotation': + resultingType = { + type: 'FloatTypeAnnotation', + }; + break; + case 'Int32TypeAnnotation': + resultingType = { + type: 'Int32TypeAnnotation', + }; + break; + case 'StringEnumTypeAnnotation': + resultingType = { + type: 'StringLiteralUnionTypeAnnotation', + types: inputType.options.map(option => { + return { + type: 'StringLiteralTypeAnnotation', + value: option, + }; + }), + }; + break; + case 'Int32EnumTypeAnnotation': + // Compat check doesn't yet have support for + // NumberLiteralUnionTypeAnnotation + resultingType = { + type: 'AnyTypeAnnotation', + }; + break; + case 'ReservedPropTypeAnnotation': + resultingType = { + type: 'ReservedTypeAnnotation', + name: inputType.name, + }; + break; + case 'MixedTypeAnnotation': + resultingType = inputType; + break; + case 'ObjectTypeAnnotation': + resultingType = ({ + type: 'ObjectTypeAnnotation', + ...(inputType.baseTypes != null + ? {baseTypes: inputType.baseTypes} + : {}), + properties: inputType.properties.map(property => ({ + name: property.name, + optional: property.optional, + typeAnnotation: convertPropToBasicTypes(property.typeAnnotation), + })), + }: ObjectTypeAnnotation); + break; + case 'ArrayTypeAnnotation': + resultingType = ({ + type: 'ArrayTypeAnnotation', + elementType: convertPropToBasicTypes(inputType.elementType), + }: ArrayTypeAnnotation); + break; + default: + (inputType.type: empty); + throw new Error('Unexpected type ' + inputType.type); + } + + return resultingType; +} diff --git a/packages/react-native-compatibility-check/src/index.flow.js b/packages/react-native-compatibility-check/src/index.flow.js new file mode 100644 index 00000000000..ad79b4145ac --- /dev/null +++ b/packages/react-native-compatibility-check/src/index.flow.js @@ -0,0 +1,12 @@ +/** + * 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 + */ + +export {compareSchemas} from './index.js'; diff --git a/packages/react-native-compatibility-check/src/index.js b/packages/react-native-compatibility-check/src/index.js new file mode 100644 index 00000000000..264b9eed430 --- /dev/null +++ b/packages/react-native-compatibility-check/src/index.js @@ -0,0 +1,54 @@ +/** + * 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 { + DiffSummary, + ExportableSchemaDiff, + FormattedDiffSummary, + SchemaDiff, +} from './DiffResults'; +import type {SchemaType} from '@react-native/codegen/src/CodegenSchema'; + +import {schemaDiffExporter} from './DiffResults.js'; +import {formatDiffSet} from './ErrorFormatting.js'; +import {buildSchemaDiff, summarizeDiffSet} from './VersionDiffing.js'; + +class CompatCheckResult { + #schemaDiff: Set; + #summary: ?DiffSummary; + + constructor(schemaDiff: Set) { + this.#schemaDiff = schemaDiff; + } + + getSummary(): DiffSummary { + if (this.#summary == null) { + this.#summary = summarizeDiffSet(this.#schemaDiff); + } + + return this.#summary; + } + + getErrors(): FormattedDiffSummary { + return formatDiffSet(this.getSummary()); + } + + getDebugInfo(): Array { + return Array.from(this.#schemaDiff, schemaDiffExporter); + } +} + +export function compareSchemas( + newSchema: SchemaType, + oldSchema: SchemaType, +): CompatCheckResult { + return new CompatCheckResult(buildSchemaDiff(newSchema, oldSchema)); +} diff --git a/scripts/build/config.js b/scripts/build/config.js index 10160feb829..e24cb22c226 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -54,6 +54,10 @@ const buildConfig: BuildConfig = { emitTypeScriptDefs: true, target: 'node', }, + 'react-native-compatibility-check': { + emitTypeScriptDefs: true, + target: 'node', + }, }, }; diff --git a/yarn.lock b/yarn.lock index e719bfc876f..b521ea79501 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4438,6 +4438,15 @@ flow-parser@0.*: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.245.0.tgz#d8ad7e706d280ce6d4189a206768c32f552b5099" integrity sha512-xUBkkpIDfDZHAebnDEX65FCVitJUctab82KFmtP5SY4cGly1vbuYNe6Muyp0NLXrgmBChVdoC2T+3/RUHi4Mww== +flow-remove-types@^2.237.2: + version "2.259.1" + resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.259.1.tgz#ee78962cbd73d85f0e94183b354c43399903c734" + integrity sha512-H+ww7Q6c4xXFAoZGZnC37FJmA9Zzu3UpQ96q3/geCQiaZiTXmM7ZeN53DYpu4be8cvPEp15K0SZTOtaiGoWU5A== + dependencies: + hermes-parser "0.25.1" + pirates "^3.0.2" + vlq "^0.2.1" + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -6773,6 +6782,11 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha512-JMaRS9L4wSRIR+6PTVEikTrq/lMGEZR43a48ETeilY0Q0iMwVnccMFrUM1k+tNzmYuIU0Vh710bCUqHX+/+ctQ== + node-releases@^2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" @@ -7170,6 +7184,13 @@ pinpoint@^1.1.0: resolved "https://registry.yarnpkg.com/pinpoint/-/pinpoint-1.1.0.tgz#0cf7757a6977f1bf7f6a32207b709e377388e874" integrity sha512-+04FTD9x7Cls2rihLlo57QDCcHoLBGn5Dk51SwtFBWkUWLxZaBXyNVpCw1S+atvE7GmnFjeaRZ0WLq3UYuqAdg== +pirates@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-3.0.2.tgz#7e6f85413fd9161ab4e12b539b06010d85954bb9" + integrity sha512-c5CgUJq6H2k6MJz72Ak1F5sN9n9wlSlJyEnwvpm9/y3WB4E3pHBDT2c6PEiS1vyJvq2bUxUAIu0EGf8Cx4Ic7Q== + dependencies: + node-modules-regexp "^1.0.0" + pirates@^4.0.1, pirates@^4.0.4, pirates@^4.0.5, pirates@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -8744,6 +8765,11 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vlq@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" + integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== + vlq@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468"