Skip to content

Commit

Permalink
RN: Strict Static View Config Validator
Browse files Browse the repository at this point in the history
Summary:
Creates a new `StaticViewConfigValidator` module that does strict, bidirectional validation. This is notably different from `verifyComponentAttributeEquivalence`, which is undirectional validation.

This will enforce that two configs are equivalent so we can start addressing the inconsistencies (especially per platform). It also improves upon the reporting format by providing more details about the invalidations.

It is hidden behind a `strict` runtime configuration parameter.

Changelog:
[Internal]

Reviewed By: RSNara

Differential Revision: D29024229

fbshipit-source-id: 10271945e089183f505205bd41de5e01faea7568
  • Loading branch information
yungsters authored and facebook-github-bot committed Oct 19, 2021
1 parent eebc829 commit addf4da
Show file tree
Hide file tree
Showing 3 changed files with 362 additions and 10 deletions.
40 changes: 30 additions & 10 deletions Libraries/NativeComponent/NativeComponentRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @format
*/

import * as StaticViewConfigValidator from './StaticViewConfigValidator';
import {createViewConfig} from './ViewConfig';
import UIManager from '../ReactNative/UIManager';
import type {
Expand Down Expand Up @@ -36,6 +37,7 @@ export function setRuntimeConfigProvider(
name: string,
) => ?{
native: boolean,
strict: boolean,
verify: boolean,
},
): void {
Expand All @@ -57,8 +59,9 @@ export function get<Config>(
viewConfigProvider: () => PartialViewConfig,
): HostComponent<Config> {
ReactNativeViewConfigRegistry.register(name, () => {
const {native, verify} = getRuntimeConfig?.(name) ?? {
const {native, strict, verify} = getRuntimeConfig?.(name) ?? {
native: true,
strict: false,
verify: false,
};

Expand All @@ -67,16 +70,33 @@ export function get<Config>(
: createViewConfig(viewConfigProvider());

if (verify) {
if (native) {
verifyComponentAttributeEquivalence(
viewConfig,
createViewConfig(viewConfigProvider()),
);
if (strict) {
const results = native
? StaticViewConfigValidator.validate(
name,
viewConfig,
createViewConfig(viewConfigProvider()),
)
: StaticViewConfigValidator.validate(
name,
getNativeComponentAttributes(name),
viewConfig,
);
if (results != null) {
console.error(results);
}
} else {
verifyComponentAttributeEquivalence(
getNativeComponentAttributes(name),
viewConfig,
);
if (native) {
verifyComponentAttributeEquivalence(
viewConfig,
createViewConfig(viewConfigProvider()),
);
} else {
verifyComponentAttributeEquivalence(
getNativeComponentAttributes(name),
viewConfig,
);
}
}
}

Expand Down
110 changes: 110 additions & 0 deletions Libraries/NativeComponent/StaticViewConfigValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Copyright (c) Facebook, Inc. and its 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 ViewConfig} from '../Renderer/shims/ReactNativeTypes';

type Difference = {
path: $ReadOnlyArray<string>,
type: 'missing' | 'unequal' | 'unexpected',
};

/**
* During the migration from native view configs to static view configs, this is
* used to validate that the two are equivalent.
*/
export function validate(
name: string,
nativeViewConfig: ViewConfig,
staticViewConfig: ViewConfig,
): ?string {
const differences = [];
accumulateDifferences(
differences,
[],
{
bubblingEventTypes: nativeViewConfig.bubblingEventTypes,
directEventTypes: nativeViewConfig.directEventTypes,
uiViewClassName: nativeViewConfig.uiViewClassName,
validAttributes: nativeViewConfig.validAttributes,
},
{
bubblingEventTypes: staticViewConfig.bubblingEventTypes,
directEventTypes: staticViewConfig.directEventTypes,
uiViewClassName: staticViewConfig.uiViewClassName,
validAttributes: staticViewConfig.validAttributes,
},
);
if (differences.length === 0) {
return null;
}
return [
`StaticViewConfigValidator: Invalid static view config for '${name}'.`,
'',
...differences.map(({path, type}) => {
switch (type) {
case 'missing':
return `- '${path.join('.')}' is missing.`;
case 'unequal':
return `- '${path.join('.')}' is the wrong value.`;
case 'unexpected':
return `- '${path.join('.')}' is present but not expected to be.`;
}
}),
'',
].join('\n');
}

function accumulateDifferences(
differences: Array<Difference>,
path: Array<string>,
nativeObject: {...},
staticObject: {...},
): void {
for (const nativeKey in nativeObject) {
const nativeValue = nativeObject[nativeKey];

if (!staticObject.hasOwnProperty(nativeKey)) {
differences.push({path: [...path, nativeKey], type: 'missing'});
continue;
}

const staticValue = staticObject[nativeKey];

const nativeValueIfObject = ifObject(nativeValue);
if (nativeValueIfObject != null) {
const staticValueIfObject = ifObject(staticValue);
if (staticValueIfObject != null) {
path.push(nativeKey);
accumulateDifferences(
differences,
path,
nativeValueIfObject,
staticValueIfObject,
);
path.pop();
continue;
}
}

if (nativeValue !== staticValue) {
differences.push({path: [...path, nativeKey], type: 'unequal'});
}
}

for (const staticKey in staticObject) {
if (!nativeObject.hasOwnProperty(staticKey)) {
differences.push({path: [...path, staticKey], type: 'unexpected'});
}
}
}

function ifObject(value: mixed): ?{...} {
return typeof value === 'object' && !Array.isArray(value) ? value : null;
}
Loading

0 comments on commit addf4da

Please sign in to comment.