Move TurboModuleRegistry validation into NativeModule Spec Parser

Summary:
## Changes
1. In the NativeModule spec parser, the moduleName is now being extracted from the TurboModuleRegistry.get<Spec>(...) call by examining the Flow ast node. Previously, we used regex parsing, which was unsafe because it could be fooled by TurboModuleRegistry.get<Spec>(...) calls in comments.
2. The logic to parse and validate the TurboModuleRegistry.get<Spec>(...) call is now centralized in the NativeModule Spec Parser (it was removed from the react-native-modules ESLint rule). The linter is now only responsible for three things:
   1. Detecting if a JavaScript file contains a TurboModuleRegistry.get<Spec> call or a TurboModule interface, and if so
   2. Running the NativeModule spec parser on it.
   3. It also validates that the Module spec's filename starts with the prefix "Native".

The React Native Modules linter now completely delegates to the NativeModules Spec parser, without doing any error checking of its own. If an error is reported by the React Native Modules linter, and that error doesn't have anything to do with the "Native" prefix, then it *must* be addressed. Otherwise, it will cause the NativeModule Spec Parser to fail on that particular spec.

Changelog: [Internal]

Reviewed By: hramos

Differential Revision: D25153243

fbshipit-source-id: da74dbb66b1d8dca3a2b1952402222c6696b73d6
This commit is contained in:
Ramanpreet Nara 2020-12-04 14:19:27 -08:00 коммит произвёл Facebook GitHub Bot
Родитель 374808f307
Коммит e289366b38
9 изменённых файлов: 259 добавлений и 828 удалений

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

