From f2970e70b26f4ea126300025605ebb9d4772a2a1 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Tue, 5 Nov 2024 17:40:38 -0800 Subject: [PATCH] Skip plugin for non-Flow JS code (#1556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/hermes/pull/1556 This is a mitigation for https://github.com/facebook/hermes/issues/1549. Updates `babel-plugin-syntax-hermes-parser` to include a new `parseLangTypes` option, which we will configure in React Native's Babel preset to abort when the file contents do not include `flow`. **Context: React Native** Originally changed in https://github.com/facebook/react-native/pull/46696, as our internal Flow support had diverged from `babel/plugin-syntax-flow` (https://github.com/facebook/react-native/issues/46601). We effectively have three flavours of JavaScript in support: - Flow@latest for the `react-native` package, shipped as source — uses `hermes-parser`. - TypeScript for product code (community template, Expo) — uses `babel/plugin-syntax-typescript`. - Plain JavaScript/JSX in product code, *which may be extended with additional user Babel plugins and needs lenient parsing* — uses base `babel/parser` (**this change**). I'd love to simplify this 😅. Reviewed By: pieterv Differential Revision: D65272155 fbshipit-source-id: b3be10410962ffffd4e0d483d10fe88d865c0783 --- .../babel-plugin-syntax-hermes-parser-test.js | 65 ++++++++++++++++--- .../src/index.js | 28 +++++++- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/__tests__/babel-plugin-syntax-hermes-parser-test.js b/tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/__tests__/babel-plugin-syntax-hermes-parser-test.js index a03108c2b20..2b503f520ae 100644 --- a/tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/__tests__/babel-plugin-syntax-hermes-parser-test.js +++ b/tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/__tests__/babel-plugin-syntax-hermes-parser-test.js @@ -16,22 +16,44 @@ import {transformSync} from '@babel/core'; import hermesParserPlugin from '../src'; import * as HermesParser from 'hermes-parser'; -const MODULE_PREAMBLE = '"use strict";\n\n'; +const MODULE_PREAMBLE = '// @flow\n\n"use strict";\n\n'; +const NON_FLOW_MODULE_PREAMBLE = '"use strict";\n\n'; describe('babel-plugin-syntax-hermes-parser', () => { - test('test basic parsing', () => { - const parseSpy = jest.spyOn(HermesParser, 'parse'); - const code = MODULE_PREAMBLE + 'const a = 1;'; + const parseSpy = jest.spyOn(HermesParser, 'parse'); + + afterEach(() => { + parseSpy.mockClear(); + }); + + test('should parse Flow files', () => { + const code = MODULE_PREAMBLE + 'const a: number = 1;'; + const output = transformSync(code, { + plugins: [hermesParserPlugin], + }); + expect(output.code).toMatchInlineSnapshot(` + ""use strict"; + + const a = 1;" + `); + expect(parseSpy).toBeCalledTimes(1); + }); + + test('should parse files without @flow annotation', () => { + const code = NON_FLOW_MODULE_PREAMBLE + 'const a: number = 1;'; const output = transformSync(code, { plugins: [hermesParserPlugin], }); - expect(output.code).toBe(code); + expect(output.code).toMatchInlineSnapshot(` + ""use strict"; + + const a = 1;" + `); expect(parseSpy).toBeCalledTimes(1); }); - test('test skip TS', () => { - const parseSpy = jest.spyOn(HermesParser, 'parse'); - const code = MODULE_PREAMBLE + 'const a: string = 1;'; + test('should skip TypeScript files', () => { + const code = NON_FLOW_MODULE_PREAMBLE + 'const a: number = 1;'; const output = transformSync(code, { plugins: [hermesParserPlugin], filename: 'foo.ts', @@ -44,8 +66,7 @@ describe('babel-plugin-syntax-hermes-parser', () => { expect(parseSpy).toBeCalledTimes(0); }); - test('test component syntax parsing', () => { - const parseSpy = jest.spyOn(HermesParser, 'parse'); + test('should parse component syntax when enabled', () => { const code = MODULE_PREAMBLE + 'component Foo() {}'; const output = transformSync(code, { plugins: [hermesParserPlugin], @@ -60,4 +81,28 @@ describe('babel-plugin-syntax-hermes-parser', () => { `); expect(parseSpy).toBeCalledTimes(1); }); + + describe("with parseLangTypes = 'flow'", () => { + test('should parse Flow files', () => { + const code = MODULE_PREAMBLE + 'const a: number = 1;'; + const output = transformSync(code, { + plugins: [hermesParserPlugin], + }); + expect(output.code).toMatchInlineSnapshot(` + ""use strict"; + + const a = 1;" + `); + expect(parseSpy).toBeCalledTimes(1); + }); + + test('should skip files without @flow annotation ', () => { + const code = NON_FLOW_MODULE_PREAMBLE + 'class Foo {}'; + const output = transformSync(code, { + plugins: [[hermesParserPlugin, {parseLangTypes: 'flow'}]], + }); + expect(output.code).toBe(code); + expect(parseSpy).toBeCalledTimes(0); + }); + }); }); diff --git a/tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/src/index.js b/tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/src/index.js index a04237fb031..83994fc29b1 100644 --- a/tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/src/index.js +++ b/tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/src/index.js @@ -14,12 +14,28 @@ import type {ParserOptions} from 'hermes-parser'; import * as HermesParser from 'hermes-parser'; +type Options = { + /** + * When set to 'flow', will check files for a `@flow` annotation to apply + * this plugin, otherwise falling back to Babel's parser. + * + * This is independent of `parserOpts.flow`, which may customise 'detect' + * behaviour within hermes-parser (downstream from this plugin). + * + * Defaults to 'all'. + */ + parseLangTypes?: 'flow' | 'all', +}; + export default function BabelPluginSyntaxHermesParser( // $FlowExpectedError[unclear-type] We don't have types for this. api: any, + options: Options, ): $ReadOnly<{...}> { api.assertVersion('^7.0.0 || ^8.0.0-alpha.6'); + const {parseLangTypes = 'all'} = options; + let curParserOpts: ParserOptions = {}; let curFilename: ?string = null; @@ -42,14 +58,20 @@ export default function BabelPluginSyntaxHermesParser( ) { return; } - const opts: ParserOptions = {}; + + const parserOpts: ParserOptions = {}; for (const [key, value] of Object.entries(curParserOpts)) { if (HermesParser.ParserOptionsKeys.has(key)) { // $FlowExpectedError[incompatible-type] - opts[key] = value; + parserOpts[key] = value; } } - return HermesParser.parse(code, {...opts, babel: true}); + + if (parseLangTypes === 'flow' && !/@flow/.test(code)) { + return; + } + + return HermesParser.parse(code, {...parserOpts, babel: true}); }, pre() {