mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Introduce visit(ast, visitor) utility
Summary: In the Codegen, we need to answer the following questions: *Question:* What are all the calls into TurboModuleRegistry? *Answer:* Find all CallExpressions that represent TurboModuleRegistry.get<Spec>() or TurboModuleRegisty.getEnforcing<Spec>(). *Question:* Is this a component spec? *Answer:* Does this spec have a CallExpression where the callee is 'codegenNativeComponent'? *Question:* Is this a module spec? *Answer:* Does this spec have an interface that extends TurboModule? All these answers can be implemented using the visitor pattern. Hence, this diff introduces the `visit` utility, and uses it to answer these questions. **Motivation:** Cleaner code. Changelog: [Internal] Reviewed By: hramos Differential Revision: D25162617 fbshipit-source-id: 66ec95fc07ecb29aa9bf9993cb826204af283d03
This commit is contained in:
1 parent
3e5779ce93
commit
898f73deef
3 files changed
+101
-132
No files matched your search
+54
-91
@@ -19,83 +19,41 @@ const {buildComponentSchema} = require('./components');
|
||||
const {wrapComponentSchema} = require('./components/schema');
|
||||
const {buildModuleSchema} = require('./modules');
|
||||
const {wrapModuleSchema} = require('./modules/schema');
|
||||
const {createParserErrorCapturer} = require('./utils');
|
||||
const {createParserErrorCapturer, visit} = require('./utils');
|
||||
const invariant = require('invariant');
|
||||
|
||||
function isComponent(ast) {
|
||||
const defaultExports = ast.body.filter(
|
||||
node => node.type === 'ExportDefaultDeclaration',
|
||||
);
|
||||
|
||||
if (defaultExports.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let declaration = defaultExports[0].declaration;
|
||||
// codegenNativeComponent can be nested inside a cast
|
||||
// expression so we need to go one level deeper
|
||||
if (declaration.type === 'TypeCastExpression') {
|
||||
declaration = declaration.expression;
|
||||
}
|
||||
|
||||
if (declaration.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
declaration.callee.type === 'Identifier' &&
|
||||
declaration.callee.name === 'codegenNativeComponent'
|
||||
);
|
||||
}
|
||||
|
||||
function isModule(
|
||||
// TODO(T71778680): Flow-type this node.
|
||||
ast: $FlowFixMe,
|
||||
) {
|
||||
const moduleInterfaces = ast.body
|
||||
.map(node => {
|
||||
if (
|
||||
node.type === 'ExportNamedDeclaration' &&
|
||||
node.exportKind === 'type' &&
|
||||
node.declaration.type === 'InterfaceDeclaration'
|
||||
) {
|
||||
return node.declaration;
|
||||
}
|
||||
return node;
|
||||
})
|
||||
.filter(declaration => {
|
||||
return (
|
||||
declaration.type === 'InterfaceDeclaration' &&
|
||||
declaration.extends.length === 1 &&
|
||||
declaration.extends[0].type === 'InterfaceExtends' &&
|
||||
declaration.extends[0].id.name === 'TurboModule'
|
||||
);
|
||||
})
|
||||
.map(declaration => declaration.id.name);
|
||||
|
||||
if (moduleInterfaces.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getConfigType(
|
||||
// TODO(T71778680): Flow-type this node.
|
||||
ast: $FlowFixMe,
|
||||
): 'module' | 'component' {
|
||||
const isConfigAComponent = isComponent(ast);
|
||||
const isConfigAModule = isModule(ast);
|
||||
let isComponent = false;
|
||||
let isModule = false;
|
||||
|
||||
if (isConfigAModule && isConfigAComponent) {
|
||||
visit(ast, {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'codegenNativeComponent'
|
||||
) {
|
||||
isComponent = true;
|
||||
}
|
||||
},
|
||||
InterfaceExtends(node) {
|
||||
if (node.id.name === 'TurboModule') {
|
||||
isModule = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (isModule && isComponent) {
|
||||
throw new Error(
|
||||
'Found type extending "TurboModule" and exported "codegenNativeComponent" declaration in one file. Split them into separated files.',
|
||||
);
|
||||
}
|
||||
|
||||
if (isConfigAModule) {
|
||||
if (isModule) {
|
||||
return 'module';
|
||||
} else if (isConfigAComponent) {
|
||||
} else if (isComponent) {
|
||||
return 'component';
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -108,40 +66,45 @@ function getConfigType(
|
||||
|
||||
function buildSchema(contents: string, filename: ?string): SchemaType {
|
||||
const ast = flowParser.parse(contents);
|
||||
|
||||
const configType = getConfigType(ast);
|
||||
|
||||
if (configType === 'component') {
|
||||
return wrapComponentSchema(buildComponentSchema(ast));
|
||||
} else {
|
||||
if (filename === undefined || filename === null) {
|
||||
throw new Error('Filepath expected while parasing a module');
|
||||
switch (configType) {
|
||||
case 'component': {
|
||||
return wrapComponentSchema(buildComponentSchema(ast));
|
||||
}
|
||||
const hasteModuleName = path.basename(filename).replace(/\.js$/, '');
|
||||
case 'module': {
|
||||
if (filename === undefined || filename === null) {
|
||||
throw new Error('Filepath expected while parasing a module');
|
||||
}
|
||||
const hasteModuleName = path.basename(filename).replace(/\.js$/, '');
|
||||
|
||||
const [parsingErrors, tryParse] = createParserErrorCapturer();
|
||||
const schema = tryParse(() =>
|
||||
buildModuleSchema(hasteModuleName, ast, tryParse),
|
||||
);
|
||||
const [parsingErrors, tryParse] = createParserErrorCapturer();
|
||||
const schema = tryParse(() =>
|
||||
buildModuleSchema(hasteModuleName, ast, tryParse),
|
||||
);
|
||||
|
||||
if (parsingErrors.length > 0) {
|
||||
/**
|
||||
* TODO(T77968131): We have two options:
|
||||
* - Throw the first error, but indicate there are more then one errors.
|
||||
* - Display all errors, nicely formatted.
|
||||
*
|
||||
* For the time being, we're just throw the first error.
|
||||
**/
|
||||
if (parsingErrors.length > 0) {
|
||||
/**
|
||||
* TODO(T77968131): We have two options:
|
||||
* - Throw the first error, but indicate there are more then one errors.
|
||||
* - Display all errors, nicely formatted.
|
||||
*
|
||||
* For the time being, we're just throw the first error.
|
||||
**/
|
||||
|
||||
throw parsingErrors[0];
|
||||
throw parsingErrors[0];
|
||||
}
|
||||
|
||||
invariant(
|
||||
schema != null,
|
||||
'When there are no parsing errors, the schema should not be null',
|
||||
);
|
||||
|
||||
return wrapModuleSchema(schema, hasteModuleName);
|
||||
}
|
||||
|
||||
invariant(
|
||||
schema != null,
|
||||
'When there are no parsing errors, the schema should not be null',
|
||||
);
|
||||
|
||||
return wrapModuleSchema(schema, hasteModuleName);
|
||||
default:
|
||||
(configType: empty);
|
||||
throw new Error(`Unsupported config type '${configType}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-24
@@ -26,7 +26,7 @@ import type {TypeDeclarationMap} from '../utils.js';
|
||||
import type {ParserErrorCapturer} from '../utils';
|
||||
import type {NativeModuleTypeAnnotation} from '../../../CodegenSchema.js';
|
||||
|
||||
const {resolveTypeAnnotation, getTypes, findChildren} = require('../utils.js');
|
||||
const {resolveTypeAnnotation, getTypes, visit} = require('../utils.js');
|
||||
const {unwrapNullable, wrapNullable} = require('./utils');
|
||||
const {
|
||||
IncorrectlyParameterizedFlowGenericParserError,
|
||||
@@ -527,7 +527,7 @@ function buildPropertySchema(
|
||||
};
|
||||
}
|
||||
|
||||
function isCallIntoModuleRegistry(node) {
|
||||
function isModuleRegistryCall(node) {
|
||||
if (node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
@@ -560,6 +560,15 @@ function isCallIntoModuleRegistry(node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isModuleInterface(node) {
|
||||
return (
|
||||
node.type === 'InterfaceDeclaration' &&
|
||||
node.extends.length === 1 &&
|
||||
node.extends[0].type === 'InterfaceExtends' &&
|
||||
node.extends[0].id.name === 'TurboModule'
|
||||
);
|
||||
}
|
||||
|
||||
function buildModuleSchema(
|
||||
hasteModuleName: string,
|
||||
/**
|
||||
@@ -569,46 +578,46 @@ function buildModuleSchema(
|
||||
tryParse: ParserErrorCapturer,
|
||||
): NativeModuleSchema {
|
||||
const types = getTypes(ast);
|
||||
const moduleInterfaceNames = (Object.keys(
|
||||
types,
|
||||
): $ReadOnlyArray<string>).filter((typeName: string) => {
|
||||
const declaration = types[typeName];
|
||||
return (
|
||||
declaration.type === 'InterfaceDeclaration' &&
|
||||
declaration.extends.length === 1 &&
|
||||
declaration.extends[0].type === 'InterfaceExtends' &&
|
||||
declaration.extends[0].id.name === 'TurboModule'
|
||||
);
|
||||
});
|
||||
const moduleSpecs = (Object.values(types): $ReadOnlyArray<$FlowFixMe>).filter(
|
||||
isModuleInterface,
|
||||
);
|
||||
|
||||
if (moduleInterfaceNames.length === 0) {
|
||||
if (moduleSpecs.length === 0) {
|
||||
throw new ModuleFlowInterfaceNotFoundParserError(hasteModuleName, ast);
|
||||
}
|
||||
|
||||
if (moduleInterfaceNames.length > 1) {
|
||||
if (moduleSpecs.length > 1) {
|
||||
throw new MoreThanOneModuleFlowInterfaceParserError(
|
||||
hasteModuleName,
|
||||
moduleInterfaceNames.map(name => types[name]),
|
||||
moduleInterfaceNames,
|
||||
moduleSpecs,
|
||||
moduleSpecs.map(node => node.id.name),
|
||||
);
|
||||
}
|
||||
|
||||
const [moduleInterfaceName] = moduleInterfaceNames;
|
||||
const [moduleSpec] = moduleSpecs;
|
||||
|
||||
if (moduleInterfaceName !== 'Spec') {
|
||||
if (moduleSpec.id.name !== 'Spec') {
|
||||
throw new MisnamedModuleFlowInterfaceParserError(
|
||||
hasteModuleName,
|
||||
types[moduleInterfaceName].id,
|
||||
moduleSpec.id,
|
||||
);
|
||||
}
|
||||
|
||||
// Parse Module Names
|
||||
const moduleName = tryParse((): string => {
|
||||
const callExpressions = findChildren(ast, isCallIntoModuleRegistry);
|
||||
const callExpressions = [];
|
||||
visit(ast, {
|
||||
CallExpression(node) {
|
||||
if (isModuleRegistryCall(node)) {
|
||||
callExpressions.push(node);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (callExpressions.length === 0) {
|
||||
throw new UnusedModuleFlowInterfaceParserError(
|
||||
hasteModuleName,
|
||||
types[moduleInterfaceName],
|
||||
moduleSpec,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -687,8 +696,7 @@ function buildModuleSchema(
|
||||
}
|
||||
});
|
||||
|
||||
const declaration = types[moduleInterfaceName];
|
||||
return (declaration.body.properties: $ReadOnlyArray<$FlowFixMe>)
|
||||
return (moduleSpec.body.properties: $ReadOnlyArray<$FlowFixMe>)
|
||||
.filter(property => property.type === 'ObjectTypeProperty')
|
||||
.map<?{
|
||||
aliasMap: NativeModuleAliasMap,
|
||||
|
||||
+15
-17
@@ -137,18 +137,14 @@ function createParserErrorCapturer(): [
|
||||
return [errors, guard];
|
||||
}
|
||||
|
||||
// TODO(T71778680): Flow-type this node.
|
||||
function findChildren(
|
||||
// TODO(T71778680): Flow-type ASTNodes.
|
||||
function visit(
|
||||
astNode: $FlowFixMe,
|
||||
predicate: (node: $FlowFixMe) => boolean,
|
||||
): $ReadOnlyArray<$FlowFixMe> {
|
||||
if (predicate(astNode)) {
|
||||
return astNode;
|
||||
}
|
||||
|
||||
const found = [];
|
||||
const queue = Object.values(astNode);
|
||||
|
||||
visitor: {
|
||||
[type: string]: (node: $FlowFixMe) => void,
|
||||
},
|
||||
) {
|
||||
const queue = [astNode];
|
||||
while (queue.length !== 0) {
|
||||
let item = queue.shift();
|
||||
|
||||
@@ -156,16 +152,18 @@ function findChildren(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
if (
|
||||
typeof item.type === 'string' &&
|
||||
typeof visitor[item.type] === 'function'
|
||||
) {
|
||||
// Don't visit any children
|
||||
visitor[item.type](item);
|
||||
} else 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 = {
|
||||
@@ -173,5 +171,5 @@ module.exports = {
|
||||
resolveTypeAnnotation,
|
||||
createParserErrorCapturer,
|
||||
getTypes,
|
||||
findChildren,
|
||||
visit,
|
||||
};
|
||||
Reference in new issue
Block a user