@ -165,80 +165,10 @@ const module = TurboModuleRegistry.getEnforcing<Spec>(
export default (module: Spec);
`;
const SAMPLE_TURBO_MODULE_MANY_MODULES_DEFAULT_EXPORT = `
// @flow
import type {RootTag, TurboModule} from '../RCTExport';
import * as TurboModuleRegistry from '../TurboModuleRegistry';
export interface Spec extends TurboModule {
// Exported methods.
+getConstants: () => {|
const1: boolean,
const2: number,
const3: string,
|};
+voidFunc: () => void;
+getBool: (arg: boolean) => boolean;
+getNumber: (arg: number) => number;
+getString: (arg: string) => string;
+getArray: (arg: Array<any>) => Array<any>;
+getObject: (arg: Object) => Object;
+getRootTag: (arg: RootTag) => RootTag;
+getValue: (x: number, y: string, z: Object) => Object;
+getValueWithCallback: (callback: (value: string) => void) => void;
+getValueWithPromise: (error: boolean) => Promise<string>;
}
export default (
(
TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule') ||
TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule2')
): Spec);
`;
const SAMPLE_TURBO_MODULE_MANY_MODULES_VARIABLE_ASSIGNMENT = `
// @flow
import type {RootTag, TurboModule} from '../RCTExport';
import * as TurboModuleRegistry from '../TurboModuleRegistry';
export interface Spec extends TurboModule {
// Exported methods.
+getConstants: () => {|
const1: boolean,
const2: number,
const3: string,
|};
+voidFunc: () => void;
+getBool: (arg: boolean) => boolean;
+getNumber: (arg: number) => number;
+getString: (arg: string) => string;
+getArray: (arg: Array<any>) => Array<any>;
+getObject: (arg: Object) => Object;
+getRootTag: (arg: RootTag) => RootTag;
+getValue: (x: number, y: string, z: Object) => Object;
+getValueWithCallback: (callback: (value: string) => void) => void;
+getValueWithPromise: (error: boolean) => Promise<string>;
}
const module1 = TurboModuleRegistry.getEnforcing<Spec>(
'SampleTurboModule',
);
const module2 = TurboModuleRegistry.getEnforcing<Spec>(
'SampleTurboModule2',
);
export default ((module1 || module2): Spec);
`;
module.exports = {
'NotANativeComponent.js': NOT_A_NATIVE_COMPONENT,
'FullNativeComponent.js': FULL_NATIVE_COMPONENT,
'FullTypedNativeComponent.js': FULL_NATIVE_COMPONENT_WITH_TYPE_EXPORT,
'NativeSampleTurboModule0.js': SAMPLE_TURBO_MODULE_SINGLE_DEFAULT_EXPORT,
'NativeSampleTurboModule1.js': SAMPLE_TURBO_MODULE_VARIABLE_ASSIGNMENT,
'NativeSampleTurboModule2.js': SAMPLE_TURBO_MODULE_MANY_MODULES_DEFAULT_EXPORT,
'NativeSampleTurboModule3.js': SAMPLE_TURBO_MODULE_MANY_MODULES_VARIABLE_ASSIGNMENT,
};

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

@ -619,502 +619,6 @@ function __getModuleSchema() {
}"
`;
exports[`Babel plugin inline view configs can inline config for NativeSampleTurboModule2.js 1`] = `
"// @flow
import type { RootTag, TurboModule } from '../RCTExport';
import * as TurboModuleRegistry from '../TurboModuleRegistry';
export interface Spec extends TurboModule {
// Exported methods.
+getConstants: () => {|
const1: boolean,
const2: number,
const3: string,
|},
+voidFunc: () => void,
+getBool: (arg: boolean) => boolean,
+getNumber: (arg: number) => number,
+getString: (arg: string) => string,
+getArray: (arg: Array<any>) => Array<any>,
+getObject: (arg: Object) => Object,
+getRootTag: (arg: RootTag) => RootTag,
+getValue: (x: number, y: string, z: Object) => Object,
+getValueWithCallback: (callback: (value: string) => void) => void,
+getValueWithPromise: (error: boolean) => Promise<string>,
}
export default (TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule', __getModuleSchema()) || TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule2', __getModuleSchema()): Spec);
function __getModuleSchema() {
if (!(global.RN$JSTurboModuleCodegenEnabled === true)) {
return undefined;
}
return {
\\"type\\": \\"NativeModule\\",
\\"aliases\\": {},
\\"spec\\": {
\\"properties\\": [{
\\"name\\": \\"getConstants\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"ObjectTypeAnnotation\\",
\\"properties\\": [{
\\"name\\": \\"const1\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"BooleanTypeAnnotation\\"
}
}, {
\\"name\\": \\"const2\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"NumberTypeAnnotation\\"
}
}, {
\\"name\\": \\"const3\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
}
}]
},
\\"params\\": []
}
}, {
\\"name\\": \\"voidFunc\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"VoidTypeAnnotation\\"
},
\\"params\\": []
}
}, {
\\"name\\": \\"getBool\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"BooleanTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"BooleanTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getNumber\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"NumberTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"NumberTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getString\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getArray\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"ArrayTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"ArrayTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getObject\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"GenericObjectTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"GenericObjectTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getRootTag\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"ReservedTypeAnnotation\\",
\\"name\\": \\"RootTag\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"ReservedTypeAnnotation\\",
\\"name\\": \\"RootTag\\"
}
}]
}
}, {
\\"name\\": \\"getValue\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"GenericObjectTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"x\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"NumberTypeAnnotation\\"
}
}, {
\\"name\\": \\"y\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
}
}, {
\\"name\\": \\"z\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"GenericObjectTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getValueWithCallback\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"VoidTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"callback\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"VoidTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"value\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
}
}]
}
}]
}
}, {
\\"name\\": \\"getValueWithPromise\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"PromiseTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"error\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"BooleanTypeAnnotation\\"
}
}]
}
}]
},
\\"moduleNames\\": [\\"SampleTurboModule\\", \\"SampleTurboModule2\\"]
};
}"
`;
exports[`Babel plugin inline view configs can inline config for NativeSampleTurboModule3.js 1`] = `
"// @flow
import type { RootTag, TurboModule } from '../RCTExport';
import * as TurboModuleRegistry from '../TurboModuleRegistry';
export interface Spec extends TurboModule {
// Exported methods.
+getConstants: () => {|
const1: boolean,
const2: number,
const3: string,
|},
+voidFunc: () => void,
+getBool: (arg: boolean) => boolean,
+getNumber: (arg: number) => number,
+getString: (arg: string) => string,
+getArray: (arg: Array<any>) => Array<any>,
+getObject: (arg: Object) => Object,
+getRootTag: (arg: RootTag) => RootTag,
+getValue: (x: number, y: string, z: Object) => Object,
+getValueWithCallback: (callback: (value: string) => void) => void,
+getValueWithPromise: (error: boolean) => Promise<string>,
}
const module1 = TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule', __getModuleSchema());
const module2 = TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule2', __getModuleSchema());
export default (module1 || module2: Spec);
function __getModuleSchema() {
if (!(global.RN$JSTurboModuleCodegenEnabled === true)) {
return undefined;
}
return {
\\"type\\": \\"NativeModule\\",
\\"aliases\\": {},
\\"spec\\": {
\\"properties\\": [{
\\"name\\": \\"getConstants\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"ObjectTypeAnnotation\\",
\\"properties\\": [{
\\"name\\": \\"const1\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"BooleanTypeAnnotation\\"
}
}, {
\\"name\\": \\"const2\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"NumberTypeAnnotation\\"
}
}, {
\\"name\\": \\"const3\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
}
}]
},
\\"params\\": []
}
}, {
\\"name\\": \\"voidFunc\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"VoidTypeAnnotation\\"
},
\\"params\\": []
}
}, {
\\"name\\": \\"getBool\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"BooleanTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"BooleanTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getNumber\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"NumberTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"NumberTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getString\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getArray\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"ArrayTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"ArrayTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getObject\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"GenericObjectTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"GenericObjectTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getRootTag\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"ReservedTypeAnnotation\\",
\\"name\\": \\"RootTag\\"
},
\\"params\\": [{
\\"name\\": \\"arg\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"ReservedTypeAnnotation\\",
\\"name\\": \\"RootTag\\"
}
}]
}
}, {
\\"name\\": \\"getValue\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"GenericObjectTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"x\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"NumberTypeAnnotation\\"
}
}, {
\\"name\\": \\"y\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
}
}, {
\\"name\\": \\"z\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"GenericObjectTypeAnnotation\\"
}
}]
}
}, {
\\"name\\": \\"getValueWithCallback\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"VoidTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"callback\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"VoidTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"value\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"StringTypeAnnotation\\"
}
}]
}
}]
}
}, {
\\"name\\": \\"getValueWithPromise\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"FunctionTypeAnnotation\\",
\\"returnTypeAnnotation\\": {
\\"type\\": \\"PromiseTypeAnnotation\\"
},
\\"params\\": [{
\\"name\\": \\"error\\",
\\"optional\\": false,
\\"typeAnnotation\\": {
\\"type\\": \\"BooleanTypeAnnotation\\"
}
}]
}
}]
},
\\"moduleNames\\": [\\"SampleTurboModule\\", \\"SampleTurboModule2\\"]
};
}"
`;
exports[`Babel plugin inline view configs can inline config for NotANativeComponent.js 1`] = `
"const requireNativeComponent = require('requireNativeComponent');

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

@ -37,101 +37,6 @@ export default TurboModuleRegistry.get<Spec>('XYZ');
},
],
},
// Untyped NativeModule require
{
code: `
import {TurboModuleRegistry, type TurboModule} from 'react-native';
export interface Spec extends TurboModule {
func1(a: string): {||},
}
export default TurboModuleRegistry.get('XYZ');
`,
filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`,
errors: [
{
message: rule.errors.untypedModuleRequire('NativeXYZ', 'get'),
},
],
},
// Incorrectly typed NativeModule require: 0 types
{
code: `
import {TurboModuleRegistry, type TurboModule} from 'react-native';
export interface Spec extends TurboModule {
func1(a: string): {||},
}
export default TurboModuleRegistry.get<>('XYZ');
`,
filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`,
errors: [
{
message: rule.errors.incorrectlyTypedModuleRequire('NativeXYZ', 'get'),
},
],
},
// Incorrectly typed NativeModule require: > 1 type
{
code: `
import {TurboModuleRegistry, type TurboModule} from 'react-native';
export interface Spec extends TurboModule {
func1(a: string): {||},
}
export default TurboModuleRegistry.get<Spec, 'test'>('XYZ');
`,
filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`,
errors: [
{
message: rule.errors.incorrectlyTypedModuleRequire('NativeXYZ', 'get'),
},
],
},
// More than one TurboModuleRegistry call
{
code: `
import {TurboModuleRegistry, type TurboModule} from 'react-native';
export interface Spec extends TurboModule {
func1(a: string): {||},
}
const NativeModule1 = TurboModuleRegistry.get<Spec>('XYZ1');
const NativeModule2 = TurboModuleRegistry.get<Spec>('XYZ2');
export default NativeModule1 || NativeModule2;
`,
filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`,
errors: [
{
message: rule.errors.multipleModuleRequires('NativeXYZ', 2),
},
{
message: rule.errors.multipleModuleRequires('NativeXYZ', 2),
},
],
},
// Called TurboModuleRegistry.getEnforcing with a variable
{
code: `
import {TurboModuleRegistry, type TurboModule} from 'react-native';
export interface Spec extends TurboModule {
func1(a: string): {||},
}
const moduleName = 'foo';
export default TurboModuleRegistry.get<Spec>(moduleName);
`,
filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`,
errors: [
{
message: rule.errors.calledModuleRequireWithWrongType(
'NativeXYZ',
'get',
'Identifier',
),
},
],
},
];
eslintTester.run('../react-native-modules', rule, {

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

@ -17,26 +17,6 @@ const ERRORS = {
misnamedHasteModule(hasteModuleName) {
return `Module ${hasteModuleName}: All files using TurboModuleRegistry must start with Native.`;
},
untypedModuleRequire(hasteModuleName, requireMethodName) {
return `Module ${hasteModuleName}: Please type parameterize the Module require: TurboModuleRegistry.${requireMethodName}<Spec>().`;
},
incorrectlyTypedModuleRequire(hasteModuleName, requireMethodName) {
return `Module ${hasteModuleName}: Type parameter of Module require must be 'Spec': TurboModuleRegistry.${requireMethodName}<Spec>().`;
},
multipleModuleRequires(hasteModuleName, numCalls) {
return `Module ${hasteModuleName}: Module spec must contain exactly one call into TurboModuleRegistry, detected ${numCalls}.`;
},
calledModuleRequireWithWrongType(hasteModuleName, requireMethodName, type) {
const a = /[aeiouy]/.test(type.toLowerCase()) ? 'an' : 'a';
return `Module ${hasteModuleName}: TurboModuleRegistry.${requireMethodName}<Spec>() must be called with a string literal, detected ${a} '${type}'.`;
},
calledModuleRequireWithWrongLiteral(
hasteModuleName,
requireMethodName,
literal,
) {
return `Module ${hasteModuleName}: TurboModuleRegistry.${requireMethodName}<Spec>() must be called with a string literal, detected ${literal}`;
},
};
let RNModuleParser;
@ -116,23 +96,11 @@ function rule(context) {
return {};
}
let moduleRequires = [];
let isModule = false;
return {
'Program:exit': function(node) {
if (moduleRequires.length === 0) {
return;
}
if (moduleRequires.length > 1) {
moduleRequires.forEach(callExpressionNode => {
context.report({
node: callExpressionNode,
message: ERRORS.multipleModuleRequires(
hasteModuleName,
moduleRequires.length,
),
});
});
if (!isModule) {
return;
}
@ -154,11 +122,13 @@ function rule(context) {
const sourceCode = context.getSourceCode().getText();
const ast = flowParser.parse(sourceCode);
guard(() => buildModuleSchema(hasteModuleName, [], ast, guard));
guard(() => buildModuleSchema(hasteModuleName, ast, guard));
parsingErrors.forEach(error => {
context.report({
loc: error.node.loc,
message: error.message,
error.nodes.forEach(flowNode => {
context.report({
loc: flowNode.loc,
message: error.message,
});
});
});
},
@ -167,82 +137,14 @@ function rule(context) {
return;
}
moduleRequires.push(node);
/**
* Validate that NativeModule requires are typed
*/
const {typeArguments} = node;
if (typeArguments == null) {
const methodName = node.callee.property.name;
context.report({
node,
message: ERRORS.untypedModuleRequire(hasteModuleName, methodName),
});
isModule = true;
},
InterfaceExtends(node) {
if (node.id.name !== 'TurboModule') {
return;
}
if (typeArguments.type !== 'TypeParameterInstantiation') {
return;
}
const [param] = typeArguments.params;
/**
* Validate that NativeModule requires are correctly typed
*/
if (
typeArguments.params.length !== 1 ||
param.type !== 'GenericTypeAnnotation' ||
param.id.name !== 'Spec'
) {
const methodName = node.callee.property.name;
context.report({
node,
message: ERRORS.incorrectlyTypedModuleRequire(
hasteModuleName,
methodName,
),
});
return;
}
/**
* Validate the TurboModuleRegistry.get<Spec>(...) argument
*/
if (node.arguments.length === 1) {
const methodName = node.callee.property.name;
if (node.arguments[0].type !== 'Literal') {
context.report({
node: node.arguments[0],
message: ERRORS.calledModuleRequireWithWrongType(
hasteModuleName,
methodName,
node.arguments[0].type,
),
});
return;
}
if (typeof node.arguments[0].value !== 'string') {
context.report({
node: node.arguments[0],
message: ERRORS.calledModuleRequireWithWrongLiteral(
hasteModuleName,
methodName,
node.arguments[0].value,
),
});
return;
}
}
return true;
isModule = true;
},
};
}

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

@ -11,11 +11,17 @@
'use strict';
class ParserError extends Error {
node: $FlowFixMe;
constructor(hasteModuleName: string, astNode: $FlowFixMe, message: string) {
nodes: $ReadOnlyArray<$FlowFixMe>;
constructor(
hasteModuleName: string,
astNodeOrNodes: $FlowFixMe,
message: string,
) {
super(`Module ${hasteModuleName}: ${message}`);
this.node = astNode;
this.nodes = Array.isArray(astNodeOrNodes)
? astNodeOrNodes
: [astNodeOrNodes];
// assign the error class name in your custom error (as a shortcut)
this.name = this.constructor.name;

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

@ -114,26 +114,6 @@ function getConfigType(
}
}
const withSpace = (...args) => args.join('\\s*');
/**
* Parse the TurboModuleRegistry.get(Enforcing)? call using RegExp.
* Why? This call can appear anywhere in the NativeModule spec. Currently,
* there is no good way of traversing the AST to find the MemberExpression
* responsible for the call.
*/
const TURBO_MODULE_REGISTRY_REQUIRE_REGEX_STRING = withSpace(
'TurboModuleRegistry',
'\\.',
'get(Enforcing)?',
'<',
'Spec',
'>',
'\\(',
'[\'"](?<nativeModuleName>[A-Za-z$_0-9]+)[\'"]',
',?',
'\\)',
);
function buildSchema(contents: string, filename: ?string): SchemaType {
const ast = flowParser.parse(contents);
@ -147,36 +127,8 @@ function buildSchema(contents: string, filename: ?string): SchemaType {
}
const hasteModuleName = path.basename(filename).replace(/\.js$/, '');
const regex = new RegExp(TURBO_MODULE_REGISTRY_REQUIRE_REGEX_STRING, 'g');
let match = regex.exec(contents);
const errorHeader = `Error while parsing Module '${hasteModuleName}'`;
if (match == null) {
throw new Error(
`${errorHeader}: No call to TurboModuleRegistry.get<Spec>('...') detected.`,
);
}
const moduleNames = [];
while (match != null) {
const resultGroups = match.groups;
invariant(
resultGroups != null,
`Couldn't parse TurboModuleRegistry.(get|getEnforcing)<Spec> call in module '${hasteModuleName}'.`,
);
if (!moduleNames.includes(resultGroups.nativeModuleName)) {
moduleNames.push(resultGroups.nativeModuleName);
}
match = regex.exec(contents);
}
const [parsingErrors, guard] = createParserErrorCapturer();
const schema = guard(() =>
buildModuleSchema(hasteModuleName, moduleNames, ast, guard),
);
const schema = guard(() => buildModuleSchema(hasteModuleName, ast, guard));
if (parsingErrors.length > 0) {
/**

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

@ -28,7 +28,7 @@ class ModuleFlowInterfaceNotParserError extends ParserError {
super(
hasteModuleName,
ast,
`Module ${hasteModuleName}: No Flow interfaces extending TurboModule were detected in this NativeModule spec.`,
`Module ${hasteModuleName}: No Flow interfaces extending TurboModule were detected in this NativeNativeModule spec.`,
);
}
}
@ -206,6 +206,91 @@ class UnsupportedFunctionReturnTypeAnnotationParserError extends ParserError {
}
}
class UnusedModuleFlowInterfaceParserError extends ParserError {
constructor(hasteModuleName: string, flowInterface: $FlowFixMe) {
super(
hasteModuleName,
flowInterface,
"Unused NativeModule spec. Please load the NativeModule by calling TurboModuleRegistry.get<Spec>('<moduleName>').",
);
}
}
class MoreThanOneModuleRegistryCallsParserError extends ParserError {
constructor(
hasteModuleName: string,
flowCallExpressions: $FlowFixMe,
numCalls: number,
) {
super(
hasteModuleName,
flowCallExpressions,
`Every NativeModule spec file must contain exactly one NativeModule load. This file contains ${numCalls}. Please simplify this spec file, splitting it as necessary, to remove the extraneous loads.`,
);
}
}
class UntypedModuleRegistryCallParserError extends ParserError {
constructor(
hasteModuleName: string,
flowCallExpression: $FlowFixMe,
methodName: string,
moduleName: string,
) {
super(
hasteModuleName,
flowCallExpression,
`Please type this NativeModule load: TurboModuleRegistry.${methodName}<Spec>('${moduleName}').`,
);
}
}
class IncorrectModuleRegistryCallTypeParameterParserError extends ParserError {
constructor(
hasteModuleName: string,
flowTypeArguments: $FlowFixMe,
methodName: string,
moduleName: string,
) {
super(
hasteModuleName,
flowTypeArguments,
`Please change these type arguments to reflect TurboModuleRegistry.${methodName}<Spec>('${moduleName}').`,
);
}
}
class IncorrectModuleRegistryCallArityParserError extends ParserError {
constructor(
hasteModuleName: string,
flowCallExpression: $FlowFixMe,
methodName: string,
incorrectArity: number,
) {
super(
hasteModuleName,
flowCallExpression,
`Please call TurboModuleRegistry.${methodName}<Spec>() with exactly one argument. Detected ${incorrectArity}.`,
);
}
}
class IncorrectModuleRegistryCallArgumentTypeParserError extends ParserError {
constructor(
hasteModuleName: string,
flowArgument: $FlowFixMe,
methodName: string,
type: string,
) {
const a = /[aeiouy]/.test(type.toLowerCase()) ? 'an' : 'a';
super(
hasteModuleName,
flowArgument,
`Please call TurboModuleRegistry.${methodName}<Spec>() with a string literal. Detected ${a} '${type}'`,
);
}
}
module.exports = {
IncorrectlyParameterizedFlowGenericParserError,
MisnamedModuleFlowInterfaceParserError,
@ -219,4 +304,10 @@ module.exports = {
UnsupportedModulePropertyParserError,
UnsupportedObjectPropertyTypeAnnotationParserError,
UnsupportedObjectPropertyValueTypeAnnotationParserError,
UnusedModuleFlowInterfaceParserError,
MoreThanOneModuleRegistryCallsParserError,
UntypedModuleRegistryCallParserError,
IncorrectModuleRegistryCallTypeParameterParserError,
IncorrectModuleRegistryCallArityParserError,
IncorrectModuleRegistryCallArgumentTypeParserError,
};

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

@ -26,7 +26,7 @@ import type {TypeDeclarationMap} from '../utils.js';
import type {ParserErrorCapturer} from '../utils';
import type {NativeModuleTypeAnnotation} from '../../../CodegenSchema.js';
const {resolveTypeAnnotation, getTypes} = require('../utils.js');
const {resolveTypeAnnotation, getTypes, findChildren} = require('../utils.js');
const {unwrapNullable, wrapNullable} = require('./utils');
const {
IncorrectlyParameterizedFlowGenericParserError,
@ -41,6 +41,12 @@ const {
UnsupportedModulePropertyParserError,
UnsupportedObjectPropertyTypeAnnotationParserError,
UnsupportedObjectPropertyValueTypeAnnotationParserError,
UnusedModuleFlowInterfaceParserError,
MoreThanOneModuleRegistryCallsParserError,
UntypedModuleRegistryCallParserError,
IncorrectModuleRegistryCallTypeParameterParserError,
IncorrectModuleRegistryCallArityParserError,
IncorrectModuleRegistryCallArgumentTypeParserError,
} = require('./errors.js');
const invariant = require('invariant');
@ -520,9 +526,41 @@ function buildPropertySchema(
};
}
function isCallIntoModuleRegistry(node) {
if (node.type !== 'CallExpression') {
return false;
}
const callExpression = node;
if (callExpression.callee.type !== 'MemberExpression') {
return false;
}
const memberExpression = callExpression.callee;
if (
!(
memberExpression.object.type === 'Identifier' &&
memberExpression.object.name === 'TurboModuleRegistry'
)
) {
return false;
}
if (
!(
memberExpression.property.type === 'Identifier' &&
(memberExpression.property.name === 'get' ||
memberExpression.property.name === 'getEnforcing')
)
) {
return false;
}
return true;
}
function buildModuleSchema(
hasteModuleName: string,
moduleNames: $ReadOnlyArray<string>,
/**
* TODO(T71778680): Flow-type this node.
*/
@ -555,6 +593,77 @@ function buildModuleSchema(
);
}
// Parse Module Names
const moduleName = guard((): string => {
const callExpressions = findChildren(ast, isCallIntoModuleRegistry);
if (callExpressions.length === 0) {
throw new UnusedModuleFlowInterfaceParserError(
hasteModuleName,
types[moduleInterfaceName],
);
}
if (callExpressions.length > 1) {
throw new MoreThanOneModuleRegistryCallsParserError(
hasteModuleName,
callExpressions,
callExpressions.length,
);
}
const [callExpression] = callExpressions;
const {typeArguments} = callExpression;
const methodName = callExpression.callee.property.name;
if (callExpression.arguments.length !== 1) {
throw new IncorrectModuleRegistryCallArityParserError(
hasteModuleName,
callExpression,
methodName,
callExpression.arguments.length,
);
}
if (callExpression.arguments[0].type !== 'Literal') {
const {type} = callExpression.arguments[0];
throw new IncorrectModuleRegistryCallArgumentTypeParserError(
hasteModuleName,
callExpression.arguments[0],
methodName,
type,
);
}
const $moduleName = callExpression.arguments[0].value;
if (typeArguments == null) {
throw new UntypedModuleRegistryCallParserError(
hasteModuleName,
callExpression,
methodName,
$moduleName,
);
}
if (
typeArguments.type !== 'TypeParameterInstantiation' ||
typeArguments.params.length !== 1 ||
typeArguments.params[0].type !== 'GenericTypeAnnotation' ||
typeArguments.params[0].id.name !== 'Spec'
) {
throw new IncorrectModuleRegistryCallTypeParameterParserError(
hasteModuleName,
typeArguments,
methodName,
$moduleName,
);
}
return $moduleName;
});
const moduleNames = moduleName == null ? [] : [moduleName];
// Some module names use platform suffix to indicate platform-exclusive modules.
// Eventually this should be made explicit in the Flow type itself.
// Also check the hasteModuleName for platform suffix.

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

@ -137,9 +137,41 @@ function createParserErrorCapturer(): [
return [errors, guard];
}
// TODO(T71778680): Flow-type this node.
function findChildren(
astNode: $FlowFixMe,
predicate: (node: $FlowFixMe) => boolean,
): $ReadOnlyArray<$FlowFixMe> {
if (predicate(astNode)) {
return astNode;
}
const found = [];
const queue = Object.values(astNode);
while (queue.length !== 0) {
let item = queue.shift();
if (!(typeof item === 'object' && item != null)) {
continue;
}
if (Array.isArray(item)) {
queue.push(...item);
} else if (typeof item.type === 'string' && predicate(item)) {
found.push(item);
} else {
queue.push(...Object.values(item));
}
}
return found;
}
module.exports = {
getValueFromTypes,
resolveTypeAnnotation,
createParserErrorCapturer,
getTypes,
findChildren,
};