RN: Strict Static View Config Validator

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
This commit is contained in:
Tim Yung 2021-10-19 00:23:22 -07:00 коммит произвёл Facebook GitHub Bot
Родитель eebc829b23
Коммит addf4dab51
3 изменённых файлов: 362 добавлений и 10 удалений

Просмотреть файл

@ -8,6 +8,7 @@
* @format
*/
import * as StaticViewConfigValidator from './StaticViewConfigValidator';
import {createViewConfig} from './ViewConfig';
import UIManager from '../ReactNative/UIManager';
import type {
@ -36,6 +37,7 @@ export function setRuntimeConfigProvider(
name: string,
) => ?{
native: boolean,
strict: boolean,
verify: boolean,
},
): void {
@ -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,
};
@ -67,6 +70,22 @@ export function get<Config>(
: createViewConfig(viewConfigProvider());
if (verify) {
if (strict) {
const results = native
? StaticViewConfigValidator.validate(
name,
viewConfig,
createViewConfig(viewConfigProvider()),
)
: StaticViewConfigValidator.validate(
name,
getNativeComponentAttributes(name),
viewConfig,
);
if (results != null) {
console.error(results);
}
} else {
if (native) {
verifyComponentAttributeEquivalence(
viewConfig,
@ -79,6 +98,7 @@ export function get<Config>(
);
}
}
}
return viewConfig;
});

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -0,0 +1,222 @@
/**
* 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.
*
* @emails oncall+react_native
* @flow strict
* @format
*/
import * as StaticViewConfigValidator from '../StaticViewConfigValidator';
test('passes for identical configs', () => {
const name = 'RCTView';
const nativeViewConfig = {
bubblingEventTypes: {
topBlur: {
phasedRegistrationNames: {
bubbled: 'onBlur',
captured: 'onBlurCapture',
},
},
topFocus: {
phasedRegistrationNames: {
bubbled: 'onFocus',
captured: 'onFocusCapture',
},
},
},
directEventTypes: {
topLayout: {
registrationName: 'onLayout',
},
},
uiViewClassName: 'RCTView',
validAttributes: {
collapsable: true,
nativeID: true,
style: {
height: true,
width: true,
},
},
};
const staticViewConfig = {
bubblingEventTypes: {
topBlur: {
phasedRegistrationNames: {
bubbled: 'onBlur',
captured: 'onBlurCapture',
},
},
topFocus: {
phasedRegistrationNames: {
bubbled: 'onFocus',
captured: 'onFocusCapture',
},
},
},
directEventTypes: {
topLayout: {
registrationName: 'onLayout',
},
},
uiViewClassName: 'RCTView',
validAttributes: {
collapsable: true,
nativeID: true,
style: {
height: true,
width: true,
},
},
};
expect(
StaticViewConfigValidator.validate(
name,
nativeViewConfig,
staticViewConfig,
),
).toBe(null);
});
test('fails for mismatched names', () => {
const name = 'RCTView';
const nativeViewConfig = {
uiViewClassName: 'RCTView',
validAttributes: {
style: {},
},
};
const staticViewConfig = {
uiViewClassName: 'RCTImage',
validAttributes: {
style: {},
},
};
expect(
StaticViewConfigValidator.validate(
name,
nativeViewConfig,
staticViewConfig,
),
).toBe(
`
StaticViewConfigValidator: Invalid static view config for 'RCTView'.
- 'uiViewClassName' is the wrong value.
`.trimStart(),
);
});
test('fails for unequal attributes', () => {
const name = 'RCTView';
const nativeViewConfig = {
uiViewClassName: 'RCTView',
validAttributes: {
nativeID: true,
style: {},
},
};
const staticViewConfig = {
uiViewClassName: 'RCTView',
validAttributes: {
nativeID: {},
style: {},
},
};
expect(
StaticViewConfigValidator.validate(
name,
nativeViewConfig,
staticViewConfig,
),
).toBe(
`
StaticViewConfigValidator: Invalid static view config for 'RCTView'.
- 'validAttributes.nativeID' is the wrong value.
`.trimStart(),
);
});
test('fails for missing attributes', () => {
const name = 'RCTView';
const nativeViewConfig = {
uiViewClassName: 'RCTView',
validAttributes: {
collapsable: true,
nativeID: true,
style: {
height: true,
width: true,
},
},
};
const staticViewConfig = {
uiViewClassName: 'RCTView',
validAttributes: {
style: {},
},
};
expect(
StaticViewConfigValidator.validate(
name,
nativeViewConfig,
staticViewConfig,
),
).toBe(
`
StaticViewConfigValidator: Invalid static view config for 'RCTView'.
- 'validAttributes.collapsable' is missing.
- 'validAttributes.nativeID' is missing.
- 'validAttributes.style.height' is missing.
- 'validAttributes.style.width' is missing.
`.trimStart(),
);
});
test('fails for unexpected attributes', () => {
const name = 'RCTView';
const nativeViewConfig = {
uiViewClassName: 'RCTView',
validAttributes: {
style: {},
},
};
const staticViewConfig = {
uiViewClassName: 'RCTView',
validAttributes: {
collapsable: true,
nativeID: true,
style: {
height: true,
width: true,
},
},
};
expect(
StaticViewConfigValidator.validate(
name,
nativeViewConfig,
staticViewConfig,
),
).toBe(
`
StaticViewConfigValidator: Invalid static view config for 'RCTView'.
- 'validAttributes.style.height' is present but not expected to be.
- 'validAttributes.style.width' is present but not expected to be.
- 'validAttributes.collapsable' is present but not expected to be.
- 'validAttributes.nativeID' is present but not expected to be.
`.trimStart(),
);
});