mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: The Flow team is improving the way Flow infers type for primitive literals. This diff prepares the codebase for the new behavior by adding type annotations, or annotations of the form `'abc' as const`. Changelog: [internal] Reviewed By: marcoww6 Differential Revision: D75188179 fbshipit-source-id: be50990f23f79cf2d8dae7576af5190218adcafe
1371 lines
39 KiB
JavaScript
1371 lines
39 KiB
JavaScript
/**
|
|
* 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<ErrorStore>;
|
|
|
|
function nestedPropertiesCheck(
|
|
typeName: string,
|
|
result: ComparisonResult,
|
|
check: checkerType,
|
|
inverseCheck: checkerType,
|
|
): Array<ErrorStore> {
|
|
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<ErrorStore> = [];
|
|
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<NamedShape<CompleteTypeAnnotation>>,
|
|
msg: string,
|
|
errorCode: ErrorCode,
|
|
): Array<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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<ErrorStore> {
|
|
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>): 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<ErrorStore>,
|
|
arrayOf: Array<Array<ErrorStore>>,
|
|
) {
|
|
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<ErrorStore>,
|
|
) {
|
|
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<ErrorStore>,
|
|
objectTypeChanges: Set<ObjectTypeChangeStore>,
|
|
): (
|
|
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<ErrorStore> = [];
|
|
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<NativeSpecErrorStore> {
|
|
const moduleErrors = new Set<NativeSpecErrorStore>();
|
|
|
|
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<ErrorStore>();
|
|
const objectTypeChanges = new Set<ObjectTypeChangeStore>();
|
|
const localAssessComparison = assessComparisonResult(
|
|
newTypes,
|
|
deprecatedTypes,
|
|
incompatibleChanges,
|
|
objectTypeChanges,
|
|
);
|
|
|
|
const newType: CompleteTypeAnnotation = {
|
|
type: 'ObjectTypeAnnotation',
|
|
properties: [
|
|
...newerNativeModule.spec.methods,
|
|
...newerNativeModule.spec.eventEmitters,
|
|
],
|
|
};
|
|
const oldType: CompleteTypeAnnotation = {
|
|
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<NativeSpecErrorStore> {
|
|
const componentErrors = new Set<NativeSpecErrorStore>();
|
|
|
|
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<ErrorStore>();
|
|
const objectTypeChanges = new Set<ObjectTypeChangeStore>();
|
|
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: CompleteTypeAnnotation = {
|
|
type: 'ObjectTypeAnnotation',
|
|
properties: [command],
|
|
};
|
|
const oldCommands: ?CompleteTypeAnnotation =
|
|
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: CompleteTypeAnnotation = {
|
|
type: 'ObjectTypeAnnotation',
|
|
properties: newerComponent.props.map(prop => ({
|
|
name: prop.name,
|
|
optional: prop.optional,
|
|
typeAnnotation: convertPropToBasicTypes(prop.typeAnnotation),
|
|
})),
|
|
};
|
|
const oldConvertedProps: CompleteTypeAnnotation = {
|
|
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<SchemaDiff> {
|
|
const diff: Set<SchemaDiff> = 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<SchemaDiff>): DiffSummary {
|
|
if (diffs.size === 0) {
|
|
return {status: 'ok', incompatibilityReport: {}};
|
|
}
|
|
const summary: Array<DiffSummary> = [];
|
|
diffs.forEach(schemaDiff => summary.push(summarizeSchemaDiff(schemaDiff)));
|
|
return summary.reduce(combineSummaries, summary[0]);
|
|
}
|