diff --git a/packages/react-native-codegen/src/parsers/typescript/errors.js b/packages/react-native-codegen/src/parsers/typescript/errors.js new file mode 100644 index 00000000000..b480f321f59 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/errors.js @@ -0,0 +1,36 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +class ParserError extends Error { + nodes: $ReadOnlyArray<$FlowFixMe>; + constructor( + hasteModuleName: string, + astNodeOrNodes: $FlowFixMe, + message: string, + ) { + super(`Module ${hasteModuleName}: ${message}`); + + this.nodes = Array.isArray(astNodeOrNodes) + ? astNodeOrNodes + : [astNodeOrNodes]; + + // assign the error class name in your custom error (as a shortcut) + this.name = this.constructor.name; + + // capturing the stack trace keeps the reference to your error class + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = { + ParserError, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/utils.js b/packages/react-native-codegen/src/parsers/typescript/utils.js new file mode 100644 index 00000000000..70d4536a12e --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/utils.js @@ -0,0 +1,210 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +const {ParserError} = require('./errors'); + +/** + * This FlowFixMe is supposed to refer to an InterfaceDeclaration or TypeAlias + * declaration type. Unfortunately, we don't have those types, because flow-parser + * generates them, and flow-parser is not type-safe. In the future, we should find + * a way to get these types from our flow parser library. + * + * TODO(T71778680): Flow type AST Nodes + */ +export type TypeDeclarationMap = {[declarationName: string]: $FlowFixMe}; + +function getTypes(ast: $FlowFixMe): TypeDeclarationMap { + return ast.body.reduce((types, node) => { + if (node.type === 'ExportNamedDeclaration' && node.exportKind === 'type') { + if ( + node.declaration.type === 'TypeAlias' || + node.declaration.type === 'InterfaceDeclaration' + ) { + types[node.declaration.id.name] = node.declaration; + } + } else if ( + node.type === 'TypeAlias' || + node.type === 'InterfaceDeclaration' + ) { + types[node.id.name] = node; + } + return types; + }, {}); +} + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +export type ASTNode = Object; + +const invariant = require('invariant'); + +type TypeAliasResolutionStatus = + | $ReadOnly<{ + successful: true, + aliasName: string, + }> + | $ReadOnly<{ + successful: false, + }>; + +function resolveTypeAnnotation( + // TODO(T71778680): This is an Flow TypeAnnotation. Flow-type this + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, +): { + nullable: boolean, + typeAnnotation: $FlowFixMe, + typeAliasResolutionStatus: TypeAliasResolutionStatus, +} { + invariant( + typeAnnotation != null, + 'resolveTypeAnnotation(): typeAnnotation cannot be null', + ); + + let node = typeAnnotation; + let nullable = false; + let typeAliasResolutionStatus: TypeAliasResolutionStatus = { + successful: false, + }; + + for (;;) { + if (node.type === 'NullableTypeAnnotation') { + nullable = true; + node = node.typeAnnotation; + } else if (node.type === 'GenericTypeAnnotation') { + typeAliasResolutionStatus = { + successful: true, + aliasName: node.id.name, + }; + const resolvedTypeAnnotation = types[node.id.name]; + if (resolvedTypeAnnotation == null) { + break; + } + + invariant( + resolvedTypeAnnotation.type === 'TypeAlias', + `GenericTypeAnnotation '${node.id.name}' must resolve to a TypeAlias. Instead, it resolved to a '${resolvedTypeAnnotation.type}'`, + ); + + node = resolvedTypeAnnotation.right; + } else { + break; + } + } + + return { + nullable: nullable, + typeAnnotation: node, + typeAliasResolutionStatus, + }; +} + +function getValueFromTypes(value: ASTNode, types: TypeDeclarationMap): ASTNode { + if (value.type === 'GenericTypeAnnotation' && types[value.id.name]) { + return getValueFromTypes(types[value.id.name].right, types); + } + return value; +} + +export type ParserErrorCapturer = (fn: () => T) => ?T; + +function createParserErrorCapturer(): [ + Array, + ParserErrorCapturer, +] { + const errors = []; + function guard(fn: () => T): ?T { + try { + return fn(); + } catch (error) { + if (!(error instanceof ParserError)) { + throw error; + } + errors.push(error); + + return null; + } + } + + return [errors, guard]; +} + +// TODO(T71778680): Flow-type ASTNodes. +function visit( + astNode: $FlowFixMe, + visitor: { + [type: string]: (node: $FlowFixMe) => void, + }, +) { + const queue = [astNode]; + while (queue.length !== 0) { + let item = queue.shift(); + + if (!(typeof item === 'object' && item != null)) { + continue; + } + + 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 { + queue.push(...Object.values(item)); + } + } +} + +// TODO(T71778680): Flow-type ASTNodes. +function isModuleRegistryCall(node: $FlowFixMe): boolean { + 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; +} + +module.exports = { + getValueFromTypes, + resolveTypeAnnotation, + createParserErrorCapturer, + getTypes, + visit, + isModuleRegistryCall, +};