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:
Ramanpreet Nara authored and Facebook GitHub Bot committed 2020-12-04 14:21:53 -08:00
1 parent 3e5779ce93
commit 898f73deef
3 files changed
+101 -132

No files matched your search

+54 -91
View File
@@ -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}'`);
}
}
@@ -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
View File
@@ -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,
};