diff --git a/package.json b/package.json index 95d5ff903468f..e865921cf0da7 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "gzip-size": "^5.1.1", "hermes-eslint": "^0.20.1", "hermes-parser": "^0.20.1", + "hermes-transform": "^0.20.1", "jest": "^29.4.2", "jest-cli": "^29.4.2", "jest-diff": "^29.4.2", @@ -132,6 +133,7 @@ "download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)", "download-build-in-codesandbox-ci": "cd scripts/release && yarn install && cd ../../ && yarn download-build-for-head || yarn build --type=node react/index react-dom/index react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime", "check-release-dependencies": "node ./scripts/release/check-release-dependencies", + "codemod": "flow-node scripts/codemod/index.js", "generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js", "flags": "node ./scripts/flags/flags.js" }, diff --git a/scripts/codemod/index.js b/scripts/codemod/index.js new file mode 100644 index 0000000000000..acecde9fc1134 --- /dev/null +++ b/scripts/codemod/index.js @@ -0,0 +1,262 @@ +/** + * 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 + */ + +import type { + ESNode, + IfStatement, + Statement, + ImportDeclaration, + LogicalExpression, + CallExpression, + ConditionalExpression, + UnaryExpression, + Identifier, + MemberExpression, +} from 'hermes-estree'; +import type {TransformContext} from 'hermes-transform'; + +const {transform, t} = require('hermes-transform'); +const {SimpleTraverser} = require('hermes-parser'); +const Glob = require('glob'); +const {readFileSync, writeFileSync} = require('fs'); +const Prettier = require('prettier'); + +/* eslint-disable no-for-of-loops/no-for-of-loops */ + +function createReplaceFlagWithValue(flagName: string, flagValue: boolean) { + return function replaceFlagWithValue(context: TransformContext) { + return { + ImportDeclaration(node: ImportDeclaration) { + context.skipTraversal(); + }, + Identifier(node: Identifier) { + if (node.parent.type === 'VariableDeclarator') { + return; + } + if (node.type === 'Identifier' && node.name === flagName) { + context.replaceNode(node, t.BooleanLiteral({value: flagValue})); + } + }, + MemberExpression(node: MemberExpression) { + if ( + node.object.type === 'Identifier' && + node.object.name === 'flags' && + node.property.type === 'Identifier' && + node.property.name === flagName + ) { + context.replaceNode(node, t.BooleanLiteral({value: flagValue})); + context.skipTraversal(); + } + }, + }; + }; +} + +function simplifyNotBoolean(context: TransformContext) { + return { + UnaryExpression(node: UnaryExpression) { + if (node.operator === '!' && node.argument.type === 'Literal') { + context.replaceNode( + node, + t.BooleanLiteral({value: !node.argument.value}) + ); + } + }, + }; +} + +function simplifyGate(context: TransformContext) { + return { + CallExpression(node: CallExpression) { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'gate' && + node.arguments.length === 1 && + node.arguments[0].type === 'ArrowFunctionExpression' && + node.arguments[0].body.type === 'Literal' + ) { + context.replaceNode(node, node.arguments[0].body); + } + }, + }; +} + +function simplifyLogicalExpression(context: TransformContext) { + return { + LogicalExpression(node: LogicalExpression) { + if ( + node.operator === '&&' && + node.left.type === 'Literal' && + node.left.value === true + ) { + context.replaceNode(node, node.right); + } + if ( + node.operator === '&&' && + node.left.type === 'Literal' && + node.left.value === false + ) { + context.replaceNode(node, node.left); + } + if ( + node.operator === '&&' && + node.right.type === 'Literal' && + node.right.value === true + ) { + context.replaceNode(node, node.left); + } + if ( + node.operator === '||' && + node.left.type === 'Literal' && + node.left.value === false + ) { + context.replaceNode(node, node.right); + } + if ( + node.operator === '||' && + node.right.type === 'Literal' && + node.right.value === false + ) { + context.replaceNode(node, node.left); + } + }, + }; +} + +function simplifyTernary(context: TransformContext) { + return { + ConditionalExpression(node: ConditionalExpression) { + if (node.test.type === 'Literal' && node.test.value === true) { + context.replaceNode(node, node.consequent); + } + if (node.test.type === 'Literal' && node.test.value === false) { + context.replaceNode(node, node.alternate); + } + }, + }; +} + +function simplifyCondition(context: TransformContext) { + let lastParent: ?ESNode = null; + return { + IfStatement(node: IfStatement) { + if (node.parent === lastParent) { + // a bug in hermes-transform prevents multiple replaceStatementWithMany + // with the same parent + return; + } + if (node.test.type === 'Literal' && node.test.value === true) { + lastParent = node.parent; + context.replaceStatementWithMany( + node, + unwrapBlockStatment(node.consequent) + ); + } + if (node.test.type === 'Literal' && node.test.value === false) { + lastParent = node.parent; + if (node.alternate == null) { + context.removeStatement(node); + } else { + context.replaceStatementWithMany( + node, + unwrapBlockStatment(node.alternate) + ); + } + } + }, + }; +} + +function unwrapBlockStatment(statement: Statement): $ReadOnlyArray { + return statement.type === 'BlockStatement' ? statement.body : [statement]; +} + +async function transformFile( + filename: string, + flagName: string, + flagValue: boolean +) { + const originalCode = readFileSync(filename, 'utf8'); + if (!originalCode.includes(flagName)) { + return false; + } + const prettierConfig = await Prettier.resolveConfig(filename); + let transformedCode = originalCode; + transformedCode = transformedCode.replaceAll(`// @gate ${flagName}\n`, ''); + transformedCode = transformedCode.replaceAll( + `// @gate ${flagName} && `, + '// @gate ' + ); + transformedCode = transformedCode.replaceAll( + `// @gate ${flagName} || `, + '// XXX REMOVE XXX' + ); + for (const createVisitors of [ + createReplaceFlagWithValue(flagName, flagValue), + simplifyNotBoolean, + simplifyGate, + simplifyLogicalExpression, + simplifyLogicalExpression, + simplifyTernary, + simplifyCondition, + simplifyCondition, + simplifyCondition, + ]) { + transformedCode = await transform( + transformedCode, + createVisitors, + prettierConfig + ); + } + if (originalCode !== transformedCode) { + writeFileSync(filename, transformedCode, 'utf8'); + return true; + } + return false; +} + +async function main(args: $ReadOnlyArray) { + if (args.length < 1) { + console.error('Usage: yarn codemod '); + process.exit(1); + } + const {FLAG_NAME: flagName, FLAG_VALUE: flagValue} = process.env; + + if (flagName == null || flagValue == null) { + console.error('Please set FLAG_NAME and FLAG_VALUE environment variables'); + process.exit(1); + return; + } + + let flagValueBoolean = flagValue !== 'false'; + + const files = new Set(); + for (const arg of args) { + for (const file of Glob.sync(arg)) { + files.add(file); + } + } + let updatedCount = 0; + for (const file of files) { + try { + const updated = await transformFile(file, flagName, flagValueBoolean); + if (updated) { + updatedCount++; + console.log(`updated ${file}`); + } + } catch (err) { + console.log(`Error transforming ${file}`, err); + } + } + console.log(`${files.size} processed, ${updatedCount} updated`); +} + +main(process.argv.slice(2)).catch(err => { + console.error('Error while transforming:', err); +}); diff --git a/scripts/shared/pathsByLanguageVersion.js b/scripts/shared/pathsByLanguageVersion.js index 10c49821af5e9..07909da7c4c5f 100644 --- a/scripts/shared/pathsByLanguageVersion.js +++ b/scripts/shared/pathsByLanguageVersion.js @@ -20,6 +20,7 @@ const esNextPaths = [ 'packages/react-interactions/**/*.js', 'packages/shared/**/*.js', // Shims and Flow environment + 'scripts/codemod/*.js', 'scripts/flow/*.js', 'scripts/rollup/shims/**/*.js', ]; diff --git a/yarn.lock b/yarn.lock index 71c9174c1c238..e1d51adfacc2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6997,6 +6997,7 @@ eslint-plugin-no-unsanitized@3.1.2: "eslint-plugin-react-internal@link:./scripts/eslint-rules": version "0.0.0" + uid "" eslint-plugin-react@^6.7.1: version "6.10.3" @@ -7961,6 +7962,11 @@ flow-bin@^0.231.0: resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.231.0.tgz#b9fde021e60ff6e40e4f0212b6c426fdde798ac7" integrity sha512-nB5/rH+bUCIAXWagwIEYM0q/2M4mgkD5q16/eZyNx7sfqsmMkZLg0KmPH5hgvzq6PkVoeCoM8sjhPyzLW2wT+w== +flow-enums-runtime@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787" + integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw== + flow-remove-types@^2.231.0: version "2.231.0" resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.231.0.tgz#64f9a99fd13b3614e28e75b1c55cb2a1f479571e" @@ -8694,7 +8700,7 @@ has@^1.0.1, has@^1.0.3: dependencies: function-bind "^1.1.1" -hermes-eslint@^0.20.1: +hermes-eslint@0.20.1, hermes-eslint@^0.20.1: version "0.20.1" resolved "https://registry.yarnpkg.com/hermes-eslint/-/hermes-eslint-0.20.1.tgz#4a731b47a6d169bbd4514aaa74bd812fd90f3554" integrity sha512-EhdvFV6RkPIJvbqN8oqFZO1oF4NlPWMjhMjCWkUJX1YL1MZMfkF7nSdx6RKTq6xK17yo+Bgv88L21xuH9GtRpw== @@ -8715,6 +8721,18 @@ hermes-parser@0.20.1, hermes-parser@^0.20.1: dependencies: hermes-estree "0.20.1" +hermes-transform@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/hermes-transform/-/hermes-transform-0.20.1.tgz#910bd0ea7cc58eca4c0acadb413d779b70dd3803" + integrity sha512-gpetyzAQvuLXVWIk8/I2A/ei/5+o8eT+QuSGd8FcWpXoYxVkYjVKLVNE9xKLsEkk2wQ1tXODY5OeOZoaz9jL7Q== + dependencies: + "@babel/code-frame" "^7.16.0" + esquery "^1.4.0" + flow-enums-runtime "^0.0.6" + hermes-eslint "0.20.1" + hermes-estree "0.20.1" + hermes-parser "0.20.1" + homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -12991,7 +13009,7 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.8: object-assign "^4.1.1" scheduler "^0.20.2" -react-is@^16.8.1, react-is@^17.0.1, "react-is@npm:react-is": +react-is@^16.8.1, react-is@^17.0.1, react-is@^18.0.0, react-is@^18.2.0, "react-is@npm:react-is": version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -14458,7 +14476,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14493,6 +14511,15 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -14553,7 +14580,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14588,6 +14615,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -16023,7 +16057,7 @@ workerize-loader@^2.0.2: dependencies: loader-utils "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -16041,6 +16075,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"