From 898f73deef5bdf0e87a9f3d8d4f0f3b1d20f87bb Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Fri, 4 Dec 2020 14:19:27 -0800 Subject: [PATCH] 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() or TurboModuleRegisty.getEnforcing(). *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 --- .../src/parsers/flow/index.js | 145 +++++++----------- .../src/parsers/flow/modules/index.js | 56 ++++--- .../src/parsers/flow/utils.js | 32 ++-- 3 files changed, 101 insertions(+), 132 deletions(-) diff --git a/packages/react-native-codegen/src/parsers/flow/index.js b/packages/react-native-codegen/src/parsers/flow/index.js index 1462be4350b..0cf479b01b1 100644 --- a/packages/react-native-codegen/src/parsers/flow/index.js +++ b/packages/react-native-codegen/src/parsers/flow/index.js @@ -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}'`); } } diff --git a/packages/react-native-codegen/src/parsers/flow/modules/index.js b/packages/react-native-codegen/src/parsers/flow/modules/index.js index 178519bef9e..955f8358d5a 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -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).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 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, };