diff --git a/packages/eslint-plugin-specs/react-native-modules.js b/packages/eslint-plugin-specs/react-native-modules.js index fa0b58440d2f1e..8a8a037ef0001a 100644 --- a/packages/eslint-plugin-specs/react-native-modules.js +++ b/packages/eslint-plugin-specs/react-native-modules.js @@ -133,12 +133,11 @@ function rule(context) { const {buildModuleSchema, createParserErrorCapturer, parser} = requireModuleParser(); - const flowParser = require('flow-parser'); const [parsingErrors, tryParse] = createParserErrorCapturer(); const sourceCode = context.getSourceCode().getText(); - const ast = flowParser.parse(sourceCode, {enums: true}); + const ast = parser.getAst(sourceCode); tryParse(() => { buildModuleSchema(hasteModuleName, ast, tryParse, parser); diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js index 00de83d9b4f706..74db8652b9ec0e 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js @@ -17,22 +17,32 @@ import { parseObjectProperty, wrapNullable, unwrapNullable, + buildSchemaFromConfigType, + buildSchema, } from '../parsers-commons'; import type {ParserType} from '../errors'; +const {Visitor} = require('../flow/Visitor'); +const {wrapComponentSchema} = require('../flow/components/schema'); +const {buildComponentSchema} = require('../flow/components'); +const {buildModuleSchema} = require('../flow/modules'); +const {isModuleRegistryCall} = require('../utils.js'); const { + ParserError, UnsupportedObjectPropertyTypeAnnotationParserError, } = require('../errors'); import {MockedParser} from '../parserMock'; -import {TypeScriptParser} from '../typescript/parser'; const parser = new MockedParser(); -const typeScriptParser = new TypeScriptParser(); const flowTranslateTypeAnnotation = require('../flow/modules/index'); const typeScriptTranslateTypeAnnotation = require('../typescript/modules/index'); +beforeEach(() => { + jest.clearAllMocks(); +}); + describe('wrapNullable', () => { describe('when nullable is true', () => { it('returns nullable type annotation', () => { @@ -338,3 +348,428 @@ describe('parseObjectProperty', () => { }); }); }); + +describe('buildSchemaFromConfigType', () => { + const astMock = { + type: 'Program', + loc: { + source: null, + start: {line: 2, column: 10}, + end: {line: 16, column: 62}, + }, + range: [11, 373], + body: [], + comments: [], + errors: [], + }; + + const componentSchemaMock = { + filename: 'filename', + componentName: 'componentName', + extendsProps: [], + events: [], + props: [], + commands: [], + }; + + const moduleSchemaMock = { + type: 'NativeModule', + aliases: {}, + spec: {properties: []}, + moduleName: '', + }; + + const wrapComponentSchemaMock = jest.fn(); + const buildComponentSchemaMock = jest.fn(_ => componentSchemaMock); + const buildModuleSchemaMock = jest.fn((_0, _1, _2, _3) => moduleSchemaMock); + + const buildSchemaFromConfigTypeHelper = ( + configType: 'module' | 'component' | 'none', + filename: ?string, + ) => + buildSchemaFromConfigType( + configType, + filename, + astMock, + wrapComponentSchemaMock, + buildComponentSchemaMock, + buildModuleSchemaMock, + parser, + ); + + describe('when configType is none', () => { + it('returns an empty schema', () => { + const schema = buildSchemaFromConfigTypeHelper('none'); + + expect(schema).toEqual({modules: {}}); + }); + }); + + describe('when configType is component', () => { + it('calls buildComponentSchema with ast and wrapComponentSchema with the result', () => { + buildSchemaFromConfigTypeHelper('component'); + + expect(buildComponentSchemaMock).toHaveBeenCalledTimes(1); + expect(buildComponentSchemaMock).toHaveBeenCalledWith(astMock); + expect(wrapComponentSchemaMock).toHaveBeenCalledTimes(1); + expect(wrapComponentSchemaMock).toHaveBeenCalledWith(componentSchemaMock); + + expect(buildModuleSchemaMock).not.toHaveBeenCalled(); + }); + }); + + describe('when configType is module', () => { + describe('when filename is undefined', () => { + it('throws an error', () => { + expect(() => buildSchemaFromConfigTypeHelper('module')).toThrow( + 'Filepath expected while parasing a module', + ); + }); + }); + + describe('when filename is null', () => { + it('throws an error', () => { + expect(() => buildSchemaFromConfigTypeHelper('module', null)).toThrow( + 'Filepath expected while parasing a module', + ); + }); + }); + + describe('when filename is defined and not null', () => { + describe('when buildModuleSchema throws', () => { + it('throws the error', () => { + const parserError = new ParserError( + 'moduleName', + astMock, + 'Something went wrong', + ); + buildModuleSchemaMock.mockImplementationOnce(() => { + throw parserError; + }); + + expect(() => + buildSchemaFromConfigTypeHelper('module', 'filename'), + ).toThrow(parserError); + }); + }); + + describe('when buildModuleSchema returns null', () => { + it('throws an error', () => { + // $FlowIgnore[incompatible-call] - This is to test an invariant + buildModuleSchemaMock.mockReturnValueOnce(null); + + expect(() => + buildSchemaFromConfigTypeHelper('module', 'filename'), + ).toThrow( + 'When there are no parsing errors, the schema should not be null', + ); + }); + }); + + describe('when buildModuleSchema returns a schema', () => { + it('calls buildModuleSchema with ast and wrapModuleSchema with the result', () => { + buildSchemaFromConfigTypeHelper('module', 'filename'); + + expect(buildModuleSchemaMock).toHaveBeenCalledTimes(1); + expect(buildModuleSchemaMock).toHaveBeenCalledWith( + 'filename', + astMock, + expect.any(Function), + parser, + ); + + expect(buildComponentSchemaMock).not.toHaveBeenCalled(); + expect(wrapComponentSchemaMock).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('isModuleRegistryCall', () => { + describe('when node is not of CallExpression type', () => { + it('returns false', () => { + const node = { + type: 'NotCallExpression', + }; + expect(isModuleRegistryCall(node)).toBe(false); + }); + }); + + describe('when node is of CallExpressionType', () => { + describe('when callee type is not of MemberExpression type', () => { + it('returns false', () => { + const node = { + type: 'CallExpression', + callee: { + type: 'NotMemberExpression', + }, + }; + expect(isModuleRegistryCall(node)).toBe(false); + }); + }); + + describe('when callee type is of MemberExpression type', () => { + describe('when memberExpression has an object of type different than "Identifier"', () => { + it('returns false', () => { + const node = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'NotIdentifier', + name: 'TurboModuleRegistry', + }, + }, + }; + expect(isModuleRegistryCall(node)).toBe(false); + }); + }); + + describe('when memberExpression has an object of name different than "TurboModuleRegistry"', () => { + it('returns false', () => { + const node = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'NotTurboModuleRegistry', + }, + }, + }; + expect(isModuleRegistryCall(node)).toBe(false); + }); + }); + + describe('when memberExpression has an object of type "Identifier" and name "TurboModuleRegistry', () => { + describe('when memberExpression has a property of type different than "Identifier"', () => { + it('returns false', () => { + const node = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'TurboModuleRegistry', + }, + property: { + type: 'NotIdentifier', + name: 'get', + }, + }, + }; + expect(isModuleRegistryCall(node)).toBe(false); + }); + }); + + describe('when memberExpression has a property of name different than "get" or "getEnforcing', () => { + it('returns false', () => { + const node = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'TurboModuleRegistry', + }, + property: { + type: 'Identifier', + name: 'NotGet', + }, + }, + }; + expect(isModuleRegistryCall(node)).toBe(false); + }); + }); + + describe('when memberExpression has a property of type "Identifier" and of name "get" or "getEnforcing', () => { + describe('when memberExpression is computed', () => { + it('returns false', () => { + const node = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'TurboModuleRegistry', + }, + property: { + type: 'Identifier', + name: 'get', + }, + computed: true, + }, + }; + expect(isModuleRegistryCall(node)).toBe(false); + }); + }); + + describe('when memberExpression is not computed', () => { + it('returns true', () => { + const node = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'TurboModuleRegistry', + }, + property: { + type: 'Identifier', + name: 'get', + }, + computed: false, + }, + }; + expect(isModuleRegistryCall(node)).toBe(true); + }); + }); + }); + }); + }); + }); + }); +}); + +describe('buildSchema', () => { + const getConfigTypeSpy = jest.spyOn(require('../utils'), 'getConfigType'); + + describe('when there is no codegenNativeComponent and no TurboModule', () => { + const contents = ''; + + it('returns an empty module', () => { + const schema = buildSchema( + contents, + 'fileName', + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + parser, + ); + + expect(getConfigTypeSpy).not.toHaveBeenCalled(); + expect(schema).toEqual({modules: {}}); + }); + }); + + describe('when there is a codegenNativeComponent', () => { + const contents = ` + import type {ViewProps} from 'ViewPropTypes'; + import type {HostComponent} from 'react-native'; + + const codegenNativeComponent = require('codegenNativeComponent'); + + export type ModuleProps = $ReadOnly<{| + ...ViewProps, + |}>; + + export default (codegenNativeComponent( + 'Module', + ): HostComponent); + `; + + it('returns a module with good properties', () => { + const schema = buildSchema( + contents, + 'fileName', + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + parser, + ); + + expect(getConfigTypeSpy).toHaveBeenCalledTimes(1); + expect(getConfigTypeSpy).toHaveBeenCalledWith( + parser.getAst(contents), + Visitor, + ); + expect(schema).toEqual({ + modules: { + Module: { + type: 'Component', + components: { + Module: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + }, + }); + }); + }); + + describe('when there is a TurboModule', () => { + const contents = ` + import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + + export interface Spec extends TurboModule { + +getArray: (a: Array) => Array; + } + + export default (TurboModuleRegistry.getEnforcing( + 'SampleTurboModule', + ): Spec); + `; + + it('returns a module with good properties', () => { + const schema = buildSchema( + contents, + 'fileName', + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + parser, + ); + + expect(getConfigTypeSpy).toHaveBeenCalledTimes(1); + expect(getConfigTypeSpy).toHaveBeenCalledWith( + parser.getAst(contents), + Visitor, + ); + expect(schema).toEqual({ + modules: { + fileName: { + type: 'NativeModule', + aliases: {}, + spec: { + properties: [ + { + name: 'getArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: {type: 'StringTypeAnnotation'}, + }, + params: [ + { + name: 'a', + optional: false, + typeAnnotation: {type: 'ArrayTypeAnnotation'}, + }, + ], + }, + }, + ], + }, + moduleName: 'SampleTurboModule', + excludedPlatforms: undefined, + }, + }, + }); + }); + }); +}); diff --git a/packages/react-native-codegen/src/parsers/__tests__/utils-test.js b/packages/react-native-codegen/src/parsers/__tests__/utils-test.js index 0421b65e3514e6..65b681bc736f1f 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/utils-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/utils-test.js @@ -11,15 +11,11 @@ 'use strict'; -const {MockedParser} = require('../parserMock'); - const { extractNativeModuleName, createParserErrorCapturer, verifyPlatforms, visit, - buildSchemaFromConfigType, - isModuleRegistryCall, } = require('../utils.js'); const {ParserError} = require('../errors'); @@ -297,297 +293,3 @@ describe('visit', () => { }); }); }); - -describe('buildSchemaFromConfigType', () => { - const parser = new MockedParser(); - - const astMock = { - type: 'Program', - loc: { - source: null, - start: {line: 2, column: 10}, - end: {line: 16, column: 62}, - }, - range: [11, 373], - body: [], - comments: [], - errors: [], - }; - - const componentSchemaMock = { - filename: 'filename', - componentName: 'componentName', - extendsProps: [], - events: [], - props: [], - commands: [], - }; - - const moduleSchemaMock = { - type: 'NativeModule', - aliases: {}, - spec: {properties: []}, - moduleName: '', - }; - - const wrapComponentSchemaMock = jest.fn(); - const buildComponentSchemaMock = jest.fn(_ => componentSchemaMock); - const wrapModuleSchemaMock = jest.spyOn( - require('../parsers-commons'), - 'wrapModuleSchema', - ); - const buildModuleSchemaMock = jest.fn((_0, _1, _2, _3) => moduleSchemaMock); - - const buildSchemaFromConfigTypeHelper = ( - configType: 'module' | 'component' | 'none', - filename: ?string, - ) => - buildSchemaFromConfigType( - configType, - filename, - astMock, - wrapComponentSchemaMock, - buildComponentSchemaMock, - buildModuleSchemaMock, - parser, - ); - - describe('when configType is none', () => { - it('returns an empty schema', () => { - const schema = buildSchemaFromConfigTypeHelper('none'); - - expect(schema).toEqual({modules: {}}); - }); - }); - - describe('when configType is component', () => { - it('calls buildComponentSchema with ast and wrapComponentSchema with the result', () => { - buildSchemaFromConfigTypeHelper('component'); - - expect(buildComponentSchemaMock).toHaveBeenCalledTimes(1); - expect(buildComponentSchemaMock).toHaveBeenCalledWith(astMock); - expect(wrapComponentSchemaMock).toHaveBeenCalledTimes(1); - expect(wrapComponentSchemaMock).toHaveBeenCalledWith(componentSchemaMock); - - expect(buildModuleSchemaMock).not.toHaveBeenCalled(); - expect(wrapModuleSchemaMock).not.toHaveBeenCalled(); - }); - }); - - describe('when configType is module', () => { - describe('when filename is undefined', () => { - it('throws an error', () => { - expect(() => buildSchemaFromConfigTypeHelper('module')).toThrow( - 'Filepath expected while parasing a module', - ); - }); - }); - - describe('when filename is null', () => { - it('throws an error', () => { - expect(() => buildSchemaFromConfigTypeHelper('module', null)).toThrow( - 'Filepath expected while parasing a module', - ); - }); - }); - - describe('when filename is defined and not null', () => { - describe('when buildModuleSchema throws', () => { - it('throws the error', () => { - const parserError = new ParserError( - 'moduleName', - astMock, - 'Something went wrong', - ); - buildModuleSchemaMock.mockImplementationOnce(() => { - throw parserError; - }); - - expect(() => - buildSchemaFromConfigTypeHelper('module', 'filename'), - ).toThrow(parserError); - }); - }); - - describe('when buildModuleSchema returns null', () => { - it('throws an error', () => { - // $FlowIgnore[incompatible-call] - This is to test an invariant - buildModuleSchemaMock.mockReturnValueOnce(null); - - expect(() => - buildSchemaFromConfigTypeHelper('module', 'filename'), - ).toThrow( - 'When there are no parsing errors, the schema should not be null', - ); - }); - }); - - describe('when buildModuleSchema returns a schema', () => { - it('calls buildModuleSchema with ast and wrapModuleSchema with the result', () => { - buildSchemaFromConfigTypeHelper('module', 'filename'); - - expect(buildModuleSchemaMock).toHaveBeenCalledTimes(1); - expect(buildModuleSchemaMock).toHaveBeenCalledWith( - 'filename', - astMock, - expect.any(Function), - parser, - ); - expect(wrapModuleSchemaMock).toHaveBeenCalledTimes(1); - expect(wrapModuleSchemaMock).toHaveBeenCalledWith( - moduleSchemaMock, - 'filename', - ); - - expect(buildComponentSchemaMock).not.toHaveBeenCalled(); - expect(wrapComponentSchemaMock).not.toHaveBeenCalled(); - }); - }); - }); - }); - - describe('isModuleRegistryCall', () => { - describe('when node is not of CallExpression type', () => { - it('returns false', () => { - const node = { - type: 'NotCallExpression', - }; - expect(isModuleRegistryCall(node)).toBe(false); - }); - }); - - describe('when node is of CallExpressionType', () => { - describe('when callee type is not of MemberExpression type', () => { - it('returns false', () => { - const node = { - type: 'CallExpression', - callee: { - type: 'NotMemberExpression', - }, - }; - expect(isModuleRegistryCall(node)).toBe(false); - }); - }); - - describe('when callee type is of MemberExpression type', () => { - describe('when memberExpression has an object of type different than "Identifier"', () => { - it('returns false', () => { - const node = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'NotIdentifier', - name: 'TurboModuleRegistry', - }, - }, - }; - expect(isModuleRegistryCall(node)).toBe(false); - }); - }); - - describe('when memberExpression has an object of name different than "TurboModuleRegistry"', () => { - it('returns false', () => { - const node = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'NotTurboModuleRegistry', - }, - }, - }; - expect(isModuleRegistryCall(node)).toBe(false); - }); - }); - - describe('when memberExpression has an object of type "Identifier" and name "TurboModuleRegistry', () => { - describe('when memberExpression has a property of type different than "Identifier"', () => { - it('returns false', () => { - const node = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'TurboModuleRegistry', - }, - property: { - type: 'NotIdentifier', - name: 'get', - }, - }, - }; - expect(isModuleRegistryCall(node)).toBe(false); - }); - }); - - describe('when memberExpression has a property of name different than "get" or "getEnforcing', () => { - it('returns false', () => { - const node = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'TurboModuleRegistry', - }, - property: { - type: 'Identifier', - name: 'NotGet', - }, - }, - }; - expect(isModuleRegistryCall(node)).toBe(false); - }); - }); - - describe('when memberExpression has a property of type "Identifier" and of name "get" or "getEnforcing', () => { - describe('when memberExpression is computed', () => { - it('returns false', () => { - const node = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'TurboModuleRegistry', - }, - property: { - type: 'Identifier', - name: 'get', - }, - computed: true, - }, - }; - expect(isModuleRegistryCall(node)).toBe(false); - }); - }); - - describe('when memberExpression is not computed', () => { - it('returns true', () => { - const node = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'TurboModuleRegistry', - }, - property: { - type: 'Identifier', - name: 'get', - }, - computed: false, - }, - }; - expect(isModuleRegistryCall(node)).toBe(true); - }); - }); - }); - }); - }); - }); - }); -}); diff --git a/packages/react-native-codegen/src/parsers/flow/Visitor.js b/packages/react-native-codegen/src/parsers/flow/Visitor.js new file mode 100644 index 00000000000000..97ab6dea874cfd --- /dev/null +++ b/packages/react-native-codegen/src/parsers/flow/Visitor.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 + * @format + */ + +'use strict'; + +const {isModuleRegistryCall} = require('../utils'); + +function Visitor(infoMap: {isComponent: boolean, isModule: boolean}): { + [type: string]: (node: $FlowFixMe) => void, +} { + return { + CallExpression(node: $FlowFixMe) { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'codegenNativeComponent' + ) { + infoMap.isComponent = true; + } + + if (isModuleRegistryCall(node)) { + infoMap.isModule = true; + } + }, + InterfaceExtends(node: $FlowFixMe) { + if (node.id.name === 'TurboModule') { + infoMap.isModule = true; + } + }, + }; +} + +module.exports = { + Visitor, +}; diff --git a/packages/react-native-codegen/src/parsers/flow/buildSchema.js b/packages/react-native-codegen/src/parsers/flow/buildSchema.js deleted file mode 100644 index db8657da3725a7..00000000000000 --- a/packages/react-native-codegen/src/parsers/flow/buildSchema.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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 - * @format - */ - -'use strict'; - -import type {SchemaType} from '../../CodegenSchema'; -import type {Parser} from '../parser'; - -// $FlowFixMe[untyped-import] there's no flowtype flow-parser -const flowParser = require('flow-parser'); -const { - getConfigType, - buildSchemaFromConfigType, - isModuleRegistryCall, -} = require('../utils'); -const {buildComponentSchema} = require('./components'); -const {wrapComponentSchema} = require('./components/schema'); -const {buildModuleSchema} = require('./modules'); - -function Visitor(infoMap: {isComponent: boolean, isModule: boolean}) { - return { - CallExpression(node: $FlowFixMe) { - if ( - node.callee.type === 'Identifier' && - node.callee.name === 'codegenNativeComponent' - ) { - infoMap.isComponent = true; - } - - if (isModuleRegistryCall(node)) { - infoMap.isModule = true; - } - }, - InterfaceExtends(node: $FlowFixMe) { - if (node.id.name === 'TurboModule') { - infoMap.isModule = true; - } - }, - }; -} - -function buildSchema( - contents: string, - filename: ?string, - parser: Parser, -): SchemaType { - // Early return for non-Spec JavaScript files - if ( - !contents.includes('codegenNativeComponent') && - !contents.includes('TurboModule') - ) { - return {modules: {}}; - } - - const ast = flowParser.parse(contents, {enums: true}); - const configType = getConfigType(ast, Visitor); - - return buildSchemaFromConfigType( - configType, - filename, - ast, - wrapComponentSchema, - buildComponentSchema, - buildModuleSchema, - parser, - ); -} - -module.exports = { - buildSchema, -}; diff --git a/packages/react-native-codegen/src/parsers/flow/index.js b/packages/react-native-codegen/src/parsers/flow/index.js index 9e2e66a6d99981..e043b7967487dc 100644 --- a/packages/react-native-codegen/src/parsers/flow/index.js +++ b/packages/react-native-codegen/src/parsers/flow/index.js @@ -13,19 +13,32 @@ import type {SchemaType} from '../../CodegenSchema.js'; const fs = require('fs'); -const {buildSchema} = require('./buildSchema'); + +const {buildSchema} = require('../parsers-commons'); +const {Visitor} = require('./Visitor'); const {FlowParser} = require('./parser'); +const {buildComponentSchema} = require('./components'); +const {wrapComponentSchema} = require('./components/schema'); +const {buildModuleSchema} = require('./modules'); const parser = new FlowParser(); function parseModuleFixture(filename: string): SchemaType { const contents = fs.readFileSync(filename, 'utf8'); - return buildSchema(contents, 'path/NativeSampleTurboModule.js', parser); + return parseString(contents, 'path/NativeSampleTurboModule.js'); } function parseString(contents: string, filename: ?string): SchemaType { - return buildSchema(contents, filename, parser); + return buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + parser, + ); } module.exports = { diff --git a/packages/react-native-codegen/src/parsers/flow/parser.js b/packages/react-native-codegen/src/parsers/flow/parser.js index 3b521bed1ffc65..59b2a6f9433fac 100644 --- a/packages/react-native-codegen/src/parsers/flow/parser.js +++ b/packages/react-native-codegen/src/parsers/flow/parser.js @@ -20,7 +20,14 @@ import type { import type {ParserType} from '../errors'; import type {Parser} from '../parser'; -const {buildSchema} = require('./buildSchema'); +// $FlowFixMe[untyped-import] there's no flowtype flow-parser +const flowParser = require('flow-parser'); + +const {buildSchema} = require('../parsers-commons'); +const {Visitor} = require('./Visitor'); +const {buildComponentSchema} = require('./components'); +const {wrapComponentSchema} = require('./components/schema'); +const {buildModuleSchema} = require('./modules'); const fs = require('fs'); @@ -89,7 +96,21 @@ class FlowParser implements Parser { parseFile(filename: string): SchemaType { const contents = fs.readFileSync(filename, 'utf8'); - return buildSchema(contents, filename, this); + return buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + this, + ); + } + + getAst(contents: string): $FlowFixMe { + return flowParser.parse(contents, { + enums: true, + }); } getFunctionTypeAnnotationParameters( diff --git a/packages/react-native-codegen/src/parsers/parser.js b/packages/react-native-codegen/src/parsers/parser.js index 118e0e78c52437..0ccaaf7522015e 100644 --- a/packages/react-native-codegen/src/parsers/parser.js +++ b/packages/react-native-codegen/src/parsers/parser.js @@ -84,6 +84,13 @@ export interface Parser { */ parseFile(filename: string): SchemaType; + /** + * Given the content of a file, it returns an AST. + * @parameter contents: the content of the file. + * @returns: the AST of the file. + */ + getAst(contents: string): $FlowFixMe; + /** * Given a FunctionTypeAnnotation, it returns an array of its parameters. * @parameter functionTypeAnnotation: a FunctionTypeAnnotation diff --git a/packages/react-native-codegen/src/parsers/parserMock.js b/packages/react-native-codegen/src/parsers/parserMock.js index d28f1944df0d2a..f0846f17cb07c0 100644 --- a/packages/react-native-codegen/src/parsers/parserMock.js +++ b/packages/react-native-codegen/src/parsers/parserMock.js @@ -20,6 +20,8 @@ import type { NativeModuleParamTypeAnnotation, } from '../CodegenSchema'; +// $FlowFixMe[untyped-import] there's no flowtype flow-parser +const flowParser = require('flow-parser'); const { UnsupportedObjectPropertyTypeAnnotationParserError, } = require('./errors'); @@ -89,6 +91,12 @@ export class MockedParser implements Parser { }; } + getAst(contents: string): $FlowFixMe { + return flowParser.parse(contents, { + enums: true, + }); + } + getFunctionTypeAnnotationParameters( functionTypeAnnotation: $FlowFixMe, ): $ReadOnlyArray<$FlowFixMe> { diff --git a/packages/react-native-codegen/src/parsers/parsers-commons.js b/packages/react-native-codegen/src/parsers/parsers-commons.js index 587a389ea8cba7..9f0310107b833a 100644 --- a/packages/react-native-codegen/src/parsers/parsers-commons.js +++ b/packages/react-native-codegen/src/parsers/parsers-commons.js @@ -27,6 +27,13 @@ import type { import type {Parser} from './parser'; import type {ParserType} from './errors'; import type {ParserErrorCapturer, TypeDeclarationMap} from './utils'; +import type {ComponentSchemaBuilderConfig} from './flow/components/schema'; + +const { + getConfigType, + extractNativeModuleName, + createParserErrorCapturer, +} = require('./utils'); const { throwIfPropertyValueTypeIsUnsupported, @@ -365,6 +372,98 @@ function buildPropertySchema( }; } +function buildSchemaFromConfigType( + configType: 'module' | 'component' | 'none', + filename: ?string, + ast: $FlowFixMe, + wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType, + buildComponentSchema: (ast: $FlowFixMe) => ComponentSchemaBuilderConfig, + buildModuleSchema: ( + hasteModuleName: string, + ast: $FlowFixMe, + tryParse: ParserErrorCapturer, + parser: Parser, + ) => NativeModuleSchema, + parser: Parser, +): SchemaType { + switch (configType) { + case 'component': { + return wrapComponentSchema(buildComponentSchema(ast)); + } + case 'module': { + if (filename === undefined || filename === null) { + throw new Error('Filepath expected while parasing a module'); + } + const nativeModuleName = extractNativeModuleName(filename); + + const [parsingErrors, tryParse] = createParserErrorCapturer(); + + const schema = tryParse(() => + buildModuleSchema(nativeModuleName, ast, tryParse, parser), + ); + + 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]; + } + + invariant( + schema != null, + 'When there are no parsing errors, the schema should not be null', + ); + + return wrapModuleSchema(schema, nativeModuleName); + } + default: + return {modules: {}}; + } +} + +function buildSchema( + contents: string, + filename: ?string, + wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType, + buildComponentSchema: (ast: $FlowFixMe) => ComponentSchemaBuilderConfig, + buildModuleSchema: ( + hasteModuleName: string, + ast: $FlowFixMe, + tryParse: ParserErrorCapturer, + parser: Parser, + ) => NativeModuleSchema, + Visitor: ({isComponent: boolean, isModule: boolean}) => { + [type: string]: (node: $FlowFixMe) => void, + }, + parser: Parser, +): SchemaType { + // Early return for non-Spec JavaScript files + if ( + !contents.includes('codegenNativeComponent') && + !contents.includes('TurboModule') + ) { + return {modules: {}}; + } + + const ast = parser.getAst(contents); + const configType = getConfigType(ast, Visitor); + + return buildSchemaFromConfigType( + configType, + filename, + ast, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + parser, + ); +} + module.exports = { wrapModuleSchema, unwrapNullable, @@ -375,4 +474,6 @@ module.exports = { translateDefault, translateFunctionTypeAnnotation, buildPropertySchema, + buildSchemaFromConfigType, + buildSchema, }; diff --git a/packages/react-native-codegen/src/parsers/typescript/Visitor.js b/packages/react-native-codegen/src/parsers/typescript/Visitor.js new file mode 100644 index 00000000000000..3bd724c8c73951 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/Visitor.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 + * @format + */ + +'use strict'; + +const {isModuleRegistryCall} = require('../utils'); + +function Visitor(infoMap: {isComponent: boolean, isModule: boolean}): { + [type: string]: (node: $FlowFixMe) => void, +} { + return { + CallExpression(node: $FlowFixMe) { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'codegenNativeComponent' + ) { + infoMap.isComponent = true; + } + + if (isModuleRegistryCall(node)) { + infoMap.isModule = true; + } + }, + + TSInterfaceDeclaration(node: $FlowFixMe) { + if ( + Array.isArray(node.extends) && + node.extends.some( + extension => extension.expression.name === 'TurboModule', + ) + ) { + infoMap.isModule = true; + } + }, + }; +} + +module.exports = { + Visitor, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/buildSchema.js b/packages/react-native-codegen/src/parsers/typescript/buildSchema.js deleted file mode 100644 index c3b56c3e535e77..00000000000000 --- a/packages/react-native-codegen/src/parsers/typescript/buildSchema.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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 - * @format - */ - -'use strict'; - -import type {SchemaType} from '../../CodegenSchema'; -import type {Parser} from '../parser'; - -// $FlowFixMe[untyped-import] Use flow-types for @babel/parser -const babelParser = require('@babel/parser'); -const { - buildSchemaFromConfigType, - getConfigType, - isModuleRegistryCall, -} = require('../utils'); -const {buildComponentSchema} = require('./components'); -const {wrapComponentSchema} = require('./components/schema'); -const {buildModuleSchema} = require('./modules'); - -function Visitor(infoMap: {isComponent: boolean, isModule: boolean}) { - return { - CallExpression(node: $FlowFixMe) { - if ( - node.callee.type === 'Identifier' && - node.callee.name === 'codegenNativeComponent' - ) { - infoMap.isComponent = true; - } - - if (isModuleRegistryCall(node)) { - infoMap.isModule = true; - } - }, - - TSInterfaceDeclaration(node: $FlowFixMe) { - if ( - Array.isArray(node.extends) && - node.extends.some( - extension => extension.expression.name === 'TurboModule', - ) - ) { - infoMap.isModule = true; - } - }, - }; -} - -function buildSchema( - contents: string, - filename: ?string, - parser: Parser, -): SchemaType { - // Early return for non-Spec JavaScript files - if ( - !contents.includes('codegenNativeComponent') && - !contents.includes('TurboModule') - ) { - return {modules: {}}; - } - - const ast = babelParser.parse(contents, { - sourceType: 'module', - plugins: ['typescript'], - }).program; - - const configType = getConfigType(ast, Visitor); - - return buildSchemaFromConfigType( - configType, - filename, - ast, - wrapComponentSchema, - buildComponentSchema, - buildModuleSchema, - parser, - ); -} - -module.exports = { - buildSchema, -}; diff --git a/packages/react-native-codegen/src/parsers/typescript/index.js b/packages/react-native-codegen/src/parsers/typescript/index.js index d790980331c75a..ceeb305c6b08da 100644 --- a/packages/react-native-codegen/src/parsers/typescript/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/index.js @@ -13,19 +13,32 @@ import type {SchemaType} from '../../CodegenSchema.js'; const fs = require('fs'); -const {buildSchema} = require('./buildSchema'); + +const {buildSchema} = require('../parsers-commons'); +const {Visitor} = require('./Visitor'); const {TypeScriptParser} = require('./parser'); +const {buildComponentSchema} = require('./components'); +const {wrapComponentSchema} = require('./components/schema'); +const {buildModuleSchema} = require('./modules'); const parser = new TypeScriptParser(); function parseModuleFixture(filename: string): SchemaType { const contents = fs.readFileSync(filename, 'utf8'); - return buildSchema(contents, 'path/NativeSampleTurboModule.ts', parser); + return parseString(contents, 'path/NativeSampleTurboModule.ts'); } function parseString(contents: string, filename: ?string): SchemaType { - return buildSchema(contents, filename, parser); + return buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + parser, + ); } module.exports = { diff --git a/packages/react-native-codegen/src/parsers/typescript/parser.js b/packages/react-native-codegen/src/parsers/typescript/parser.js index 680b7af7470c49..b95249fbe4be85 100644 --- a/packages/react-native-codegen/src/parsers/typescript/parser.js +++ b/packages/react-native-codegen/src/parsers/typescript/parser.js @@ -20,7 +20,14 @@ import type { import type {ParserType} from '../errors'; import type {Parser} from '../parser'; -const {buildSchema} = require('./buildSchema'); +// $FlowFixMe[untyped-import] Use flow-types for @babel/parser +const babelParser = require('@babel/parser'); + +const {buildSchema} = require('../parsers-commons'); +const {Visitor} = require('./Visitor'); +const {buildComponentSchema} = require('./components'); +const {wrapComponentSchema} = require('./components/schema'); +const {buildModuleSchema} = require('./modules'); const fs = require('fs'); @@ -95,7 +102,22 @@ class TypeScriptParser implements Parser { parseFile(filename: string): SchemaType { const contents = fs.readFileSync(filename, 'utf8'); - return buildSchema(contents, filename, this); + return buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + this, + ); + } + + getAst(contents: string): $FlowFixMe { + return babelParser.parse(contents, { + sourceType: 'module', + plugins: ['typescript'], + }).program; } getFunctionTypeAnnotationParameters( diff --git a/packages/react-native-codegen/src/parsers/utils.js b/packages/react-native-codegen/src/parsers/utils.js index 1c4362bbf04ea5..f3ad6483e06eaa 100644 --- a/packages/react-native-codegen/src/parsers/utils.js +++ b/packages/react-native-codegen/src/parsers/utils.js @@ -10,15 +10,9 @@ 'use strict'; -import type {ComponentSchemaBuilderConfig} from './flow/components/schema'; -import type {NativeModuleSchema, SchemaType} from '../CodegenSchema'; -import type {Parser} from './parser'; - const {ParserError} = require('./errors'); -const {wrapModuleSchema} = require('./parsers-commons'); const path = require('path'); -const invariant = require('invariant'); export type TypeDeclarationMap = {[declarationName: string]: $FlowFixMe}; @@ -125,60 +119,6 @@ function visit( } } -function buildSchemaFromConfigType( - configType: 'module' | 'component' | 'none', - filename: ?string, - ast: $FlowFixMe, - wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType, - buildComponentSchema: (ast: $FlowFixMe) => ComponentSchemaBuilderConfig, - buildModuleSchema: ( - hasteModuleName: string, - ast: $FlowFixMe, - tryParse: ParserErrorCapturer, - parser: Parser, - ) => NativeModuleSchema, - parser: Parser, -): SchemaType { - switch (configType) { - case 'component': { - return wrapComponentSchema(buildComponentSchema(ast)); - } - case 'module': { - if (filename === undefined || filename === null) { - throw new Error('Filepath expected while parasing a module'); - } - const nativeModuleName = extractNativeModuleName(filename); - - const [parsingErrors, tryParse] = createParserErrorCapturer(); - - const schema = tryParse(() => - buildModuleSchema(nativeModuleName, ast, tryParse, parser), - ); - - 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]; - } - - invariant( - schema != null, - 'When there are no parsing errors, the schema should not be null', - ); - - return wrapModuleSchema(schema, nativeModuleName); - } - default: - return {modules: {}}; - } -} - function getConfigType( // TODO(T71778680): Flow-type this node. ast: $FlowFixMe, @@ -254,6 +194,5 @@ module.exports = { createParserErrorCapturer, verifyPlatforms, visit, - buildSchemaFromConfigType, isModuleRegistryCall, };