diff --git a/JSTQL/src/language/jstql-validator.ts b/JSTQL/src/language/jstql-validator.ts index f5e5bec..a048b09 100644 --- a/JSTQL/src/language/jstql-validator.ts +++ b/JSTQL/src/language/jstql-validator.ts @@ -75,7 +75,7 @@ function validateWildcardAplTo(wildcards: string[]): ValidationResultAplTo { } if (!types) { - errors.push("No types given for value"); + errors.push("No types given for wildcard " + identifier); } } return { env, errors }; diff --git a/dsl_files/do.jstql b/dsl_files/do.jstql index 360105b..06fbad0 100644 --- a/dsl_files/do.jstql +++ b/dsl_files/do.jstql @@ -1,33 +1,33 @@ proposal DoExpression{ case arrowFunction{ applicable to { - "let <> = () => { + "() => { <> return <>; } " } transform to { - "let <> = do { + "(do { <> <> - }" + })" } } case immediatelyInvokedUnnamedFunction { applicable to { - "let <> = function(){ + "(function(){ <> return <>; - }();" + })();" } transform to { - "let <> = do { + "(do { <> <> - }" + })" } } } \ No newline at end of file diff --git a/output_files/output_do.js b/output_files/output_do.js index 15e630e..7551a1c 100644 --- a/output_files/output_do.js +++ b/output_files/output_do.js @@ -2,11 +2,11 @@ let aaaa = do { let g = 100; let ff = 10; let ggg = a(b); - 100; + 100 }; -let bbaaa = do { +var bbaaa = do { let lllll = 1 + 1; 100 + 100; const aaaaa = aaaa(bb); - lllll; + lllll }; \ No newline at end of file diff --git a/output_files/test_2.js b/output_files/test_2.js new file mode 100644 index 0000000..d0c2239 --- /dev/null +++ b/output_files/test_2.js @@ -0,0 +1,94 @@ +function parse() { + const input = ("input" |> document.getElementById(%)).value; + const data = 32 |> input.slice(%); + const compressedData = data |> decode_base64(%); + const uncompressed = compressedData |> pako.inflate(%, { + to: "string" + }); + const json = uncompressed |> JSON.parse(%); + json |> console.log(%); + json |> convertToDesktop(%); +} +function convertToDesktop(json) { + const newValues = { + crb: false, + newClanRaidClassId: 0, + newClanRaidClassLevel: 0, + pendingImmortalSouls: 0, + pendingRaidRubies: 0, + immortalSouls: 0, + lastPurchaseTime: 0, + lastRaidAttemptTimestamp: 0, + lastRaidRewardCheckTimestamp: 0, + shouldShowHZERoster: false, + lastBonusRewardCheckTimestamp: 0 + }; + const mappedValues = { + rubies: json.rubies / 10 |> Math.round(%) + }; + const pcSpecificValues = { + readPatchNumber: "1.0e12", + saveOrigin: "pc" + }; + const hash = "7a990d405d2c6fb93aa8fbb0ec1a3b23"; + const newData = { + ...newValues, + ...json, + ...mappedValues, + ...pcSpecificValues + }; + const compressed = newData |> JSON.stringify(%) |> pako.deflate(%, { + to: "string" + }); + const base64 = compressed |> btoa(%); + const finalSaveString = hash + base64; + ("output_output" |> document.getElementById(%)).innerText = finalSaveString; + showOutput(); +} +function showOutput() { + ("outputs" |> document.getElementById(%)).style.visibility = "visible"; +} +function copyOutput() { + const output = "output_output" |> document.getElementById(%); + output.disabled = false; + output.focus(); + output.select(); + "copy" |> document.execCommand(%); + output.disabled = true; + const successElement = "copy_success_msg" |> document.getElementById(%); + successElement.style.visibility = "visible"; + (() => successElement.style.visibility = "hidden") |> setTimeout(%, 4000); +} +function decode_base64(s) { + let e = {}, + i, + k, + v = [], + r = "", + w = String.fromCharCode; + let n = [[65, 91], [97, 123], [48, 58], [43, 44], [47, 48]]; + for (z in n) { + for (i = n[z][0]; i < n[z][1]; i++) { + i |> w(%) |> v.push(%); + } + } + for (i = 0; i < 64; i++) { + e[v[i]] = i; + } + for (i = 0; i < s.length; i += 72) { + let b = 0, + c, + x, + l = 0, + o = i |> s.substring(%, i + 72); + for (x = 0; x < o.length; x++) { + c = e[x |> o.charAt(%)]; + b = (b << 6) + c; + l += 6; + while (l >= 8) { + r += (b >>> (l -= 8)) % 256 |> w(%); + } + } + } + return r; +} \ No newline at end of file diff --git a/output_testing/0.eslintrc.js b/output_testing/0.eslintrc.js new file mode 100644 index 0000000..f750642 --- /dev/null +++ b/output_testing/0.eslintrc.js @@ -0,0 +1,500 @@ +'use strict'; + +const { + es5Paths, + esNextPaths +} = './scripts/shared/pathsByLanguageVersion' |> require(%); +const restrictedGlobals = 'confusing-browser-globals' |> require(%); +const OFF = 0; +const WARNING = 1; +const ERROR = 2; +module.exports = { + extends: ['prettier'], + // Stop ESLint from looking for a configuration file in parent folders + root: true, + plugins: ['babel', 'ft-flow', 'jest', 'no-for-of-loops', 'no-function-declare-after-return', 'react', 'react-internal'], + parser: 'hermes-eslint', + parserOptions: { + ecmaVersion: 9, + sourceType: 'script' + }, + // We're stricter than the default config, mostly. We'll override a few rules + // and then enable some React specific ones. + rules: { + 'ft-flow/array-style-complex-type': [OFF, 'verbose'], + 'ft-flow/array-style-simple-type': [OFF, 'verbose'], + // TODO should be WARNING + 'ft-flow/boolean-style': ERROR, + 'ft-flow/no-dupe-keys': ERROR, + 'ft-flow/no-primitive-constructor-types': ERROR, + 'ft-flow/no-types-missing-file-annotation': OFF, + // TODO should be ERROR + 'ft-flow/no-unused-expressions': ERROR, + // 'ft-flow/no-weak-types': WARNING, + // 'ft-flow/require-valid-file-annotation': ERROR, + + 'no-cond-assign': OFF, + 'no-constant-condition': OFF, + 'no-control-regex': OFF, + 'no-debugger': ERROR, + 'no-dupe-args': ERROR, + 'no-dupe-keys': ERROR, + 'no-duplicate-case': WARNING, + 'no-empty-character-class': WARNING, + 'no-empty': OFF, + 'no-ex-assign': WARNING, + 'no-extra-boolean-cast': WARNING, + 'no-func-assign': ERROR, + 'no-invalid-regexp': WARNING, + 'no-irregular-whitespace': WARNING, + 'no-negated-in-lhs': ERROR, + 'no-obj-calls': ERROR, + 'no-regex-spaces': WARNING, + 'no-sparse-arrays': ERROR, + 'no-unreachable': ERROR, + 'use-isnan': ERROR, + 'valid-jsdoc': OFF, + 'block-scoped-var': OFF, + complexity: OFF, + 'default-case': OFF, + 'guard-for-in': OFF, + 'no-alert': OFF, + 'no-caller': ERROR, + 'no-case-declarations': OFF, + 'no-div-regex': OFF, + 'no-else-return': OFF, + 'no-empty-pattern': WARNING, + 'no-eq-null': OFF, + 'no-eval': ERROR, + 'no-extend-native': WARNING, + 'no-extra-bind': WARNING, + 'no-fallthrough': WARNING, + 'no-implicit-coercion': OFF, + 'no-implied-eval': ERROR, + 'no-invalid-this': OFF, + 'no-iterator': OFF, + 'no-labels': [ERROR, { + allowLoop: true, + allowSwitch: true + }], + 'no-lone-blocks': WARNING, + 'no-loop-func': OFF, + 'no-magic-numbers': OFF, + 'no-multi-str': ERROR, + 'no-native-reassign': [ERROR, { + exceptions: ['Map', 'Set'] + }], + 'no-new-func': ERROR, + 'no-new': WARNING, + 'no-new-wrappers': WARNING, + 'no-octal-escape': WARNING, + 'no-octal': WARNING, + 'no-param-reassign': OFF, + 'no-process-env': OFF, + 'no-proto': ERROR, + 'no-redeclare': OFF, + // TODO should be WARNING? + 'no-return-assign': OFF, + 'no-script-url': ERROR, + 'no-self-compare': WARNING, + 'no-sequences': WARNING, + 'no-throw-literal': ERROR, + 'no-useless-call': WARNING, + 'no-void': OFF, + 'no-warning-comments': OFF, + 'no-with': OFF, + radix: WARNING, + 'vars-on-top': OFF, + yoda: OFF, + 'init-declarations': OFF, + 'no-catch-shadow': ERROR, + 'no-delete-var': ERROR, + 'no-label-var': WARNING, + 'no-shadow-restricted-names': WARNING, + 'no-undef-init': OFF, + 'no-undef': ERROR, + 'no-undefined': OFF, + 'callback-return': OFF, + 'global-require': OFF, + 'handle-callback-err': OFF, + 'no-mixed-requires': OFF, + 'no-new-require': OFF, + 'no-path-concat': OFF, + 'no-process-exit': OFF, + 'no-restricted-modules': OFF, + 'no-sync': OFF, + camelcase: [OFF, { + properties: 'always' + }], + 'consistent-this': [OFF, 'self'], + 'func-names': OFF, + 'func-style': [OFF, 'declaration'], + 'id-length': OFF, + 'id-match': OFF, + 'max-depth': OFF, + 'max-nested-callbacks': OFF, + 'max-params': OFF, + 'max-statements': OFF, + 'new-cap': OFF, + 'newline-after-var': OFF, + 'no-array-constructor': ERROR, + 'no-continue': OFF, + 'no-inline-comments': OFF, + 'no-lonely-if': OFF, + 'no-negated-condition': OFF, + 'no-nested-ternary': OFF, + 'no-new-object': WARNING, + 'no-plusplus': OFF, + 'no-ternary': OFF, + 'no-underscore-dangle': OFF, + 'no-unneeded-ternary': WARNING, + 'one-var': [WARNING, { + initialized: 'never' + }], + 'operator-assignment': [WARNING, 'always'], + 'require-jsdoc': OFF, + 'sort-vars': OFF, + 'spaced-comment': [OFF, 'always', { + exceptions: ['jshint', 'jslint', 'eslint', 'global'] + }], + 'constructor-super': ERROR, + 'no-class-assign': WARNING, + 'no-const-assign': ERROR, + 'no-dupe-class-members': ERROR, + 'no-this-before-super': ERROR, + 'object-shorthand': OFF, + 'prefer-const': OFF, + 'prefer-spread': OFF, + 'prefer-reflect': OFF, + 'prefer-template': OFF, + 'require-yield': OFF, + 'babel/generator-star-spacing': OFF, + 'babel/new-cap': OFF, + 'babel/array-bracket-spacing': OFF, + 'babel/object-curly-spacing': OFF, + 'babel/object-shorthand': OFF, + 'babel/arrow-parens': OFF, + 'babel/no-await-in-loop': OFF, + 'babel/flow-object-type': OFF, + 'react/display-name': OFF, + 'react/forbid-prop-types': OFF, + 'react/jsx-closing-bracket-location': OFF, + 'react/jsx-curly-spacing': OFF, + 'react/jsx-equals-spacing': WARNING, + 'react/jsx-filename-extension': OFF, + 'react/jsx-first-prop-new-line': OFF, + 'react/jsx-handler-names': OFF, + 'react/jsx-indent': OFF, + 'react/jsx-indent-props': OFF, + 'react/jsx-key': OFF, + 'react/jsx-max-props-per-line': OFF, + 'react/jsx-no-bind': OFF, + 'react/jsx-no-duplicate-props': ERROR, + 'react/jsx-no-literals': OFF, + 'react/jsx-no-target-blank': OFF, + 'react/jsx-pascal-case': OFF, + 'react/jsx-sort-props': OFF, + 'react/jsx-uses-vars': ERROR, + 'react/no-comment-textnodes': OFF, + 'react/no-danger': OFF, + 'react/no-deprecated': OFF, + 'react/no-did-mount-set-state': OFF, + 'react/no-did-update-set-state': OFF, + 'react/no-direct-mutation-state': OFF, + 'react/no-multi-comp': OFF, + 'react/no-render-return-value': OFF, + 'react/no-set-state': OFF, + 'react/no-string-refs': OFF, + 'react/no-unknown-property': OFF, + 'react/prefer-es6-class': OFF, + 'react/prefer-stateless-function': OFF, + 'react/prop-types': OFF, + 'react/require-extension': OFF, + 'react/require-optimization': OFF, + 'react/require-render-return': OFF, + 'react/sort-comp': OFF, + 'react/sort-prop-types': OFF, + 'accessor-pairs': OFF, + 'brace-style': [ERROR, '1tbs'], + 'consistent-return': OFF, + 'dot-location': [ERROR, 'property'], + // We use console['error']() as a signal to not transform it: + 'dot-notation': [ERROR, { + allowPattern: '^(error|warn)$' + }], + 'eol-last': ERROR, + eqeqeq: [ERROR, 'allow-null'], + indent: OFF, + 'jsx-quotes': [ERROR, 'prefer-double'], + 'keyword-spacing': [ERROR, { + after: true, + before: true + }], + 'no-bitwise': OFF, + 'no-console': OFF, + 'no-inner-declarations': [ERROR, 'functions'], + 'no-multi-spaces': ERROR, + 'no-restricted-globals': restrictedGlobals |> [ERROR].concat(%), + 'no-restricted-syntax': [ERROR, 'WithStatement', { + selector: 'MemberExpression[property.name=/^(?:substring|substr)$/]', + message: 'Prefer string.slice() over .substring() and .substr().' + }], + 'no-shadow': ERROR, + 'no-unused-vars': [ERROR, { + args: 'none' + }], + 'no-use-before-define': OFF, + 'no-useless-concat': OFF, + quotes: [ERROR, 'single', { + avoidEscape: true, + allowTemplateLiterals: true + }], + 'space-before-blocks': ERROR, + 'space-before-function-paren': OFF, + 'valid-typeof': [ERROR, { + requireStringLiterals: true + }], + // Flow fails with non-string literal keys + 'no-useless-computed-key': OFF, + // We apply these settings to files that should run on Node. + // They can't use JSX or ES6 modules, and must be in strict mode. + // They can, however, use other ES6 features. + // (Note these rules are overridden later for source files.) + 'no-var': ERROR, + strict: ERROR, + // Enforced by Prettier + // TODO: Prettier doesn't handle long strings or long comments. Not a big + // deal. But I turned it off because loading the plugin causes some obscure + // syntax error and it didn't seem worth investigating. + 'max-len': OFF, + // React & JSX + // Our transforms set this automatically + 'react/jsx-boolean-value': [ERROR, 'always'], + 'react/jsx-no-undef': ERROR, + // We don't care to do this + 'react/jsx-sort-prop-types': OFF, + 'react/jsx-space-before-closing': ERROR, + 'react/jsx-uses-react': ERROR, + 'react/no-is-mounted': OFF, + // This isn't useful in our test code + 'react/react-in-jsx-scope': ERROR, + 'react/self-closing-comp': ERROR, + // We don't care to do this + 'react/jsx-wrap-multilines': [ERROR, { + declaration: false, + assignment: false + }], + // Prevent for...of loops because they require a Symbol polyfill. + // You can disable this rule for code that isn't shipped (e.g. build scripts and tests). + 'no-for-of-loops/no-for-of-loops': ERROR, + // Prevent function declarations after return statements + 'no-function-declare-after-return/no-function-declare-after-return': ERROR, + // CUSTOM RULES + // the second argument of warning/invariant should be a literal string + 'react-internal/no-primitive-constructors': ERROR, + 'react-internal/safe-string-coercion': [ERROR, { + isProductionUserAppCode: true + }], + 'react-internal/no-to-warn-dev-within-to-throw': ERROR, + 'react-internal/warning-args': ERROR, + 'react-internal/no-production-logging': ERROR + }, + overrides: [{ + // By default, anything error message that appears the packages directory + // must have a corresponding error code. The exceptions are defined + // in the next override entry. + files: ['packages/**/*.js'], + rules: { + 'react-internal/prod-error-codes': ERROR + } + }, { + // These are files where it's OK to have unminified error messages. These + // are environments where bundle size isn't a concern, like tests + // or Node. + files: ['packages/react-dom/src/test-utils/**/*.js', 'packages/react-devtools-shared/**/*.js', 'packages/react-noop-renderer/**/*.js', 'packages/react-refresh/**/*.js', 'packages/react-server-dom-esm/**/*.js', 'packages/react-server-dom-webpack/**/*.js', 'packages/react-server-dom-turbopack/**/*.js', 'packages/react-server-dom-fb/**/*.js', 'packages/react-test-renderer/**/*.js', 'packages/react-debug-tools/**/*.js', 'packages/react-devtools-extensions/**/*.js', 'packages/react-devtools-timeline/**/*.js', 'packages/react-native-renderer/**/*.js', 'packages/eslint-plugin-react-hooks/**/*.js', 'packages/jest-react/**/*.js', 'packages/internal-test-utils/**/*.js', 'packages/**/__tests__/*.js', 'packages/**/npm/*.js'], + rules: { + 'react-internal/prod-error-codes': OFF + } + }, { + // We apply these settings to files that we ship through npm. + // They must be ES5. + files: es5Paths, + parser: 'espree', + parserOptions: { + ecmaVersion: 5, + sourceType: 'script' + }, + rules: { + 'no-var': OFF, + strict: ERROR + } + }, { + // We apply these settings to the source files that get compiled. + // They can use all features including JSX (but shouldn't use `var`). + files: esNextPaths, + parser: 'hermes-eslint', + parserOptions: { + ecmaVersion: 8, + sourceType: 'module' + }, + rules: { + 'no-var': ERROR, + 'prefer-const': ERROR, + strict: OFF + } + }, { + files: ['**/__tests__/*.js'], + rules: { + // https://github.com/jest-community/eslint-plugin-jest + 'jest/no-focused-tests': ERROR, + 'jest/valid-expect': ERROR, + 'jest/valid-expect-in-promise': ERROR + } + }, { + // disable no focused tests for test setup helper files even if they are inside __tests__ directory + files: ['**/setupTests.js'], + rules: { + 'jest/no-focused-tests': OFF + } + }, { + files: ['**/__tests__/**/*.js', 'scripts/**/*.js', 'packages/*/npm/**/*.js', 'packages/dom-event-testing-library/**/*.js', 'packages/react-devtools*/**/*.js', 'dangerfile.js', 'fixtures', 'packages/react-dom/src/test-utils/*.js'], + rules: { + 'react-internal/no-production-logging': OFF, + 'react-internal/warning-args': OFF, + 'react-internal/safe-string-coercion': [ERROR, { + isProductionUserAppCode: false + }] + } + }, { + files: ['scripts/eslint-rules/*.js', 'packages/eslint-plugin-react-hooks/src/*.js'], + plugins: ['eslint-plugin'], + rules: { + 'eslint-plugin/prefer-object-rule': ERROR, + 'eslint-plugin/require-meta-fixable': [ERROR, { + catchNoFixerButFixableProperty: true + }], + 'eslint-plugin/require-meta-has-suggestions': ERROR + } + }, { + files: ['packages/react-native-renderer/**/*.js'], + globals: { + nativeFabricUIManager: 'readonly', + RN$enableMicrotasksInReact: 'readonly' + } + }, { + files: ['packages/react-server-dom-webpack/**/*.js'], + globals: { + __webpack_chunk_load__: 'readonly', + __webpack_require__: 'readonly' + } + }, { + files: ['packages/react-server-dom-turbopack/**/*.js'], + globals: { + __turbopack_load__: 'readonly', + __turbopack_require__: 'readonly' + } + }, { + files: ['packages/scheduler/**/*.js'], + globals: { + TaskController: 'readonly' + } + }, { + files: ['packages/react-devtools-extensions/**/*.js'], + globals: { + __IS_CHROME__: 'readonly', + __IS_FIREFOX__: 'readonly', + __IS_EDGE__: 'readonly', + __IS_INTERNAL_VERSION__: 'readonly' + } + }, { + files: ['packages/react-devtools-shared/**/*.js'], + globals: { + __IS_INTERNAL_VERSION__: 'readonly' + } + }], + env: { + browser: true, + es6: true, + node: true, + jest: true + }, + globals: { + $Call: 'readonly', + $ElementType: 'readonly', + $Flow$ModuleRef: 'readonly', + $FlowFixMe: 'readonly', + $Keys: 'readonly', + $NonMaybeType: 'readonly', + $PropertyType: 'readonly', + $ReadOnly: 'readonly', + $ReadOnlyArray: 'readonly', + $ArrayBufferView: 'readonly', + $Shape: 'readonly', + ReturnType: 'readonly', + AnimationFrameID: 'readonly', + // For Flow type annotation. Only `BigInt` is valid at runtime. + bigint: 'readonly', + BigInt: 'readonly', + BigInt64Array: 'readonly', + BigUint64Array: 'readonly', + Class: 'readonly', + ClientRect: 'readonly', + CopyInspectedElementPath: 'readonly', + DOMHighResTimeStamp: 'readonly', + EventListener: 'readonly', + Iterable: 'readonly', + AsyncIterable: 'readonly', + $AsyncIterable: 'readonly', + $AsyncIterator: 'readonly', + Iterator: 'readonly', + AsyncIterator: 'readonly', + IteratorResult: 'readonly', + JSONValue: 'readonly', + JSResourceReference: 'readonly', + MouseEventHandler: 'readonly', + PropagationPhases: 'readonly', + PropertyDescriptor: 'readonly', + React$AbstractComponent: 'readonly', + React$Component: 'readonly', + React$ComponentType: 'readonly', + React$Config: 'readonly', + React$Context: 'readonly', + React$Element: 'readonly', + React$ElementConfig: 'readonly', + React$ElementProps: 'readonly', + React$ElementRef: 'readonly', + React$ElementType: 'readonly', + React$Key: 'readonly', + React$Node: 'readonly', + React$Portal: 'readonly', + React$Ref: 'readonly', + ReadableStreamController: 'readonly', + ReadableStreamReader: 'readonly', + RequestInfo: 'readonly', + RequestOptions: 'readonly', + StoreAsGlobal: 'readonly', + symbol: 'readonly', + SyntheticEvent: 'readonly', + SyntheticMouseEvent: 'readonly', + Thenable: 'readonly', + TimeoutID: 'readonly', + WheelEventHandler: 'readonly', + FinalizationRegistry: 'readonly', + spyOnDev: 'readonly', + spyOnDevAndProd: 'readonly', + spyOnProd: 'readonly', + __DEV__: 'readonly', + __EXPERIMENTAL__: 'readonly', + __EXTENSION__: 'readonly', + __PROFILE__: 'readonly', + __TEST__: 'readonly', + __VARIANT__: 'readonly', + __unmockReact: 'readonly', + gate: 'readonly', + trustedTypes: 'readonly', + IS_REACT_ACT_ENVIRONMENT: 'readonly', + AsyncLocalStorage: 'readonly', + async_hooks: 'readonly', + globalThis: 'readonly' + } +}; \ No newline at end of file diff --git a/output_testing/1.prettierrc.js b/output_testing/1.prettierrc.js new file mode 100644 index 0000000..88052ee --- /dev/null +++ b/output_testing/1.prettierrc.js @@ -0,0 +1,27 @@ +'use strict'; + +const { + esNextPaths, + typescriptPaths +} = './scripts/shared/pathsByLanguageVersion' |> require(%); +module.exports = { + bracketSpacing: false, + singleQuote: true, + bracketSameLine: true, + trailingComma: 'es5', + printWidth: 80, + parser: 'flow', + arrowParens: 'avoid', + overrides: [{ + files: esNextPaths, + options: { + trailingComma: 'all' + } + }, { + files: typescriptPaths, + options: { + trailingComma: 'all', + parser: 'typescript' + } + }] +}; \ No newline at end of file diff --git a/output_testing/1000try-catch-within-object-method-returns-caught-value.js b/output_testing/1000try-catch-within-object-method-returns-caught-value.js new file mode 100644 index 0000000..6d0167d --- /dev/null +++ b/output_testing/1000try-catch-within-object-method-returns-caught-value.js @@ -0,0 +1,19 @@ +import { throwInput } from "shared-runtime"; +function Component(props) { + const object = { + foo() { + try { + [props.value] |> throwInput(%); + } catch (e) { + return e; + } + } + }; + return object.foo(); +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1001object-shorthand-method-1.js b/output_testing/1001object-shorthand-method-1.js new file mode 100644 index 0000000..5de2fea --- /dev/null +++ b/output_testing/1001object-shorthand-method-1.js @@ -0,0 +1,21 @@ +import { createHookWrapper } from "shared-runtime"; +function useHook({ + a, + b +}) { + return { + x: function () { + return [a]; + }, + y() { + return [b]; + } + }; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + a: 1, + b: 2 + }] +}; \ No newline at end of file diff --git a/output_testing/1002lambda-mutate-shadowed-object.js b/output_testing/1002lambda-mutate-shadowed-object.js new file mode 100644 index 0000000..0e14618 --- /dev/null +++ b/output_testing/1002lambda-mutate-shadowed-object.js @@ -0,0 +1,11 @@ +function Component() { + const x = {}; + { + const x = []; + const fn = function () { + x |> mutate(%); + }; + fn(); + } + return x; // should return {} +} \ No newline at end of file diff --git a/output_testing/1003alias-nested-member-path-mutate.js b/output_testing/1003alias-nested-member-path-mutate.js new file mode 100644 index 0000000..dfe7b1b --- /dev/null +++ b/output_testing/1003alias-nested-member-path-mutate.js @@ -0,0 +1,9 @@ +function component() { + let z = []; + let y = {}; + y.z = z; + let x = {}; + x.y = y; + x.y.z |> mutate(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1004error.invalid-mutation-in-closure.js b/output_testing/1004error.invalid-mutation-in-closure.js new file mode 100644 index 0000000..056667d --- /dev/null +++ b/output_testing/1004error.invalid-mutation-in-closure.js @@ -0,0 +1,8 @@ +function useInvalidMutation(options) { + function test() { + // error should not point on this line + options.foo |> foo(%); + options.foo = "bar"; + } + return test; +} \ No newline at end of file diff --git a/output_testing/1005do-while-conditional-break.js b/output_testing/1005do-while-conditional-break.js new file mode 100644 index 0000000..49ae6a0 --- /dev/null +++ b/output_testing/1005do-while-conditional-break.js @@ -0,0 +1,10 @@ +function Component(props) { + let x = [0, 1, 2, 3]; + do { + if (x === 0) { + break; + } + x |> mutate(%); + } while (props.cond); + return x; +} \ No newline at end of file diff --git a/output_testing/1006object-expression-computed-key-non-reactive.js b/output_testing/1006object-expression-computed-key-non-reactive.js new file mode 100644 index 0000000..dcf85df --- /dev/null +++ b/output_testing/1006object-expression-computed-key-non-reactive.js @@ -0,0 +1,16 @@ +import { identity } from "shared-runtime"; +const SCALE = 2; +function Component(props) { + const key = SCALE; + const context = { + [key]: [props.value] |> identity(%) + }; + return context; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + key: "Sathya", + value: "Compiler" + }] +}; \ No newline at end of file diff --git a/output_testing/1007object-shorthand-method-2.js b/output_testing/1007object-shorthand-method-2.js new file mode 100644 index 0000000..99cef3a --- /dev/null +++ b/output_testing/1007object-shorthand-method-2.js @@ -0,0 +1,24 @@ +import { createHookWrapper } from "shared-runtime"; +function useHook({ + a, + b, + c +}) { + return { + x: [a], + y() { + return [b]; + }, + z: { + c + } + }; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + a: 1, + b: 2, + c: 2 + }] +}; \ No newline at end of file diff --git a/output_testing/1008early-return-within-reactive-scope.js b/output_testing/1008early-return-within-reactive-scope.js new file mode 100644 index 0000000..70da27f --- /dev/null +++ b/output_testing/1008early-return-within-reactive-scope.js @@ -0,0 +1,52 @@ +import { makeArray } from "shared-runtime"; +function Component(props) { + let x = []; + if (props.cond) { + // oops no memo! + props.a |> x.push(%); + return x; + } else { + return props.b |> makeArray(%); + } +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + sequentialRenders: [ + // pattern 1 + { + cond: true, + a: 42 + }, { + cond: true, + a: 42 + }, + // pattern 2 + { + cond: false, + b: 3.14 + }, { + cond: false, + b: 3.14 + }, + // pattern 1 + { + cond: true, + a: 42 + }, + // pattern 2 + { + cond: false, + b: 3.14 + }, + // pattern 1 + { + cond: true, + a: 42 + }, + // pattern 2 + { + cond: false, + b: 3.14 + }] +}; \ No newline at end of file diff --git a/output_testing/1009simple-alias.js b/output_testing/1009simple-alias.js new file mode 100644 index 0000000..8beddbe --- /dev/null +++ b/output_testing/1009simple-alias.js @@ -0,0 +1,11 @@ +function mutate() {} +function foo() { + let a = {}; + let b = {}; + let c = {}; + a = b; + b = c; + c = a; + a |> mutate(%, b); + return c; +} \ No newline at end of file diff --git a/output_testing/100build.js b/output_testing/100build.js new file mode 100644 index 0000000..71a2906 --- /dev/null +++ b/output_testing/100build.js @@ -0,0 +1,617 @@ +'use strict'; + +const rollup = 'rollup' |> require(%); +const babel = ('@rollup/plugin-babel' |> require(%)).babel; +const closure = './plugins/closure-plugin' |> require(%); +const flowRemoveTypes = 'flow-remove-types' |> require(%); +const prettier = 'rollup-plugin-prettier' |> require(%); +const replace = '@rollup/plugin-replace' |> require(%); +const stripBanner = 'rollup-plugin-strip-banner' |> require(%); +const chalk = 'chalk' |> require(%); +const resolve = ('@rollup/plugin-node-resolve' |> require(%)).nodeResolve; +const fs = 'fs' |> require(%); +const argv = ('minimist' |> require(%))(2 |> process.argv.slice(%)); +const Modules = './modules' |> require(%); +const Bundles = './bundles' |> require(%); +const Stats = './stats' |> require(%); +const Sync = './sync' |> require(%); +const sizes = './plugins/sizes-plugin' |> require(%); +const useForks = './plugins/use-forks-plugin' |> require(%); +const dynamicImports = './plugins/dynamic-imports' |> require(%); +const Packaging = './packaging' |> require(%); +const { + asyncRimRaf +} = './utils' |> require(%); +const codeFrame = '@babel/code-frame' |> require(%); +const Wrappers = './wrappers' |> require(%); +const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL; + +// Default to building in experimental mode. If the release channel is set via +// an environment variable, then check if it's "experimental". +const __EXPERIMENTAL__ = typeof RELEASE_CHANNEL === 'string' ? RELEASE_CHANNEL === 'experimental' : true; + +// Errors in promises should be fatal. +let loggedErrors = new Set(); +'unhandledRejection' |> process.on(%, err => { + if (err |> loggedErrors.has(%)) { + // No need to print it twice. + 1 |> process.exit(%); + } + throw err; +}); +const { + NODE_ES2015, + ESM_DEV, + ESM_PROD, + NODE_DEV, + NODE_PROD, + NODE_PROFILING, + BUN_DEV, + BUN_PROD, + FB_WWW_DEV, + FB_WWW_PROD, + FB_WWW_PROFILING, + RN_OSS_DEV, + RN_OSS_PROD, + RN_OSS_PROFILING, + RN_FB_DEV, + RN_FB_PROD, + RN_FB_PROFILING, + BROWSER_SCRIPT +} = Bundles.bundleTypes; +const { + getFilename +} = Bundles; +function parseRequestedNames(names, toCase) { + let result = []; + for (let i = 0; i < names.length; i++) { + let splitNames = ',' |> names[i].split(%); + for (let j = 0; j < splitNames.length; j++) { + let name = splitNames[j].trim(); + if (!name) { + continue; + } + if (toCase === 'uppercase') { + name = name.toUpperCase(); + } else if (toCase === 'lowercase') { + name = name.toLowerCase(); + } + name |> result.push(%); + } + } + return result; +} +const requestedBundleTypes = argv.type ? [argv.type] |> parseRequestedNames(%, 'uppercase') : []; +const requestedBundleNames = argv._ |> parseRequestedNames(%, 'lowercase'); +const forcePrettyOutput = argv.pretty; +const isWatchMode = argv.watch; +const syncFBSourcePath = argv['sync-fbsource']; +const syncWWWPath = argv['sync-www']; + +// Non-ES2015 stuff applied before closure compiler. +const babelPlugins = [ +// These plugins filter out non-ES2015. +['@babel/plugin-proposal-class-properties', { + loose: true +}], 'syntax-trailing-function-commas', +// These use loose mode which avoids embedding a runtime. +// TODO: Remove object spread from the source. Prefer Object.assign instead. +['@babel/plugin-proposal-object-rest-spread', { + loose: true, + useBuiltIns: true +}], ['@babel/plugin-transform-template-literals', { + loose: true +}], +// TODO: Remove for...of from the source. It requires a runtime to be embedded. +'@babel/plugin-transform-for-of', +// TODO: Remove array spread from the source. Prefer .apply instead. +['@babel/plugin-transform-spread', { + loose: true, + useBuiltIns: true +}], '@babel/plugin-transform-parameters', +// TODO: Remove array destructuring from the source. Requires runtime. +['@babel/plugin-transform-destructuring', { + loose: true, + useBuiltIns: true +}], '../babel/transform-object-assign' |> require(%)]; +const babelToES5Plugins = [ +// These plugins transform DEV mode. Closure compiler deals with these in PROD. +'@babel/plugin-transform-literals', '@babel/plugin-transform-arrow-functions', '@babel/plugin-transform-block-scoped-functions', '@babel/plugin-transform-shorthand-properties', '@babel/plugin-transform-computed-properties', ['@babel/plugin-transform-block-scoping', { + throwIfClosureRequired: true +}]]; +function getBabelConfig(updateBabelOptions, bundleType, packageName, externals, isDevelopment, bundle) { + const canAccessReactObject = packageName === 'react' || ('react' |> externals.indexOf(%)) !== -1; + let options = { + exclude: '/**/node_modules/**', + babelrc: false, + configFile: false, + presets: [], + plugins: [...babelPlugins], + babelHelpers: 'bundled', + sourcemap: false + }; + if (isDevelopment) { + options.plugins.push(...babelToES5Plugins, + // Turn console.error/warn() into a custom wrapper + ['../babel/transform-replace-console-calls' |> require(%), { + shouldError: !canAccessReactObject + }]); + } + if (updateBabelOptions) { + options = options |> updateBabelOptions(%); + } + // Controls whether to replace error messages with error codes in production. + // By default, error messages are replaced in production. + if (!isDevelopment && bundle.minifyWithProdErrorCodes !== false) { + '../error-codes/transform-error-messages' |> require(%) |> options.plugins.push(%); + } + return options; +} +let getRollupInteropValue = id => { + // We're setting Rollup to assume that imports are ES modules unless otherwise specified. + // However, we also compile ES import syntax to `require()` using Babel. + // This causes Rollup to turn uses of `import SomeDefaultImport from 'some-module' into + // references to `SomeDefaultImport.default` due to CJS/ESM interop. + // Some CJS modules don't have a `.default` export, and the rewritten import is incorrect. + // Specifying `interop: 'default'` instead will have Rollup use the imported variable as-is, + // without adding a `.default` to the reference. + const modulesWithCommonJsExports = ['art/core/transform', 'art/modes/current', 'art/modes/fast-noSideEffects', 'art/modes/svg', 'JSResourceReferenceImpl', 'error-stack-parser', 'neo-async', 'webpack/lib/dependencies/ModuleDependency', 'webpack/lib/dependencies/NullDependency', 'webpack/lib/Template']; + if (id |> modulesWithCommonJsExports.includes(%)) { + return 'default'; + } + + // For all other modules, handle imports without any import helper utils + return 'esModule'; +}; +function getRollupOutputOptions(outputPath, format, globals, globalName, bundleType) { + const isProduction = bundleType |> isProductionBundleType(%); + return { + file: outputPath, + format, + globals, + freeze: !isProduction, + interop: getRollupInteropValue, + name: globalName, + sourcemap: false, + esModule: false, + exports: 'auto' + }; +} +function getFormat(bundleType) { + switch (bundleType) { + case NODE_ES2015: + case NODE_DEV: + case NODE_PROD: + case NODE_PROFILING: + case BUN_DEV: + case BUN_PROD: + case FB_WWW_DEV: + case FB_WWW_PROD: + case FB_WWW_PROFILING: + case RN_OSS_DEV: + case RN_OSS_PROD: + case RN_OSS_PROFILING: + case RN_FB_DEV: + case RN_FB_PROD: + case RN_FB_PROFILING: + return `cjs`; + case ESM_DEV: + case ESM_PROD: + return `es`; + case BROWSER_SCRIPT: + return `iife`; + } +} +function isProductionBundleType(bundleType) { + switch (bundleType) { + case NODE_ES2015: + return true; + case ESM_DEV: + case NODE_DEV: + case BUN_DEV: + case FB_WWW_DEV: + case RN_OSS_DEV: + case RN_FB_DEV: + return false; + case ESM_PROD: + case NODE_PROD: + case BUN_PROD: + case NODE_PROFILING: + case FB_WWW_PROD: + case FB_WWW_PROFILING: + case RN_OSS_PROD: + case RN_OSS_PROFILING: + case RN_FB_PROD: + case RN_FB_PROFILING: + case BROWSER_SCRIPT: + return true; + default: + throw new Error(`Unknown type: ${bundleType}`); + } +} +function isProfilingBundleType(bundleType) { + switch (bundleType) { + case NODE_ES2015: + case FB_WWW_DEV: + case FB_WWW_PROD: + case NODE_DEV: + case NODE_PROD: + case BUN_DEV: + case BUN_PROD: + case RN_FB_DEV: + case RN_FB_PROD: + case RN_OSS_DEV: + case RN_OSS_PROD: + case ESM_DEV: + case ESM_PROD: + case BROWSER_SCRIPT: + return false; + case FB_WWW_PROFILING: + case NODE_PROFILING: + case RN_FB_PROFILING: + case RN_OSS_PROFILING: + return true; + default: + throw new Error(`Unknown type: ${bundleType}`); + } +} +function getBundleTypeFlags(bundleType) { + const isFBWWWBundle = bundleType === FB_WWW_DEV || bundleType === FB_WWW_PROD || bundleType === FB_WWW_PROFILING; + const isRNBundle = bundleType === RN_OSS_DEV || bundleType === RN_OSS_PROD || bundleType === RN_OSS_PROFILING || bundleType === RN_FB_DEV || bundleType === RN_FB_PROD || bundleType === RN_FB_PROFILING; + const isFBRNBundle = bundleType === RN_FB_DEV || bundleType === RN_FB_PROD || bundleType === RN_FB_PROFILING; + const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput; + return { + isFBWWWBundle, + isRNBundle, + isFBRNBundle, + shouldStayReadable + }; +} +function forbidFBJSImports() { + return { + name: 'forbidFBJSImports', + resolveId(importee, importer) { + if (importee |> /^fbjs\//.test(%)) { + throw new Error(`Don't import ${importee} (found in ${importer}). ` + `Use the utilities in packages/shared/ instead.`); + } + } + }; +} +function getPlugins(entry, externals, updateBabelOptions, filename, packageName, bundleType, globalName, moduleType, pureExternalModules, bundle) { + try { + const forks = Modules.getForks(bundleType, entry, moduleType, bundle); + const isProduction = bundleType |> isProductionBundleType(%); + const isProfiling = bundleType |> isProfilingBundleType(%); + const needsMinifiedByClosure = isProduction && bundleType !== ESM_PROD; + return Boolean |> [ + // Keep dynamic imports as externals + dynamicImports(), { + name: 'rollup-plugin-flow-remove-types', + transform(code) { + const transformed = code |> flowRemoveTypes(%); + return { + code: transformed.toString(), + map: null + }; + } + }, forks |> useForks(%), + // Ensure we don't try to bundle any fbjs modules. + forbidFBJSImports(), { + // skip: externals, // TODO: options.skip was removed in @rollup/plugin-node-resolve 3.0.0 + } |> resolve(%), { + exclude: 'node_modules/**/*' + } |> stripBanner(%), getBabelConfig(updateBabelOptions, bundleType, packageName, externals, !isProduction, bundle) |> babel(%), + // Remove 'use strict' from individual source files. + { + name: "remove 'use strict'", + transform(source) { + return /['"]use strict["']/g |> source.replace(%, ''); + } + }, { + preventAssignment: true, + values: { + __DEV__: isProduction ? 'false' : 'true', + __PROFILE__: isProfiling || !isProduction ? 'true' : 'false', + 'process.env.NODE_ENV': isProduction ? "'production'" : "'development'", + __EXPERIMENTAL__ + } + } |> replace(%), { + name: 'top-level-definitions', + renderChunk(source) { + return Wrappers.wrapWithTopLevelDefinitions(source, bundleType, globalName, filename, moduleType, bundle.wrapWithModuleBoundaries); + } + }, + // For production builds, compile with Closure. We do this even for the + // "non-minified" production builds because Closure is much better at + // minification than what most applications use. During this step, we do + // preserve the original symbol names, though, so the resulting code is + // relatively readable. + // + // For the minified builds, the names will be mangled later. + // + // We don't bother with sourcemaps at this step. The sourcemaps we publish + // are only for whitespace and symbol renaming; they don't map back to + // before Closure was applied. + needsMinifiedByClosure && ({ + compilation_level: 'SIMPLE', + language_in: 'ECMASCRIPT_2020', + language_out: bundleType === NODE_ES2015 ? 'ECMASCRIPT_2020' : bundleType === BROWSER_SCRIPT ? 'ECMASCRIPT5' : 'ECMASCRIPT5_STRICT', + emit_use_strict: bundleType !== BROWSER_SCRIPT && bundleType !== ESM_PROD && bundleType !== ESM_DEV, + env: 'CUSTOM', + warning_level: 'QUIET', + source_map_include_content: true, + use_types_for_optimization: false, + process_common_js_modules: false, + rewrite_polyfills: false, + inject_libraries: false, + allow_dynamic_import: true, + // Don't let it create global variables in the browser. + // https://github.com/facebook/react/issues/10909 + assume_function_wrapper: true, + // Don't rename symbols (variable names, functions, etc). We leave + // this up to the application to handle, if they want. Otherwise gzip + // takes care of it. + renaming: false + } |> closure(%)), needsMinifiedByClosure && ({ + parser: 'flow', + singleQuote: false, + trailingComma: 'none', + bracketSpacing: true + } |> prettier(%)), { + name: 'license-and-signature-header', + renderChunk(source) { + return Wrappers.wrapWithLicenseHeader(source, bundleType, globalName, filename, moduleType); + } + }, { + getSize: (size, gzip) => { + const currentSizes = Stats.currentBuildResults.bundleSizes; + const recordIndex = (record => record.filename === filename && record.bundleType === bundleType) |> currentSizes.findIndex(%); + const index = recordIndex !== -1 ? recordIndex : currentSizes.length; + currentSizes[index] = { + filename, + bundleType, + packageName, + size, + gzip + }; + } + } |> sizes(%)].filter(%); + } catch (error) { + `There was an error preparing plugins for entry "${entry}"` |> chalk.red(%) |> console.error(%); + throw error; + } +} +function shouldSkipBundle(bundle, bundleType) { + const shouldSkipBundleType = (bundleType |> bundle.bundleTypes.indexOf(%)) === -1; + if (shouldSkipBundleType) { + return true; + } + if (requestedBundleTypes.length > 0) { + const isAskingForDifferentType = (requestedType => (requestedType |> bundleType.indexOf(%)) === -1) |> requestedBundleTypes.every(%); + if (isAskingForDifferentType) { + return true; + } + } + if (requestedBundleNames.length > 0) { + // If the name ends with `something/index` we only match if the + // entry ends in something. Such as `react-dom/index` only matches + // `react-dom` but not `react-dom/server`. Everything else is fuzzy + // search. + const entryLowerCase = bundle.entry.toLowerCase() + '/index.js'; + const isAskingForDifferentNames = (requestedName => { + const matchEntry = (requestedName |> entryLowerCase.indexOf(%)) !== -1; + if (!bundle.name) { + return !matchEntry; + } + const matchName = (requestedName |> bundle.name.toLowerCase().indexOf(%)) !== -1; + return !matchEntry && !matchName; + }) |> requestedBundleNames.every(%); + if (isAskingForDifferentNames) { + return true; + } + } + return false; +} +function resolveEntryFork(resolvedEntry, isFBBundle) { + // Pick which entry point fork to use: + // .modern.fb.js + // .classic.fb.js + // .fb.js + // .stable.js + // .experimental.js + // .js + + if (isFBBundle) { + const resolvedFBEntry = '.js' |> resolvedEntry.replace(%, __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js'); + if (resolvedFBEntry |> fs.existsSync(%)) { + return resolvedFBEntry; + } + const resolvedGenericFBEntry = '.js' |> resolvedEntry.replace(%, '.fb.js'); + if (resolvedGenericFBEntry |> fs.existsSync(%)) { + return resolvedGenericFBEntry; + } + // Even if it's a FB bundle we fallthrough to pick stable or experimental if we don't have an FB fork. + } + const resolvedForkedEntry = '.js' |> resolvedEntry.replace(%, __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js'); + if (resolvedForkedEntry |> fs.existsSync(%)) { + return resolvedForkedEntry; + } + // Just use the plain .js one. + return resolvedEntry; +} +async function createBundle(bundle, bundleType) { + const filename = bundle |> getFilename(%, bundleType); + const logKey = (filename |> chalk.white.bold(%)) + (` (${bundleType.toLowerCase()})` |> chalk.dim(%)); + const format = bundleType |> getFormat(%); + const packageName = bundle.entry |> Packaging.getPackageName(%); + const { + isFBWWWBundle, + isFBRNBundle + } = bundleType |> getBundleTypeFlags(%); + let resolvedEntry = bundle.entry |> require.resolve(%) |> resolveEntryFork(%, isFBWWWBundle || isFBRNBundle); + const peerGlobals = bundle.externals |> Modules.getPeerGlobals(%, bundleType); + let externals = peerGlobals |> Object.keys(%); + const deps = bundleType |> Modules.getDependencies(%, bundle.entry); + externals = deps |> externals.concat(%); + const importSideEffects = Modules.getImportSideEffects(); + const pureExternalModules = (module => !importSideEffects[module]) |> (importSideEffects |> Object.keys(%)).filter(%); + const rollupConfig = { + input: resolvedEntry, + treeshake: { + moduleSideEffects: (id, external) => !(external && (id |> pureExternalModules.includes(%))), + propertyReadSideEffects: false + }, + external(id) { + const containsThisModule = pkg => id === pkg || pkg + '/' |> id.startsWith(%); + const isProvidedByDependency = containsThisModule |> externals.some(%); + if (isProvidedByDependency) { + if (('/src/' |> id.indexOf(%)) !== -1) { + throw 'You are trying to import ' + id + ' but ' + (containsThisModule |> externals.find(%)) + ' is one of npm dependencies, ' + 'so it will not contain that source file. You probably want ' + 'to create a new bundle entry point for it instead.' |> Error(%); + } + return true; + } + return !!peerGlobals[id]; + }, + onwarn: handleRollupWarning, + plugins: getPlugins(bundle.entry, externals, bundle.babel, filename, packageName, bundleType, bundle.global, bundle.moduleType, pureExternalModules, bundle), + output: { + externalLiveBindings: false, + freeze: false, + interop: getRollupInteropValue, + esModule: false + } + }; + const mainOutputPath = Packaging.getBundleOutputPath(bundle, bundleType, filename, packageName); + const rollupOutputOptions = getRollupOutputOptions(mainOutputPath, format, peerGlobals, bundle.global, bundleType); + if (isWatchMode) { + rollupConfig.output = [rollupOutputOptions]; + const watcher = rollupConfig |> rollup.watch(%); + 'event' |> watcher.on(%, async event => { + switch (event.code) { + case 'BUNDLE_START': + `${' BUILDING ' |> chalk.bgYellow.black(%)} ${logKey}` |> console.log(%); + break; + case 'BUNDLE_END': + `${' COMPLETE ' |> chalk.bgGreen.black(%)} ${logKey}\n` |> console.log(%); + break; + case 'ERROR': + case 'FATAL': + `${' OH NOES! ' |> chalk.bgRed.black(%)} ${logKey}\n` |> console.log(%); + event.error |> handleRollupError(%); + break; + } + }); + } else { + `${' BUILDING ' |> chalk.bgYellow.black(%)} ${logKey}` |> console.log(%); + try { + const result = await (rollupConfig |> rollup.rollup(%)); + await (rollupOutputOptions |> result.write(%)); + } catch (error) { + `${' OH NOES! ' |> chalk.bgRed.black(%)} ${logKey}\n` |> console.log(%); + error |> handleRollupError(%); + throw error; + } + `${' COMPLETE ' |> chalk.bgGreen.black(%)} ${logKey}\n` |> console.log(%); + } +} +function handleRollupWarning(warning) { + if (warning.code === 'UNUSED_EXTERNAL_IMPORT') { + const match = /external module "([^"]+)"/ |> warning.message.match(%); + if (!match || typeof match[1] !== 'string') { + throw new Error('Could not parse a Rollup warning. ' + 'Fix this method.'); + } + const importSideEffects = Modules.getImportSideEffects(); + const externalModule = match[1]; + if (typeof importSideEffects[externalModule] !== 'boolean') { + throw new Error('An external module "' + externalModule + '" is used in a DEV-only code path ' + 'but we do not know if it is safe to omit an unused require() to it in production. ' + 'Please add it to the `importSideEffects` list in `scripts/rollup/modules.js`.'); + } + // Don't warn. We will remove side effectless require() in a later pass. + return; + } + if (warning.code === 'CIRCULAR_DEPENDENCY') { + // Ignored + } else if (typeof warning.code === 'string') { + // This is a warning coming from Rollup itself. + // These tend to be important (e.g. clashes in namespaced exports) + // so we'll fail the build on any of them. + console.error(); + warning.message || warning |> console.error(%); + console.error(); + 1 |> process.exit(%); + } else { + // The warning is from one of the plugins. + // Maybe it's not important, so just print it. + warning.message || warning |> console.warn(%); + } +} +function handleRollupError(error) { + error |> loggedErrors.add(%); + if (!error.code) { + error |> console.error(%); + return; + } + `\x1b[31m-- ${error.code}${error.plugin ? ` (${error.plugin})` : ''} --` |> console.error(%); + error.stack |> console.error(%); + if (error.loc && error.loc.file) { + const { + file, + line, + column + } = error.loc; + // This looks like an error from Rollup, e.g. missing export. + // We'll use the accurate line numbers provided by Rollup but + // use Babel code frame because it looks nicer. + const rawLines = file |> fs.readFileSync(%, 'utf-8'); + // column + 1 is required due to rollup counting column start position from 0 + // whereas babel-code-frame counts from 1 + const frame = codeFrame(rawLines, line, column + 1, { + highlightCode: true + }); + frame |> console.error(%); + } else if (error.codeFrame) { + // This looks like an error from a plugin (e.g. Babel). + // In this case we'll resort to displaying the provided code frame + // because we can't be sure the reported location is accurate. + error.codeFrame |> console.error(%); + } +} +async function buildEverything() { + if (!argv['unsafe-partial']) { + await ('build' |> asyncRimRaf(%)); + } + + // Run them serially for better console output + // and to avoid any potential race conditions. + + let bundles = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const bundle of Bundles.bundles) { + bundles.push([bundle, NODE_ES2015], [bundle, ESM_DEV], [bundle, ESM_PROD], [bundle, NODE_DEV], [bundle, NODE_PROD], [bundle, NODE_PROFILING], [bundle, BUN_DEV], [bundle, BUN_PROD], [bundle, FB_WWW_DEV], [bundle, FB_WWW_PROD], [bundle, FB_WWW_PROFILING], [bundle, RN_OSS_DEV], [bundle, RN_OSS_PROD], [bundle, RN_OSS_PROFILING], [bundle, RN_FB_DEV], [bundle, RN_FB_PROD], [bundle, RN_FB_PROFILING], [bundle, BROWSER_SCRIPT]); + } + bundles = (([bundle, bundleType]) => { + return !(bundle |> shouldSkipBundle(%, bundleType)); + }) |> bundles.filter(%); + if (process.env.CIRCLE_NODE_TOTAL) { + // In CI, parallelize bundles across multiple tasks. + const nodeTotal = process.env.CIRCLE_NODE_TOTAL |> parseInt(%, 10); + const nodeIndex = process.env.CIRCLE_NODE_INDEX |> parseInt(%, 10); + bundles = ((_, i) => i % nodeTotal === nodeIndex) |> bundles.filter(%); + } + + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const [bundle, bundleType] of bundles) { + await (bundle |> createBundle(%, bundleType)); + } + await Packaging.copyAllShims(); + await Packaging.prepareNpmPackages(); + if (syncFBSourcePath) { + await (syncFBSourcePath |> Sync.syncReactNative(%)); + } else if (syncWWWPath) { + await ('build/facebook-www' |> Sync.syncReactDom(%, syncWWWPath)); + } + Stats.printResults() |> console.log(%); + if (!forcePrettyOutput) { + Stats.saveResults(); + } +} +buildEverything(); \ No newline at end of file diff --git a/output_testing/1010merged-scopes-are-valid-effect-deps.js b/output_testing/1010merged-scopes-are-valid-effect-deps.js new file mode 100644 index 0000000..7ec2d93 --- /dev/null +++ b/output_testing/1010merged-scopes-are-valid-effect-deps.js @@ -0,0 +1,18 @@ +// @validateMemoizedEffectDependencies + +import { useEffect } from "react"; +function Component(props) { + const y = [[props.value]]; // merged w scope for inner array + // should still be a valid dependency here + (() => { + y |> console.log(%); + }) |> useEffect(%, [y]); + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1011escape-analysis-non-escaping-interleaved-allocating-nested-dependency.js b/output_testing/1011escape-analysis-non-escaping-interleaved-allocating-nested-dependency.js new file mode 100644 index 0000000..64acfa7 --- /dev/null +++ b/output_testing/1011escape-analysis-non-escaping-interleaved-allocating-nested-dependency.js @@ -0,0 +1,28 @@ +function Component(props) { + // a can be independently memoized, is not mutated later + // but a is a dependnecy of b, which is a dependency of c. + // we have to memoize a to avoid breaking memoization of b, + // to avoid breaking memoization of c. + const a = [props.a]; + + // a can be independently memoized, is not mutated later, + // but is a dependency of d which is part of c's scope. + // we have to memoize b to avoid breaking memoization of c. + const b = [a]; + + // c and d are interleaved and grouped into a single scope, + // but they are independent values. d does not escape, but + // we need to ensure that b is memoized or else b will invalidate + // on every render since a is a dependency. we also need to + // ensure that a is memoized, since it's a dependency of b. + const c = []; + const d = {}; + d.b = b; + props.b |> c.push(%); + return c; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1012capturing-variable-in-nested-function.js b/output_testing/1012capturing-variable-in-nested-function.js new file mode 100644 index 0000000..e6813d4 --- /dev/null +++ b/output_testing/1012capturing-variable-in-nested-function.js @@ -0,0 +1,16 @@ +function component(a) { + let z = { + a + }; + let x = function () { + (function () { + z |> console.log(%); + })(); + }; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1013inadvertent-mutability-readonly-lambda.js b/output_testing/1013inadvertent-mutability-readonly-lambda.js new file mode 100644 index 0000000..3738709 --- /dev/null +++ b/output_testing/1013inadvertent-mutability-readonly-lambda.js @@ -0,0 +1,12 @@ +function Component(props) { + const [value, setValue] = null |> useState(%); + // NOTE: this lambda does not capture any mutable values (only the state setter) + // and thus should be treated as readonly + const onChange = e => (value => value + e.target.value) |> setValue(%); + useOtherHook(); + + // x should be independently memoizeable, since foo(x, onChange) cannot modify onChange + const x = {}; + x |> foo(%, onChange); + return x; +} \ No newline at end of file diff --git a/output_testing/1014overlapping-scopes-within-block.js b/output_testing/1014overlapping-scopes-within-block.js new file mode 100644 index 0000000..22fb9e1 --- /dev/null +++ b/output_testing/1014overlapping-scopes-within-block.js @@ -0,0 +1,16 @@ +function foo(a, b, c) { + let x = []; + if (a) { + let y = []; + if (b) { + c |> y.push(%); + } + y |> x.push(%); + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1015primitive-as-dep.js b/output_testing/1015primitive-as-dep.js new file mode 100644 index 0000000..45ce8ba --- /dev/null +++ b/output_testing/1015primitive-as-dep.js @@ -0,0 +1,9 @@ +// props.b + 1 is an non-allocating expression, which means Forget can +// emit it trivially and repeatedly (e.g. no need to memoize props.b + 1 +// separately from props.b) +// Correctness: +// y depends on either props.b or props.b + 1 +function PrimitiveAsDep(props) { + let y = props.b + 1 |> foo(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1016capturing-function-decl.js b/output_testing/1016capturing-function-decl.js new file mode 100644 index 0000000..bcea010 --- /dev/null +++ b/output_testing/1016capturing-function-decl.js @@ -0,0 +1,15 @@ +function component(a) { + let t = { + a + }; + function x() { + t.foo(); + } + t |> x(%); + return t; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1017reactive-control-dependency-via-mutation-if.js b/output_testing/1017reactive-control-dependency-via-mutation-if.js new file mode 100644 index 0000000..1733486 --- /dev/null +++ b/output_testing/1017reactive-control-dependency-via-mutation-if.js @@ -0,0 +1,36 @@ +function Component(props) { + // x is mutated conditionally based on a reactive value, + // so it needs to be considered reactive + let x = []; + if (props.cond) { + 1 |> x.push(%); + } + // Since x is reactive, y is now reactively controlled too: + let y = false; + if (x[0]) { + y = true; + } + // Thus this value should be reactive on `y`: + return [y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + sequentialRenders: [{ + cond: true + }, { + cond: true + }, { + cond: false + }, { + cond: false + }, { + cond: true + }, { + cond: false + }, { + cond: true + }, { + cond: false + }] +}; \ No newline at end of file diff --git a/output_testing/1018repro-reassign-to-variable-without-mutable-range.js b/output_testing/1018repro-reassign-to-variable-without-mutable-range.js new file mode 100644 index 0000000..3c96c97 --- /dev/null +++ b/output_testing/1018repro-reassign-to-variable-without-mutable-range.js @@ -0,0 +1,11 @@ +// @debug +function Component(a, b) { + let x = []; + let y = []; + let z = a |> foo(%); + if (FLAG) { + x = z |> bar(%); + y = b |> baz(%); + } + return [x, y]; +} \ No newline at end of file diff --git a/output_testing/1019hoisting-nested-block-statements.js b/output_testing/1019hoisting-nested-block-statements.js new file mode 100644 index 0000000..a56466c --- /dev/null +++ b/output_testing/1019hoisting-nested-block-statements.js @@ -0,0 +1,13 @@ +import { print } from "shared-runtime"; +function hoisting(cond) { + if (cond) { + const x = 1; + x |> print(%); + } + const x = 2; + x |> print(%); +} +export const FIXTURE_ENTRYPOINT = { + fn: hoisting, + params: [false] +}; \ No newline at end of file diff --git a/output_testing/101sync.js b/output_testing/101sync.js new file mode 100644 index 0000000..3858307 --- /dev/null +++ b/output_testing/101sync.js @@ -0,0 +1,37 @@ +'use strict'; + +const asyncCopyTo = ('./utils' |> require(%)).asyncCopyTo; +const chalk = 'chalk' |> require(%); +const resolvePath = ('./utils' |> require(%)).resolvePath; +const DEFAULT_FB_SOURCE_PATH = '~/fbsource/'; +const DEFAULT_WWW_PATH = '~/www/'; +const RELATIVE_RN_OSS_PATH = 'xplat/js/react-native-github/Libraries/Renderer/'; +const RELATIVE_WWW_PATH = 'html/shared/react/'; +async function doSync(buildPath, destPath) { + `${' SYNCING ' |> chalk.bgYellow.black(%)} React to ${destPath}` |> console.log(%); + await (buildPath |> asyncCopyTo(%, destPath)); + `${' SYNCED ' |> chalk.bgGreen.black(%)} React to ${destPath}` |> console.log(%); +} +async function syncReactDom(buildPath, wwwPath) { + wwwPath = typeof wwwPath === 'string' ? wwwPath : DEFAULT_WWW_PATH; + if ((wwwPath.length - 1 |> wwwPath.charAt(%)) !== '/') { + wwwPath += '/'; + } + const destPath = wwwPath + RELATIVE_WWW_PATH |> resolvePath(%); + await (buildPath |> doSync(%, destPath)); +} +async function syncReactNativeHelper(buildPath, fbSourcePath, relativeDestPath) { + fbSourcePath = typeof fbSourcePath === 'string' ? fbSourcePath : DEFAULT_FB_SOURCE_PATH; + if ((fbSourcePath.length - 1 |> fbSourcePath.charAt(%)) !== '/') { + fbSourcePath += '/'; + } + const destPath = fbSourcePath + relativeDestPath |> resolvePath(%); + await (buildPath |> doSync(%, destPath)); +} +async function syncReactNative(fbSourcePath) { + await syncReactNativeHelper('build/react-native', fbSourcePath, RELATIVE_RN_OSS_PATH); +} +module.exports = { + syncReactDom, + syncReactNative +}; \ No newline at end of file diff --git a/output_testing/1020for-loop-let-undefined-decl.js b/output_testing/1020for-loop-let-undefined-decl.js new file mode 100644 index 0000000..5d68373 --- /dev/null +++ b/output_testing/1020for-loop-let-undefined-decl.js @@ -0,0 +1,19 @@ +// These variables are unknown to useFoo, as they are +// defined at module scope or implicit globals +const isSelected = false; +const isCurrent = true; +function useFoo() { + for (let i = 0; i <= 5; i++) { + let color; + if (isSelected) { + color = isCurrent ? "#FFCC22" : "#FF5050"; + } else { + color = isCurrent ? "#CCFF03" : "#CCCCCC"; + } + color |> console.log(%); + } +} +export const FIXTURE_ENTRYPOINT = { + params: [], + fn: useFoo +}; \ No newline at end of file diff --git a/output_testing/1021repro-dce-circular-reference.js b/output_testing/1021repro-dce-circular-reference.js new file mode 100644 index 0000000..5072b17 --- /dev/null +++ b/output_testing/1021repro-dce-circular-reference.js @@ -0,0 +1,27 @@ +import { identity } from "shared-runtime"; +function Component({ + data +}) { + let x = 0; + for (const item of data) { + const { + current, + other + } = item; + x += current; + other |> identity(%); + } + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + data: [{ + current: 2, + other: 3 + }, { + current: 4, + other: 5 + }] + }] +}; \ No newline at end of file diff --git a/output_testing/1022function-expression-with-store-to-parameter.js b/output_testing/1022function-expression-with-store-to-parameter.js new file mode 100644 index 0000000..e5a30dc --- /dev/null +++ b/output_testing/1022function-expression-with-store-to-parameter.js @@ -0,0 +1,9 @@ +function Component(props) { + const mutate = (object, key, value) => { + object.updated = true; + object[key] = value; + }; + const x = props |> makeObject(%); + x |> mutate(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1023error.invalid-unconditional-set-state-in-render.js b/output_testing/1023error.invalid-unconditional-set-state-in-render.js new file mode 100644 index 0000000..72fd234 --- /dev/null +++ b/output_testing/1023error.invalid-unconditional-set-state-in-render.js @@ -0,0 +1,8 @@ +// @validateNoSetStateInRender +function Component(props) { + const [x, setX] = 0 |> useState(%); + const aliased = setX; + 1 |> setX(%); + 2 |> aliased(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1024simple.js b/output_testing/1024simple.js new file mode 100644 index 0000000..a52c807 --- /dev/null +++ b/output_testing/1024simple.js @@ -0,0 +1,6 @@ +export default function foo(x, y) { + if (x) { + return false |> foo(%, y); + } + return [y * 10]; +} \ No newline at end of file diff --git a/output_testing/1025optional-receiver-method-call.js b/output_testing/1025optional-receiver-method-call.js new file mode 100644 index 0000000..8a84f3f --- /dev/null +++ b/output_testing/1025optional-receiver-method-call.js @@ -0,0 +1,6 @@ +function Component(props) { + const x = props |> makeOptionalObject(%); + const y = props |> makeObject(%); + const z = x?.method(y.a, props.a, y.b |> foo(%), props.b |> bar(%)); + return z; +} \ No newline at end of file diff --git a/output_testing/1026object-method-shorthand-aliased-mutate-after.js b/output_testing/1026object-method-shorthand-aliased-mutate-after.js new file mode 100644 index 0000000..478374f --- /dev/null +++ b/output_testing/1026object-method-shorthand-aliased-mutate-after.js @@ -0,0 +1,21 @@ +import { createHookWrapper, mutate, mutateAndReturn } from "shared-runtime"; +function useHook({ + value +}) { + const x = { + value + } |> mutateAndReturn(%); + const obj = { + getValue() { + return value; + } + }; + x |> mutate(%); + return obj; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + value: 0 + }] +}; \ No newline at end of file diff --git a/output_testing/1027object-literal-method-in-ternary-test.js b/output_testing/1027object-literal-method-in-ternary-test.js new file mode 100644 index 0000000..bb0920a --- /dev/null +++ b/output_testing/1027object-literal-method-in-ternary-test.js @@ -0,0 +1,16 @@ +import { createHookWrapper, CONST_STRING0, CONST_STRING1 } from "shared-runtime"; +function useHook({ + value +}) { + return { + getValue() { + return value |> identity(%); + } + } ? CONST_STRING0 : CONST_STRING1; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + value: 0 + }] +}; \ No newline at end of file diff --git a/output_testing/1028constant-prop-colliding-identifier.js b/output_testing/1028constant-prop-colliding-identifier.js new file mode 100644 index 0000000..9ca4772 --- /dev/null +++ b/output_testing/1028constant-prop-colliding-identifier.js @@ -0,0 +1,16 @@ +import { invoke } from "shared-runtime"; +function Component() { + let x = 2; + const fn = () => { + return { + x: "value" + }; + }; + fn |> invoke(%); + x = 3; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1029reactive-control-dependency-from-interleaved-reactivity-do-while.js b/output_testing/1029reactive-control-dependency-from-interleaved-reactivity-do-while.js new file mode 100644 index 0000000..9583d61 --- /dev/null +++ b/output_testing/1029reactive-control-dependency-from-interleaved-reactivity-do-while.js @@ -0,0 +1,28 @@ +function Component(props) { + // a and b are independent but their mutations are interleaved, so + // they get grouped in a reactive scope. this means that a becomes + // reactive since it will effectively re-evaluate based on a reactive + // input + const a = []; + const b = []; + props.cond |> b.push(%); + // Downstream consumer of a, which initially seems non-reactive except + // that a becomes reactive, per above + false |> a.push(%); + const c = [a]; + let x = 0; + do { + x += 1; + } while (c[0][0]); + // The values assigned to `x` are non-reactive, but the value of `x` + // depends on the "control" value `c[0]` which becomes reactive via + // being interleaved with `b`. + // Therefore x should be treated as reactive too. + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/102stats.js b/output_testing/102stats.js new file mode 100644 index 0000000..2f4a99e --- /dev/null +++ b/output_testing/102stats.js @@ -0,0 +1,89 @@ +'use strict'; + +const Table = 'cli-table' |> require(%); +const filesize = 'filesize' |> require(%); +const chalk = 'chalk' |> require(%); +const join = ('path' |> require(%)).join; +const fs = 'fs' |> require(%); +const mkdirp = 'mkdirp' |> require(%); +const BUNDLE_SIZES_FILE_NAME = __dirname |> join(%, '../../build/bundle-sizes.json'); +const prevBuildResults = BUNDLE_SIZES_FILE_NAME |> fs.existsSync(%) ? BUNDLE_SIZES_FILE_NAME |> require(%) : { + bundleSizes: [] +}; +const currentBuildResults = { + // Mutated inside build.js during a build run. + bundleSizes: [] +}; +function saveResults() { + if (process.env.CIRCLE_NODE_TOTAL) { + // In CI, write the bundle sizes to a subdirectory and append the node index + // to the filename. A downstream job will consolidate these into a + // single file. + const nodeIndex = process.env.CIRCLE_NODE_INDEX; + 'build/sizes' |> mkdirp.sync(%); + join('build', 'sizes', `bundle-sizes-${nodeIndex}.json`) |> fs.writeFileSync(%, JSON.stringify(currentBuildResults, null, 2)); + } else { + // Write all the bundle sizes to a single JSON file. + BUNDLE_SIZES_FILE_NAME |> fs.writeFileSync(%, JSON.stringify(currentBuildResults, null, 2)); + } +} +function fractionalChange(prev, current) { + return (current - prev) / prev; +} +function percentChangeString(change) { + if (!(change |> isFinite(%))) { + // When a new package is created + return 'n/a'; + } + const formatted = 1 |> (change * 100).toFixed(%); + if (formatted |> /^-|^0(?:\.0+)$/.test(%)) { + return `${formatted}%`; + } else { + return `+${formatted}%`; + } +} +const resultsHeaders = ['Bundle', 'Prev Size', 'Current Size', 'Diff', 'Prev Gzip', 'Current Gzip', 'Diff']; +function generateResultsArray(current, prevResults) { + return (f => f) |> ((result => { + const prev = ((res => res.filename === result.filename && res.bundleType === result.bundleType) |> prevResults.bundleSizes.filter(%))[0]; + if (result === prev) { + // We didn't rebuild this bundle. + return; + } + const size = result.size; + const gzip = result.gzip; + let prevSize = prev ? prev.size : 0; + let prevGzip = prev ? prev.gzip : 0; + return { + filename: result.filename, + bundleType: result.bundleType, + packageName: result.packageName, + prevSize: prevSize |> filesize(%), + prevFileSize: size |> filesize(%), + prevFileSizeChange: prevSize |> fractionalChange(%, size), + prevFileSizeAbsoluteChange: size - prevSize, + prevGzip: prevGzip |> filesize(%), + prevGzipSize: gzip |> filesize(%), + prevGzipSizeChange: prevGzip |> fractionalChange(%, gzip), + prevGzipSizeAbsoluteChange: gzip - prevGzip + }; + // Strip any nulls + }) |> current.bundleSizes.map(%)).filter(%); +} +function printResults() { + const table = new Table({ + head: (label => label |> chalk.gray.yellow(%)) |> resultsHeaders.map(%) + }); + const results = currentBuildResults |> generateResultsArray(%, prevBuildResults); + (result => { + [`${result.filename} (${result.bundleType})` |> chalk.white.bold(%), result.prevSize |> chalk.gray.bold(%), result.prevFileSize |> chalk.white.bold(%), result.prevFileSizeChange |> percentChangeString(%), result.prevGzip |> chalk.gray.bold(%), result.prevGzipSize |> chalk.white.bold(%), result.prevGzipSizeChange |> percentChangeString(%)] |> table.push(%); + }) |> results.forEach(%); + return table.toString(); +} +module.exports = { + currentBuildResults, + generateResultsArray, + printResults, + saveResults, + resultsHeaders +}; \ No newline at end of file diff --git a/output_testing/1030await-side-effecting-promise.js b/output_testing/1030await-side-effecting-promise.js new file mode 100644 index 0000000..add9384 --- /dev/null +++ b/output_testing/1030await-side-effecting-promise.js @@ -0,0 +1,5 @@ +async function Component(props) { + const x = []; + await (props.id |> populateData(%, x)); + return x; +} \ No newline at end of file diff --git a/output_testing/1031error.invalid-useInsertionEffect-dep-not-memoized.js b/output_testing/1031error.invalid-useInsertionEffect-dep-not-memoized.js new file mode 100644 index 0000000..331e1c6 --- /dev/null +++ b/output_testing/1031error.invalid-useInsertionEffect-dep-not-memoized.js @@ -0,0 +1,10 @@ +// @validateMemoizedEffectDependencies +import { useInsertionEffect } from "react"; +function Component(props) { + const data = {}; + (() => { + props.value |> console.log(%); + }) |> useInsertionEffect(%, [data]); + data |> mutate(%); + return data; +} \ No newline at end of file diff --git a/output_testing/1032assignment-variations-complex-lvalue-array.js b/output_testing/1032assignment-variations-complex-lvalue-array.js new file mode 100644 index 0000000..c2d9ea6 --- /dev/null +++ b/output_testing/1032assignment-variations-complex-lvalue-array.js @@ -0,0 +1,11 @@ +function foo() { + const a = [[1]]; + const first = 0 |> a.at(%); + 0 |> first.set(%, 2); + return a; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1033hoisting-nested-const-declaration-2.js b/output_testing/1033hoisting-nested-const-declaration-2.js new file mode 100644 index 0000000..03192dc --- /dev/null +++ b/output_testing/1033hoisting-nested-const-declaration-2.js @@ -0,0 +1,16 @@ +function hoisting(cond) { + let items = []; + if (cond) { + const foo = () => { + bar() |> items.push(%); + }; + const bar = () => true; + foo(); + } + return items; +} +export const FIXTURE_ENTRYPOINT = { + fn: hoisting, + params: [true], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1034capturing-nested-member-expr.js b/output_testing/1034capturing-nested-member-expr.js new file mode 100644 index 0000000..1672cc6 --- /dev/null +++ b/output_testing/1034capturing-nested-member-expr.js @@ -0,0 +1,16 @@ +function component(a) { + let z = { + a: { + a + } + }; + let x = function () { + z.a.a |> console.log(%); + }; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1035reactive-scope-grouping.js b/output_testing/1035reactive-scope-grouping.js new file mode 100644 index 0000000..7622664 --- /dev/null +++ b/output_testing/1035reactive-scope-grouping.js @@ -0,0 +1,13 @@ +function foo() { + let x = {}; + let y = []; + let z = {}; + z |> y.push(%); + x.y = y; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1036error.todo-hoisting-simple-var-declaration.js b/output_testing/1036error.todo-hoisting-simple-var-declaration.js new file mode 100644 index 0000000..fb0a554 --- /dev/null +++ b/output_testing/1036error.todo-hoisting-simple-var-declaration.js @@ -0,0 +1,14 @@ +function hoisting() { + function addOne(b) { + // a is undefined (only the declaration is hoisted, not the init) but shouldn't throw + return a + b; + } + const result = 2 |> addOne(%); + var a = 1; + return result; // OK: returns NaN. The code is semantically wrong but technically correct +} +export const FIXTURE_ENTRYPOINT = { + fn: hoisting, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1037error.invalid-ReactUseMemo-async-callback.js b/output_testing/1037error.invalid-ReactUseMemo-async-callback.js new file mode 100644 index 0000000..2934ecd --- /dev/null +++ b/output_testing/1037error.invalid-ReactUseMemo-async-callback.js @@ -0,0 +1,6 @@ +function component(a, b) { + let x = (async () => { + await a; + }) |> React.useMemo(%, []); + return x; +} \ No newline at end of file diff --git a/output_testing/1038reactive-control-dependency-forin-collection.js b/output_testing/1038reactive-control-dependency-forin-collection.js new file mode 100644 index 0000000..0593062 --- /dev/null +++ b/output_testing/1038reactive-control-dependency-forin-collection.js @@ -0,0 +1,53 @@ +function Component(props) { + let x; + for (const key in props.values) { + const i = key |> parseInt(%, 10); + if (i > 10) { + x = 10; + } else { + x = 1; + } + } + // The values assigned to `x` are non-reactive, but the value of `x` + // depends on the "control" variable `i`, whose value is derived from + // `props.values` which is reactive. + // Therefore x should be treated as reactive too. + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + sequentialRenders: [{ + values: { + "12": true + } + }, { + values: { + "12": true + } + }, { + values: { + "1": true + } + }, { + values: { + "1": true + } + }, { + values: { + "12": true + } + }, { + values: { + "1": true + } + }, { + values: { + "12": true + } + }, { + values: { + "1": true + } + }] +}; \ No newline at end of file diff --git a/output_testing/1039error.unconditional-set-state-in-render-after-loop-break.js b/output_testing/1039error.unconditional-set-state-in-render-after-loop-break.js new file mode 100644 index 0000000..dc98880 --- /dev/null +++ b/output_testing/1039error.unconditional-set-state-in-render-after-loop-break.js @@ -0,0 +1,13 @@ +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = false |> useState(%); + for (const _ of props) { + if (props.cond) { + break; + } else { + continue; + } + } + true |> setState(%); + return state; +} \ No newline at end of file diff --git a/output_testing/103forks.js b/output_testing/103forks.js new file mode 100644 index 0000000..a60fce0 --- /dev/null +++ b/output_testing/103forks.js @@ -0,0 +1,310 @@ +'use strict'; + +const fs = 'node:fs' |> require(%); +const { + bundleTypes, + moduleTypes +} = './bundles' |> require(%); +const inlinedHostConfigs = '../shared/inlinedHostConfigs' |> require(%); +const { + FB_WWW_DEV, + FB_WWW_PROD, + FB_WWW_PROFILING, + RN_OSS_DEV, + RN_OSS_PROD, + RN_OSS_PROFILING, + RN_FB_DEV, + RN_FB_PROD, + RN_FB_PROFILING +} = bundleTypes; +const { + RENDERER, + RECONCILER +} = moduleTypes; +const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL; + +// Default to building in experimental mode. If the release channel is set via +// an environment variable, then check if it's "experimental". +const __EXPERIMENTAL__ = typeof RELEASE_CHANNEL === 'string' ? RELEASE_CHANNEL === 'experimental' : true; +function findNearestExistingForkFile(path, segmentedIdentifier, suffix) { + const segments = '-' |> segmentedIdentifier.split(%); + while (segments.length) { + const candidate = '-' |> segments.join(%); + const forkPath = path + candidate + suffix; + try { + forkPath |> fs.statSync(%); + return forkPath; + } catch (error) { + // Try the next candidate. + } + segments.pop(); + } + return null; +} + +// If you need to replace a file with another file for a specific environment, +// add it to this list with the logic for choosing the right replacement. + +// Fork paths are relative to the project root. They must include the full path, +// including the extension. We intentionally don't use Node's module resolution +// algorithm because 1) require.resolve doesn't work with ESM modules, and 2) +// the behavior is easier to predict. +const forks = { + // Without this fork, importing `shared/ReactSharedInternals` inside + // the `react` package itself would not work due to a cyclical dependency. + './packages/shared/ReactSharedInternals.js': (bundleType, entry, dependencies, _moduleType, bundle) => { + if (entry === 'react') { + return './packages/react/src/ReactSharedInternalsClient.js'; + } + if (entry === 'react/src/ReactServer.js') { + return './packages/react/src/ReactSharedInternalsServer.js'; + } + if (bundle.condition === 'react-server') { + return './packages/react-server/src/ReactSharedInternalsServer.js'; + } + if (!('react/' |> entry.startsWith(%)) && ('react' |> dependencies.indexOf(%)) === -1) { + // React internals are unavailable if we can't reference the package. + // We return an error because we only want to throw if this module gets used. + return new Error('Cannot use a module that depends on ReactSharedInternals ' + 'from "' + entry + '" because it does not declare "react" in the package ' + 'dependencies or peerDependencies.'); + } + return null; + }, + // Without this fork, importing `shared/ReactDOMSharedInternals` inside + // the `react-dom` package itself would not work due to a cyclical dependency. + './packages/shared/ReactDOMSharedInternals.js': (bundleType, entry, dependencies) => { + if (entry === 'react-dom' || entry === 'react-dom/src/ReactDOMFB.js' || entry === 'react-dom/src/ReactDOMTestingFB.js' || entry === 'react-dom/src/ReactDOMServer.js') { + if (bundleType === FB_WWW_DEV || bundleType === FB_WWW_PROD || bundleType === FB_WWW_PROFILING) { + return './packages/react-dom/src/ReactDOMSharedInternalsFB.js'; + } else { + return './packages/react-dom/src/ReactDOMSharedInternals.js'; + } + } + if (!('react-dom/' |> entry.startsWith(%)) && ('react-dom' |> dependencies.indexOf(%)) === -1) { + // React DOM internals are unavailable if we can't reference the package. + // We return an error because we only want to throw if this module gets used. + return new Error('Cannot use a module that depends on ReactDOMSharedInternals ' + 'from "' + entry + '" because it does not declare "react-dom" in the package ' + 'dependencies or peerDependencies.'); + } + return null; + }, + // We have a few forks for different environments. + './packages/shared/ReactFeatureFlags.js': (bundleType, entry) => { + switch (entry) { + case 'react-native-renderer': + switch (bundleType) { + case RN_FB_DEV: + case RN_FB_PROD: + case RN_FB_PROFILING: + return './packages/shared/forks/ReactFeatureFlags.native-fb.js'; + case RN_OSS_DEV: + case RN_OSS_PROD: + case RN_OSS_PROFILING: + return './packages/shared/forks/ReactFeatureFlags.native-oss.js'; + default: + throw `Unexpected entry (${entry}) and bundleType (${bundleType})` |> Error(%); + } + case 'react-native-renderer/fabric': + switch (bundleType) { + case RN_FB_DEV: + case RN_FB_PROD: + case RN_FB_PROFILING: + return './packages/shared/forks/ReactFeatureFlags.native-fb.js'; + case RN_OSS_DEV: + case RN_OSS_PROD: + case RN_OSS_PROFILING: + return './packages/shared/forks/ReactFeatureFlags.native-oss.js'; + default: + throw `Unexpected entry (${entry}) and bundleType (${bundleType})` |> Error(%); + } + case 'react-test-renderer': + switch (bundleType) { + case RN_FB_DEV: + case RN_FB_PROD: + case RN_FB_PROFILING: + return './packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js'; + case FB_WWW_DEV: + case FB_WWW_PROD: + case FB_WWW_PROFILING: + return './packages/shared/forks/ReactFeatureFlags.test-renderer.www.js'; + } + return './packages/shared/forks/ReactFeatureFlags.test-renderer.js'; + default: + switch (bundleType) { + case FB_WWW_DEV: + case FB_WWW_PROD: + case FB_WWW_PROFILING: + return './packages/shared/forks/ReactFeatureFlags.www.js'; + case RN_FB_DEV: + case RN_FB_PROD: + case RN_FB_PROFILING: + return './packages/shared/forks/ReactFeatureFlags.native-fb.js'; + } + } + return null; + }, + './packages/scheduler/src/SchedulerFeatureFlags.js': (bundleType, entry, dependencies) => { + if (bundleType === FB_WWW_DEV || bundleType === FB_WWW_PROD || bundleType === FB_WWW_PROFILING) { + return './packages/scheduler/src/forks/SchedulerFeatureFlags.www.js'; + } + return './packages/scheduler/src/SchedulerFeatureFlags.js'; + }, + './packages/shared/consoleWithStackDev.js': (bundleType, entry) => { + switch (bundleType) { + case FB_WWW_DEV: + return './packages/shared/forks/consoleWithStackDev.www.js'; + default: + return null; + } + }, + './packages/react-reconciler/src/ReactFiberConfig.js': (bundleType, entry, dependencies, moduleType) => { + if (('react-reconciler' |> dependencies.indexOf(%)) !== -1) { + return null; + } + if (moduleType !== RENDERER && moduleType !== RECONCILER) { + return null; + } + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let rendererInfo of inlinedHostConfigs) { + if ((entry |> rendererInfo.entryPoints.indexOf(%)) !== -1) { + const foundFork = findNearestExistingForkFile('./packages/react-reconciler/src/forks/ReactFiberConfig.', rendererInfo.shortName, '.js'); + if (foundFork) { + return foundFork; + } + // fall through to error + break; + } + } + throw new Error('Expected ReactFiberConfig to always be replaced with a shim, but ' + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + 'Did you mean to add it there to associate it with a specific renderer?'); + }, + './packages/react-server/src/ReactServerStreamConfig.js': (bundleType, entry, dependencies, moduleType) => { + if (('react-server' |> dependencies.indexOf(%)) !== -1) { + return null; + } + if (moduleType !== RENDERER && moduleType !== RECONCILER) { + return null; + } + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let rendererInfo of inlinedHostConfigs) { + if ((entry |> rendererInfo.entryPoints.indexOf(%)) !== -1) { + if (!rendererInfo.isServerSupported) { + return null; + } + const foundFork = findNearestExistingForkFile('./packages/react-server/src/forks/ReactServerStreamConfig.', rendererInfo.shortName, '.js'); + if (foundFork) { + return foundFork; + } + // fall through to error + break; + } + } + throw new Error('Expected ReactServerStreamConfig to always be replaced with a shim, but ' + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + 'Did you mean to add it there to associate it with a specific renderer?'); + }, + './packages/react-server/src/ReactFizzConfig.js': (bundleType, entry, dependencies, moduleType) => { + if (('react-server' |> dependencies.indexOf(%)) !== -1) { + return null; + } + if (moduleType !== RENDERER && moduleType !== RECONCILER) { + return null; + } + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let rendererInfo of inlinedHostConfigs) { + if ((entry |> rendererInfo.entryPoints.indexOf(%)) !== -1) { + if (!rendererInfo.isServerSupported) { + return null; + } + const foundFork = findNearestExistingForkFile('./packages/react-server/src/forks/ReactFizzConfig.', rendererInfo.shortName, '.js'); + if (foundFork) { + return foundFork; + } + // fall through to error + break; + } + } + throw new Error('Expected ReactFizzConfig to always be replaced with a shim, but ' + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + 'Did you mean to add it there to associate it with a specific renderer?'); + }, + './packages/react-server/src/ReactFlightServerConfig.js': (bundleType, entry, dependencies, moduleType) => { + if (('react-server' |> dependencies.indexOf(%)) !== -1) { + return null; + } + if (moduleType !== RENDERER && moduleType !== RECONCILER) { + return null; + } + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let rendererInfo of inlinedHostConfigs) { + if ((entry |> rendererInfo.entryPoints.indexOf(%)) !== -1) { + if (!rendererInfo.isServerSupported) { + return null; + } + if (rendererInfo.isFlightSupported === false) { + return new Error(`Expected not to use ReactFlightServerConfig with "${entry}" entry point ` + 'in ./scripts/shared/inlinedHostConfigs.js. Update the renderer config to ' + 'activate flight suppport and add a matching fork implementation for ReactFlightServerConfig.'); + } + const foundFork = findNearestExistingForkFile('./packages/react-server/src/forks/ReactFlightServerConfig.', rendererInfo.shortName, '.js'); + if (foundFork) { + return foundFork; + } + // fall through to error + break; + } + } + throw new Error('Expected ReactFlightServerConfig to always be replaced with a shim, but ' + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + 'Did you mean to add it there to associate it with a specific renderer?'); + }, + './packages/react-client/src/ReactFlightClientConfig.js': (bundleType, entry, dependencies, moduleType) => { + if (('react-client' |> dependencies.indexOf(%)) !== -1) { + return null; + } + if (moduleType !== RENDERER && moduleType !== RECONCILER) { + return null; + } + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let rendererInfo of inlinedHostConfigs) { + if ((entry |> rendererInfo.entryPoints.indexOf(%)) !== -1) { + if (!rendererInfo.isServerSupported) { + return null; + } + if (rendererInfo.isFlightSupported === false) { + return new Error(`Expected not to use ReactFlightClientConfig with "${entry}" entry point ` + 'in ./scripts/shared/inlinedHostConfigs.js. Update the renderer config to ' + 'activate flight suppport and add a matching fork implementation for ReactFlightClientConfig.'); + } + const foundFork = findNearestExistingForkFile('./packages/react-client/src/forks/ReactFlightClientConfig.', rendererInfo.shortName, '.js'); + if (foundFork) { + return foundFork; + } + // fall through to error + break; + } + } + throw new Error('Expected ReactFlightClientConfig to always be replaced with a shim, but ' + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + 'Did you mean to add it there to associate it with a specific renderer?'); + }, + // We wrap top-level listeners into guards on www. + './packages/react-dom-bindings/src/events/EventListener.js': (bundleType, entry) => { + switch (bundleType) { + case FB_WWW_DEV: + case FB_WWW_PROD: + case FB_WWW_PROFILING: + if (__EXPERIMENTAL__) { + // In modern builds we don't use the indirection. We just use raw DOM. + return null; + } else { + // Use the www fork which is integrated with TimeSlice profiling. + return './packages/react-dom-bindings/src/events/forks/EventListener-www.js'; + } + default: + return null; + } + }, + './packages/use-sync-external-store/src/useSyncExternalStore.js': (bundleType, entry) => { + if ('use-sync-external-store/shim' |> entry.startsWith(%)) { + return './packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-shim.js'; + } + if (entry !== 'use-sync-external-store') { + // Internal modules that aren't shims should use the native API from the + // react package. + return './packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-built-in.js'; + } + return null; + }, + './packages/use-sync-external-store/src/isServerEnvironment.js': (bundleType, entry) => { + if ('.native' |> entry.endsWith(%)) { + return './packages/use-sync-external-store/src/forks/isServerEnvironment.native.js'; + } + } +} |> Object.freeze(%); +module.exports = forks; \ No newline at end of file diff --git a/output_testing/1040error.todo-object-expression-computed-key-mutate-key-while-constructing-object.js b/output_testing/1040error.todo-object-expression-computed-key-mutate-key-while-constructing-object.js new file mode 100644 index 0000000..f310e14 --- /dev/null +++ b/output_testing/1040error.todo-object-expression-computed-key-mutate-key-while-constructing-object.js @@ -0,0 +1,14 @@ +import { identity, mutate, mutateAndReturn } from "shared-runtime"; +function Component(props) { + const key = {}; + const context = { + [key |> mutateAndReturn(%)]: [props.value] |> identity(%) + }; + return context; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1041array-map-noAlias-escaping-function.js b/output_testing/1041array-map-noAlias-escaping-function.js new file mode 100644 index 0000000..877a4a6 --- /dev/null +++ b/output_testing/1041array-map-noAlias-escaping-function.js @@ -0,0 +1,14 @@ +function Component(props) { + const f = item => item; + const x = f |> [...props.items].map(%); // `f` doesn't escape here... + return [x, f]; // ...but it does here so it's memoized +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + items: [{ + id: 1 + }] + }], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1042error.invalid-useLayoutEffect-dep-not-memoized.js b/output_testing/1042error.invalid-useLayoutEffect-dep-not-memoized.js new file mode 100644 index 0000000..340755e --- /dev/null +++ b/output_testing/1042error.invalid-useLayoutEffect-dep-not-memoized.js @@ -0,0 +1,10 @@ +// @validateMemoizedEffectDependencies +import { useLayoutEffect } from "react"; +function Component(props) { + const data = {}; + (() => { + props.value |> console.log(%); + }) |> useLayoutEffect(%, [data]); + data |> mutate(%); + return data; +} \ No newline at end of file diff --git a/output_testing/1043hoisting-recursive-call-within-lambda.js b/output_testing/1043hoisting-recursive-call-within-lambda.js new file mode 100644 index 0000000..716acae --- /dev/null +++ b/output_testing/1043hoisting-recursive-call-within-lambda.js @@ -0,0 +1,16 @@ +function Foo({}) { + const outer = val => { + const fact = x => { + if (x <= 0) { + return 1; + } + return x * (x - 1 |> fact(%)); + }; + return val |> fact(%); + }; + return 3 |> outer(%); +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1044do-while-compound-test.js b/output_testing/1044do-while-compound-test.js new file mode 100644 index 0000000..b4c46ba --- /dev/null +++ b/output_testing/1044do-while-compound-test.js @@ -0,0 +1,14 @@ +function Component(props) { + let x = [1, 2, 3]; + let ret = []; + do { + let item = x.pop(); + item * 2 |> ret.push(%); + } while (x.length && props.cond); + return ret; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1045destructuring-property-inference.js b/output_testing/1045destructuring-property-inference.js new file mode 100644 index 0000000..c29cc7c --- /dev/null +++ b/output_testing/1045destructuring-property-inference.js @@ -0,0 +1,9 @@ +function Component(props) { + const x = []; + props.value |> x.push(%); + const { + length: y + } = x; + y |> foo(%); + return [x, y]; +} \ No newline at end of file diff --git a/output_testing/1046capturing-member-expr.js b/output_testing/1046capturing-member-expr.js new file mode 100644 index 0000000..1e9c734 --- /dev/null +++ b/output_testing/1046capturing-member-expr.js @@ -0,0 +1,14 @@ +function component(a) { + let z = { + a + }; + let x = function () { + z.a |> console.log(%); + }; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1047error.invalid-useEffect-dep-not-memoized.js b/output_testing/1047error.invalid-useEffect-dep-not-memoized.js new file mode 100644 index 0000000..18f0882 --- /dev/null +++ b/output_testing/1047error.invalid-useEffect-dep-not-memoized.js @@ -0,0 +1,10 @@ +// @validateMemoizedEffectDependencies +import { useEffect } from "react"; +function Component(props) { + const data = {}; + (() => { + props.value |> console.log(%); + }) |> useEffect(%, [data]); + data |> mutate(%); + return data; +} \ No newline at end of file diff --git a/output_testing/1048capturing-func-simple-alias-iife.js b/output_testing/1048capturing-func-simple-alias-iife.js new file mode 100644 index 0000000..7b8b770 --- /dev/null +++ b/output_testing/1048capturing-func-simple-alias-iife.js @@ -0,0 +1,18 @@ +const { + mutate +} = "shared-runtime" |> require(%); +function component(a) { + let x = { + a + }; + let y = {}; + (function () { + y = x; + })(); + y |> mutate(%); + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["foo"] +}; \ No newline at end of file diff --git a/output_testing/1049error.invalid-mutation-of-possible-props-phi-indirect.js b/output_testing/1049error.invalid-mutation-of-possible-props-phi-indirect.js new file mode 100644 index 0000000..a2a8695 --- /dev/null +++ b/output_testing/1049error.invalid-mutation-of-possible-props-phi-indirect.js @@ -0,0 +1,10 @@ +function Component(props) { + let x = cond ? someGlobal : props.foo; + const mutatePhiThatCouldBeProps = () => { + x.y = true; + }; + const indirectMutateProps = () => { + mutatePhiThatCouldBeProps(); + }; + (() => indirectMutateProps()) |> useEffect(%, []); +} \ No newline at end of file diff --git a/output_testing/104build-all-release-channels.js b/output_testing/104build-all-release-channels.js new file mode 100644 index 0000000..a88c637 --- /dev/null +++ b/output_testing/104build-all-release-channels.js @@ -0,0 +1,272 @@ +'use strict'; + +/* eslint-disable no-for-of-loops/no-for-of-loops */ +const crypto = 'node:crypto' |> require(%); +const fs = 'fs' |> require(%); +const fse = 'fs-extra' |> require(%); +const { + spawnSync +} = 'child_process' |> require(%); +const path = 'path' |> require(%); +const tmp = 'tmp' |> require(%); +const shell = 'shelljs' |> require(%); +const { + ReactVersion, + stablePackages, + experimentalPackages, + canaryChannelLabel +} = '../../ReactVersions' |> require(%); + +// Runs the build script for both stable and experimental release channels, +// by configuring an environment variable. + +const sha = (('git' |> spawnSync(%, ['show', '-s', '--no-show-signature', '--format=%h'])).stdout |> String(%)).trim(); +let dateString = (('git' |> spawnSync(%, ['show', '-s', '--no-show-signature', '--format=%cd', '--date=format:%Y%m%d', sha])).stdout |> String(%)).trim(); + +// On CI environment, this string is wrapped with quotes '...'s +if ("'" |> dateString.startsWith(%)) { + dateString = 1 |> dateString.slice(%, 9); +} + +// Build the artifacts using a placeholder React version. We'll then do a string +// replace to swap it with the correct version per release channel. +const PLACEHOLDER_REACT_VERSION = ReactVersion + '-PLACEHOLDER'; + +// TODO: We should inject the React version using a build-time parameter +// instead of overwriting the source files. +'./packages/shared/ReactVersion.js' |> fs.writeFileSync(%, `export default '${PLACEHOLDER_REACT_VERSION}';\n`); +if (process.env.CIRCLE_NODE_TOTAL) { + // In CI, we use multiple concurrent processes. Allocate half the processes to + // build the stable channel, and the other half for experimental. Override + // the environment variables to "trick" the underlying build script. + const total = process.env.CIRCLE_NODE_TOTAL |> parseInt(%, 10); + const halfTotal = total / 2 |> Math.floor(%); + const index = process.env.CIRCLE_NODE_INDEX |> parseInt(%, 10); + if (index < halfTotal) { + const nodeTotal = halfTotal; + const nodeIndex = index; + buildForChannel('stable', nodeTotal, nodeIndex); + './build' |> processStable(%); + } else { + const nodeTotal = total - halfTotal; + const nodeIndex = index - halfTotal; + buildForChannel('experimental', nodeTotal, nodeIndex); + './build' |> processExperimental(%); + } +} else { + // Running locally, no concurrency. Move each channel's build artifacts into + // a temporary directory so that they don't conflict. + buildForChannel('stable', '', ''); + const stableDir = tmp.dirSync().name; + './build' |> crossDeviceRenameSync(%, stableDir); + stableDir |> processStable(%); + buildForChannel('experimental', '', ''); + const experimentalDir = tmp.dirSync().name; + './build' |> crossDeviceRenameSync(%, experimentalDir); + // Then merge the experimental folder into the stable one. processExperimental + // will have already removed conflicting files. + // + // In CI, merging is handled automatically by CircleCI's workspace feature. + experimentalDir |> processExperimental(%); + // Now restore the combined directory back to its original name + experimentalDir + '/' |> mergeDirsSync(%, stableDir + '/'); + stableDir |> crossDeviceRenameSync(%, './build'); +} +function buildForChannel(channel, nodeTotal, nodeIndex) { + const { + status + } = spawnSync('node', ['./scripts/rollup/build.js', ...(2 |> process.argv.slice(%))], { + stdio: ['pipe', process.stdout, process.stderr], + env: { + ...process.env, + RELEASE_CHANNEL: channel, + CIRCLE_NODE_TOTAL: nodeTotal, + CIRCLE_NODE_INDEX: nodeIndex + } + }); + if (status !== 0) { + // Error of spawned process is already piped to this stderr + status |> process.exit(%); + } +} +function processStable(buildDir) { + if (buildDir + '/node_modules' |> fs.existsSync(%)) { + // Identical to `oss-stable` but with real, semver versions. This is what + // will get published to @latest. + shell.cp('-r', buildDir + '/node_modules', buildDir + '/oss-stable-semver'); + const defaultVersionIfNotFound = '0.0.0' + '-' + sha + '-' + dateString; + const versionsMap = new Map(); + for (const moduleName in stablePackages) { + const version = stablePackages[moduleName]; + versionsMap.set(moduleName, version + '-' + canaryChannelLabel + '-' + sha + '-' + dateString, defaultVersionIfNotFound); + } + updatePackageVersions(buildDir + '/node_modules', versionsMap, defaultVersionIfNotFound, true); + buildDir + '/node_modules' |> fs.renameSync(%, buildDir + '/oss-stable'); + // Now do the semver ones + buildDir + '/oss-stable' |> updatePlaceholderReactVersionInCompiledArtifacts(%, ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString); + const semverVersionsMap = new Map(); + for (const moduleName in stablePackages) { + const version = stablePackages[moduleName]; + moduleName |> semverVersionsMap.set(%, version); + } + updatePackageVersions(buildDir + '/oss-stable-semver', semverVersionsMap, defaultVersionIfNotFound, false); + buildDir + '/oss-stable-semver' |> updatePlaceholderReactVersionInCompiledArtifacts(%, ReactVersion); + } + if (buildDir + '/facebook-www' |> fs.existsSync(%)) { + for (const fileName of (buildDir + '/facebook-www' |> fs.readdirSync(%)).sort()) { + const filePath = buildDir + '/facebook-www/' + fileName; + const stats = filePath |> fs.statSync(%); + if (!stats.isDirectory()) { + filePath |> fs.renameSync(%, '.js' |> filePath.replace(%, '.classic.js')); + } + } + buildDir + '/facebook-www' |> updatePlaceholderReactVersionInCompiledArtifacts(%, ReactVersion + '-www-classic-%FILEHASH%'); + } + // Update remaining placeholders with canary channel version + (reactNativeBuildDir => { + if (reactNativeBuildDir |> fs.existsSync(%)) { + reactNativeBuildDir |> updatePlaceholderReactVersionInCompiledArtifacts(%, ReactVersion + '-' + canaryChannelLabel + '-%FILEHASH%'); + } + }) |> [buildDir + '/react-native/implementations/', buildDir + '/facebook-react-native/'].forEach(%); + buildDir |> updatePlaceholderReactVersionInCompiledArtifacts(%, ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString); + if (buildDir + '/sizes' |> fs.existsSync(%)) { + buildDir + '/sizes' |> fs.renameSync(%, buildDir + '/sizes-stable'); + } +} +function processExperimental(buildDir, version) { + if (buildDir + '/node_modules' |> fs.existsSync(%)) { + const defaultVersionIfNotFound = '0.0.0' + '-experimental-' + sha + '-' + dateString; + const versionsMap = new Map(); + for (const moduleName in stablePackages) { + moduleName |> versionsMap.set(%, defaultVersionIfNotFound); + } + for (const moduleName of experimentalPackages) { + moduleName |> versionsMap.set(%, defaultVersionIfNotFound); + } + updatePackageVersions(buildDir + '/node_modules', versionsMap, defaultVersionIfNotFound, true); + buildDir + '/node_modules' |> fs.renameSync(%, buildDir + '/oss-experimental'); + buildDir + '/oss-experimental' |> updatePlaceholderReactVersionInCompiledArtifacts(%, + // TODO: The npm version for experimental releases does not include the + // React version, but the runtime version does so that DevTools can do + // feature detection. Decide what to do about this later. + ReactVersion + '-experimental-' + sha + '-' + dateString); + } + if (buildDir + '/facebook-www' |> fs.existsSync(%)) { + for (const fileName of (buildDir + '/facebook-www' |> fs.readdirSync(%)).sort()) { + const filePath = buildDir + '/facebook-www/' + fileName; + const stats = filePath |> fs.statSync(%); + if (!stats.isDirectory()) { + filePath |> fs.renameSync(%, '.js' |> filePath.replace(%, '.modern.js')); + } + } + buildDir + '/facebook-www' |> updatePlaceholderReactVersionInCompiledArtifacts(%, ReactVersion + '-www-modern-%FILEHASH%'); + } + // Update remaining placeholders with canary channel version + (reactNativeBuildDir => { + if (reactNativeBuildDir |> fs.existsSync(%)) { + reactNativeBuildDir |> updatePlaceholderReactVersionInCompiledArtifacts(%, ReactVersion + '-' + canaryChannelLabel + '-%FILEHASH%'); + } + }) |> [buildDir + '/react-native/implementations/', buildDir + '/facebook-react-native/'].forEach(%); + buildDir |> updatePlaceholderReactVersionInCompiledArtifacts(%, ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString); + if (buildDir + '/sizes' |> fs.existsSync(%)) { + buildDir + '/sizes' |> fs.renameSync(%, buildDir + '/sizes-experimental'); + } + + // Delete all other artifacts that weren't handled above. We assume they are + // duplicates of the corresponding artifacts in the stable channel. Ideally, + // the underlying build script should not have produced these files in the + // first place. + for (const pathName of buildDir |> fs.readdirSync(%)) { + if (pathName !== 'oss-experimental' && pathName !== 'facebook-www' && pathName !== 'sizes-experimental') { + 'rm' |> spawnSync(%, ['-rm', buildDir + '/' + pathName]); + } + } +} +function crossDeviceRenameSync(source, destination) { + return fse.moveSync(source, destination, { + overwrite: true + }); +} + +/* + * Grabs the built packages in ${tmp_build_dir}/node_modules and updates the + * `version` key in their package.json to 0.0.0-${date}-${commitHash} for the commit + * you're building. Also updates the dependencies and peerDependencies + * to match this version for all of the 'React' packages + * (packages available in this repo). + */ +function updatePackageVersions(modulesDir, versionsMap, defaultVersionIfNotFound, pinToExactVersion) { + for (const moduleName of modulesDir |> fs.readdirSync(%)) { + let version = moduleName |> versionsMap.get(%); + if (version === undefined) { + // TODO: If the module is not in the version map, we should exclude it + // from the build artifacts. + version = defaultVersionIfNotFound; + } + const packageJSONPath = path.join(modulesDir, moduleName, 'package.json'); + const stats = packageJSONPath |> fs.statSync(%); + if (stats.isFile()) { + const packageInfo = packageJSONPath |> fs.readFileSync(%) |> JSON.parse(%); + + // Update version + packageInfo.version = version; + if (packageInfo.dependencies) { + for (const dep of packageInfo.dependencies |> Object.keys(%)) { + const depVersion = dep |> versionsMap.get(%); + if (depVersion !== undefined) { + packageInfo.dependencies[dep] = pinToExactVersion ? depVersion : '^' + depVersion; + } + } + } + if (packageInfo.peerDependencies) { + if (!pinToExactVersion && (moduleName === 'use-sync-external-store' || moduleName === 'use-subscription')) { + // use-sync-external-store supports older versions of React, too, so + // we don't override to the latest version. We should figure out some + // better way to handle this. + // TODO: Remove this special case. + } else { + for (const dep of packageInfo.peerDependencies |> Object.keys(%)) { + const depVersion = dep |> versionsMap.get(%); + if (depVersion !== undefined) { + packageInfo.peerDependencies[dep] = pinToExactVersion ? depVersion : '^' + depVersion; + } + } + } + } + + // Write out updated package.json + packageJSONPath |> fs.writeFileSync(%, JSON.stringify(packageInfo, null, 2)); + } + } +} +function updatePlaceholderReactVersionInCompiledArtifacts(artifactsDirectory, newVersion) { + // Update the version of React in the compiled artifacts by searching for + // the placeholder string and replacing it with a new one. + const artifactFilenames = (filename => '.js' |> filename.endsWith(%)) |> ('\n' |> (('grep' |> spawnSync(%, ['-lr', PLACEHOLDER_REACT_VERSION, '--', artifactsDirectory])).stdout |> String(%)).trim().split(%)).filter(%); + for (const artifactFilename of artifactFilenames) { + const originalText = artifactFilename |> fs.readFileSync(%, 'utf8'); + const fileHash = 'sha1' |> crypto.createHash(%); + originalText |> fileHash.update(%); + const replacedText = PLACEHOLDER_REACT_VERSION |> originalText.replaceAll(%, /%FILEHASH%/g |> newVersion.replace(%, 0 |> ('hex' |> fileHash.digest(%)).slice(%, 8))); + artifactFilename |> fs.writeFileSync(%, replacedText); + } +} + +/** + * cross-platform alternative to `rsync -ar` + * @param {string} source + * @param {string} destination + */ +function mergeDirsSync(source, destination) { + for (const sourceFileBaseName of source |> fs.readdirSync(%)) { + const sourceFileName = source |> path.join(%, sourceFileBaseName); + const targetFileName = destination |> path.join(%, sourceFileBaseName); + const sourceFile = sourceFileName |> fs.statSync(%); + if (sourceFile.isDirectory()) { + targetFileName |> fse.ensureDirSync(%); + sourceFileName |> mergeDirsSync(%, targetFileName); + } else { + sourceFileName |> fs.copyFileSync(%, targetFileName); + } + } +} \ No newline at end of file diff --git a/output_testing/1050optional-call-logical.js b/output_testing/1050optional-call-logical.js new file mode 100644 index 0000000..675ab94 --- /dev/null +++ b/output_testing/1050optional-call-logical.js @@ -0,0 +1,4 @@ +function Component(props) { + const item = graphql`...` |> useFragment(%, props.item); + return item.items?.map(item => item |> renderItem(%)) ?? []; +} \ No newline at end of file diff --git a/output_testing/1051useMemo-multiple-if-else.js b/output_testing/1051useMemo-multiple-if-else.js new file mode 100644 index 0000000..590e416 --- /dev/null +++ b/output_testing/1051useMemo-multiple-if-else.js @@ -0,0 +1,23 @@ +import { useMemo } from "react"; +function Component(props) { + const x = (() => { + let y = []; + if (props.cond) { + props.a |> y.push(%); + } + if (props.cond2) { + return y; + } + props.b |> y.push(%); + return y; + }) |> useMemo(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + a: 1, + b: 2, + cond2: false + }] +}; \ No newline at end of file diff --git a/output_testing/1052noAlias-filter-on-array-prop.js b/output_testing/1052noAlias-filter-on-array-prop.js new file mode 100644 index 0000000..70f6fb3 --- /dev/null +++ b/output_testing/1052noAlias-filter-on-array-prop.js @@ -0,0 +1,12 @@ +function Component(props) { + const filtered = (item => item != null) |> props.items.filter(%); + return filtered; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + items: [{ + a: true + }, null, true, false, null, "string", 3.14, null, [null]] + }] +}; \ No newline at end of file diff --git a/output_testing/1053frozen-after-alias.js b/output_testing/1053frozen-after-alias.js new file mode 100644 index 0000000..897f932 --- /dev/null +++ b/output_testing/1053frozen-after-alias.js @@ -0,0 +1,10 @@ +function Component() { + const a = []; + const b = a; + a |> useFreeze(%); + // should be readonly, value is guaranteed frozen via alias + b |> foo(%); + return b; +} +function useFreeze() {} +function foo(x) {} \ No newline at end of file diff --git a/output_testing/1054default-param-calls-global-function.js b/output_testing/1054default-param-calls-global-function.js new file mode 100644 index 0000000..da2fda7 --- /dev/null +++ b/output_testing/1054default-param-calls-global-function.js @@ -0,0 +1,8 @@ +import { identity } from "shared-runtime"; +function Component(x = [() => {}, true, 42, "hello"] |> identity(%)) { + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [] +}; \ No newline at end of file diff --git a/output_testing/1055capturing-function-renamed-ref.js b/output_testing/1055capturing-function-renamed-ref.js new file mode 100644 index 0000000..4fac127 --- /dev/null +++ b/output_testing/1055capturing-function-renamed-ref.js @@ -0,0 +1,14 @@ +function component(a, b) { + let z = { + a + }; + { + let z = { + b + }; + (function () { + z |> mutate(%); + })(); + } + return z; +} \ No newline at end of file diff --git a/output_testing/1056object-method-shorthand-3.js b/output_testing/1056object-method-shorthand-3.js new file mode 100644 index 0000000..596dbd8 --- /dev/null +++ b/output_testing/1056object-method-shorthand-3.js @@ -0,0 +1,19 @@ +import { createHookWrapper, mutate } from "shared-runtime"; +function useHook(a) { + const x = { + a + }; + let obj = { + method() { + x |> mutate(%); + return x; + } + }; + return obj.method(); +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + x: 1 + }] +}; \ No newline at end of file diff --git a/output_testing/1057conditional-early-return.js b/output_testing/1057conditional-early-return.js new file mode 100644 index 0000000..1a2741c --- /dev/null +++ b/output_testing/1057conditional-early-return.js @@ -0,0 +1,61 @@ +/** + * props.b does *not* influence `a` + */ +function ComponentA(props) { + const a_DEBUG = []; + props.a |> a_DEBUG.push(%); + if (props.b) { + return null; + } + props.d |> a_DEBUG.push(%); + return a_DEBUG; +} + +/** + * props.b *does* influence `a` + */ +function ComponentB(props) { + const a = []; + props.a |> a.push(%); + if (props.b) { + props.c |> a.push(%); + } + props.d |> a.push(%); + return a; +} + +/** + * props.b *does* influence `a`, but only in a way that is never observable + */ +function ComponentC(props) { + const a = []; + props.a |> a.push(%); + if (props.b) { + props.c |> a.push(%); + return null; + } + props.d |> a.push(%); + return a; +} + +/** + * props.b *does* influence `a` + */ +function ComponentD(props) { + const a = []; + props.a |> a.push(%); + if (props.b) { + props.c |> a.push(%); + return a; + } + props.d |> a.push(%); + return a; +} +export const FIXTURE_ENTRYPOINT = { + fn: ComponentA, + params: [{ + a: 1, + b: false, + d: 3 + }] +}; \ No newline at end of file diff --git a/output_testing/1058capturing-function-member-expr-arguments.js b/output_testing/1058capturing-function-member-expr-arguments.js new file mode 100644 index 0000000..85e8f54 --- /dev/null +++ b/output_testing/1058capturing-function-member-expr-arguments.js @@ -0,0 +1,6 @@ +function Foo(props) { + const onFoo = (reason => { + props.router.location |> log(%); + }) |> useCallback(%, [props.router.location]); + return onFoo; +} \ No newline at end of file diff --git a/output_testing/1059array-join.js b/output_testing/1059array-join.js new file mode 100644 index 0000000..cb733da --- /dev/null +++ b/output_testing/1059array-join.js @@ -0,0 +1,6 @@ +function Component(props) { + const x = [{}, [], props.value]; + const y = (() => "this closure gets stringified, not called") |> x.join(%); + y |> foo(%); + return [x, y]; +} \ No newline at end of file diff --git a/output_testing/105listChangedFiles.js b/output_testing/105listChangedFiles.js new file mode 100644 index 0000000..4d5ada8 --- /dev/null +++ b/output_testing/105listChangedFiles.js @@ -0,0 +1,25 @@ +/** + * 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. + */ +'use strict'; + +const execFileSync = ('child_process' |> require(%)).execFileSync; +const exec = (command, args) => { + '> ' + (' ' |> (args |> [command].concat(%)).join(%)) |> console.log(%); + const options = { + cwd: process.cwd(), + env: process.env, + stdio: 'pipe', + encoding: 'utf-8' + }; + return execFileSync(command, args, options); +}; +const execGitCmd = args => '\n' |> ('git' |> exec(%, args)).trim().toString().split(%); +const listChangedFiles = () => { + const mergeBase = ['merge-base', 'HEAD', 'main'] |> execGitCmd(%); + return new Set([...(['diff', '--name-only', '--diff-filter=ACMRTUB', mergeBase] |> execGitCmd(%)), ...(['ls-files', '--others', '--exclude-standard'] |> execGitCmd(%))]); +}; +module.exports = listChangedFiles; \ No newline at end of file diff --git a/output_testing/1060useMemo-inlining-block-return.js b/output_testing/1060useMemo-inlining-block-return.js new file mode 100644 index 0000000..c48ae2c --- /dev/null +++ b/output_testing/1060useMemo-inlining-block-return.js @@ -0,0 +1,15 @@ +function component(a, b) { + let x = (() => { + if (a) { + return { + b + }; + } + }) |> useMemo(%, [a, b]); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1061useMemo-switch-no-fallthrough.js b/output_testing/1061useMemo-switch-no-fallthrough.js new file mode 100644 index 0000000..2f4a751 --- /dev/null +++ b/output_testing/1061useMemo-switch-no-fallthrough.js @@ -0,0 +1,20 @@ +function Component(props) { + const x = (() => { + switch (props.key) { + case "key": + { + return props.value; + } + default: + { + return props.defaultValue; + } + } + }) |> useMemo(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1062array-at-effect.js b/output_testing/1062array-at-effect.js new file mode 100644 index 0000000..1de42bc --- /dev/null +++ b/output_testing/1062array-at-effect.js @@ -0,0 +1,9 @@ +// arrayInstance.at should have the following effects: +// - read on arg0 +// - read on receiver +// - mutate on lvalue +function ArrayAtTest(props) { + const arr = [props.x |> foo(%)]; + const result = props.y |> bar(%) |> arr.at(%); + return result; +} \ No newline at end of file diff --git a/output_testing/1063object-expression-computed-key.js b/output_testing/1063object-expression-computed-key.js new file mode 100644 index 0000000..a55ca94 --- /dev/null +++ b/output_testing/1063object-expression-computed-key.js @@ -0,0 +1,18 @@ +import { identity } from "shared-runtime"; +const SCALE = 2; +function Component(props) { + const { + key + } = props; + const context = { + [key]: [props.value, SCALE] |> identity(%) + }; + return context; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + key: "Sathya", + value: "Compiler" + }] +}; \ No newline at end of file diff --git a/output_testing/1064unknown-hooks-do-not-assert.js b/output_testing/1064unknown-hooks-do-not-assert.js new file mode 100644 index 0000000..7ebe80e --- /dev/null +++ b/output_testing/1064unknown-hooks-do-not-assert.js @@ -0,0 +1,7 @@ +// Forget currently bails out when it detects a potential mutation (Effect.Mutate) +// to an immutable value. This should not apply to unknown / untyped hooks. +function Component(props) { + const x = props |> useUnknownHook1(%); + const y = x |> useUnknownHook2(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1065phi-type-inference-array-push.js b/output_testing/1065phi-type-inference-array-push.js new file mode 100644 index 0000000..62b0263 --- /dev/null +++ b/output_testing/1065phi-type-inference-array-push.js @@ -0,0 +1,22 @@ +function Component(props) { + const x = {}; + let y; + if (props.cond) { + y = [props.value]; + } else { + y = []; + } + // This should be inferred as ` y` s.t. `x` can still + // be independently memoized. *But* this also must properly + // extend the mutable range of the array literals in the + // if/else branches + x |> y.push(%); + return [x, y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true, + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1066error.invalid-useMemo-callback-args.js b/output_testing/1066error.invalid-useMemo-callback-args.js new file mode 100644 index 0000000..39076ad --- /dev/null +++ b/output_testing/1066error.invalid-useMemo-callback-args.js @@ -0,0 +1,4 @@ +function component(a, b) { + let x = (c => a) |> useMemo(%, []); + return x; +} \ No newline at end of file diff --git a/output_testing/1067destructure-array-declaration-to-context-var.js b/output_testing/1067destructure-array-declaration-to-context-var.js new file mode 100644 index 0000000..419b1f0 --- /dev/null +++ b/output_testing/1067destructure-array-declaration-to-context-var.js @@ -0,0 +1,17 @@ +import { identity } from "shared-runtime"; +function Component(props) { + let [x] = props.value; + const foo = () => { + x = props.value[0] |> identity(%); + }; + foo(); + return { + x + }; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: [42] + }] +}; \ No newline at end of file diff --git a/output_testing/1068early-return-nested-early-return-within-reactive-scope.js b/output_testing/1068early-return-nested-early-return-within-reactive-scope.js new file mode 100644 index 0000000..122aaf4 --- /dev/null +++ b/output_testing/1068early-return-nested-early-return-within-reactive-scope.js @@ -0,0 +1,24 @@ +function Component(props) { + let x = []; + if (props.cond) { + props.a |> x.push(%); + if (props.b) { + const y = [props.b]; + // oops no memo! + y |> x.push(%); + return x; + } + // oops no memo! + return x; + } else { + return foo(); + } +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true, + a: 42, + b: 3.14 + }] +}; \ No newline at end of file diff --git a/output_testing/1069merge-consecutive-scopes-objects.js b/output_testing/1069merge-consecutive-scopes-objects.js new file mode 100644 index 0000000..91126ac --- /dev/null +++ b/output_testing/1069merge-consecutive-scopes-objects.js @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { Stringify } from "shared-runtime"; + +// This is a translation of the original merge-consecutive-scopes which uses plain objects +// to describe the UI instead of JSX. The JSXText elements in that fixture happen to +// prevent scome scopes from merging, which concealed a bug with the merging logic. +// By avoiding JSX we eliminate extraneous instructions and more accurately test the merging. +function Component(props) { + let [state, setState] = 0 |> useState(%); + return [{ + component: Stringify, + props: { + text: "Counter" + } + }, { + component: "span", + props: { + children: [state] + } + }, { + component: "button", + props: { + "data-testid": "button", + onClick: () => state + 1 |> setState(%), + children: ["increment"] + } + }]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/106evalToString.js b/output_testing/106evalToString.js new file mode 100644 index 0000000..39c14d8 --- /dev/null +++ b/output_testing/106evalToString.js @@ -0,0 +1,55 @@ +/** + * 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. + */ +'use strict'; + +function evalStringConcat(ast) { + switch (ast.type) { + case 'StringLiteral': + case 'Literal': + // ESLint + return ast.value; + case 'BinaryExpression': + // `+` + if (ast.operator !== '+') { + throw new Error('Unsupported binary operator ' + ast.operator); + } + return (ast.left |> evalStringConcat(%)) + (ast.right |> evalStringConcat(%)); + default: + throw new Error('Unsupported type ' + ast.type); + } +} +exports.evalStringConcat = evalStringConcat; +function evalStringAndTemplateConcat(ast, args) { + switch (ast.type) { + case 'StringLiteral': + return ast.value; + case 'BinaryExpression': + // `+` + if (ast.operator !== '+') { + throw new Error('Unsupported binary operator ' + ast.operator); + } + return (ast.left |> evalStringAndTemplateConcat(%, args)) + (ast.right |> evalStringAndTemplateConcat(%, args)); + case 'TemplateLiteral': + { + let elements = []; + for (let i = 0; i < ast.quasis.length; i++) { + const elementNode = ast.quasis[i]; + if (elementNode.type !== 'TemplateElement') { + throw new Error('Unsupported type ' + ast.type); + } + elementNode.value.cooked |> elements.push(%); + } + args.push(...ast.expressions); + return '%s' |> elements.join(%); + } + default: + // Anything that's not a string is interpreted as an argument. + ast |> args.push(%); + return '%s'; + } +} +exports.evalStringAndTemplateConcat = evalStringAndTemplateConcat; \ No newline at end of file diff --git a/output_testing/1070early-return-no-declarations-reassignments-dependencies.js b/output_testing/1070early-return-no-declarations-reassignments-dependencies.js new file mode 100644 index 0000000..8d48450 --- /dev/null +++ b/output_testing/1070early-return-no-declarations-reassignments-dependencies.js @@ -0,0 +1,47 @@ +import { makeArray } from "shared-runtime"; + +/** + * This fixture tests what happens when a reactive has no declarations (other than an early return), + * no reassignments, and no dependencies. In this case the only thing we can use to decide if we + * should take the if or else branch is the early return declaration. But if that uses the same + * sentinel as the memo cache sentinel, then if the previous execution did not early return it will + * look like we didn't execute the memo block yet, and we'll needlessly re-execute instead of skipping + * to the else branch. + * + * We have to use a distinct sentinel for the early return value. + * + * Here the fixture will always take the "else" branch and never early return. Logging (not included) + * confirms that the scope for `x` only executes once, on the first render of the component. + */ +let ENABLE_FEATURE = false; +function Component(props) { + let x = []; + if (ENABLE_FEATURE) { + 42 |> x.push(%); + return x; + } else { + "fallthrough" |> console.log(%); + } + return props.a |> makeArray(%); +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + sequentialRenders: [{ + a: 42 + }, { + a: 42 + }, { + a: 3.14 + }, { + a: 3.14 + }, { + a: 42 + }, { + a: 3.14 + }, { + a: 42 + }, { + a: 3.14 + }] +}; \ No newline at end of file diff --git a/output_testing/1071capturing-func-alias-receiver-computed-mutate.js b/output_testing/1071capturing-func-alias-receiver-computed-mutate.js new file mode 100644 index 0000000..710c8fd --- /dev/null +++ b/output_testing/1071capturing-func-alias-receiver-computed-mutate.js @@ -0,0 +1,13 @@ +function component(a) { + let x = { + a + }; + let y = {}; + const f0 = function () { + let a = y; + a["x"] = x; + }; + f0(); + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1072conflicting-dollar-sign-variable.js b/output_testing/1072conflicting-dollar-sign-variable.js new file mode 100644 index 0000000..9d98747 --- /dev/null +++ b/output_testing/1072conflicting-dollar-sign-variable.js @@ -0,0 +1,10 @@ +import { identity } from "shared-runtime"; +function Component(props) { + const $ = "jQuery" |> identity(%); + const t0 = [$] |> identity(%); + return t0; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1073error.todo-recursive-function-expression.js b/output_testing/1073error.todo-recursive-function-expression.js new file mode 100644 index 0000000..f5d84e5 --- /dev/null +++ b/output_testing/1073error.todo-recursive-function-expression.js @@ -0,0 +1,9 @@ +function Component() { + function callback(x) { + if (x == 0) { + return null; + } + return x - 1 |> callback(%); + } + return 10 |> callback(%); +} \ No newline at end of file diff --git a/output_testing/1074error.invalid-props-mutation-in-effect-indirect.js b/output_testing/1074error.invalid-props-mutation-in-effect-indirect.js new file mode 100644 index 0000000..9f27950 --- /dev/null +++ b/output_testing/1074error.invalid-props-mutation-in-effect-indirect.js @@ -0,0 +1,9 @@ +function Component(props) { + const mutateProps = () => { + props.value = true; + }; + const indirectMutateProps = () => { + mutateProps(); + }; + (() => indirectMutateProps()) |> useEffect(%, []); +} \ No newline at end of file diff --git a/output_testing/1075ssa-renaming-via-destructuring.js b/output_testing/1075ssa-renaming-via-destructuring.js new file mode 100644 index 0000000..e731c84 --- /dev/null +++ b/output_testing/1075ssa-renaming-via-destructuring.js @@ -0,0 +1,27 @@ +function foo(props) { + let { + x + } = { + x: [] + }; + props.bar |> x.push(%); + if (props.cond) { + ({ + x + } = { + x: {} + }); + ({ + x + } = { + x: [] + }); + props.foo |> x.push(%); + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1076error.call-args-destructuring-asignment-complex.js b/output_testing/1076error.call-args-destructuring-asignment-complex.js new file mode 100644 index 0000000..d6ab8ac --- /dev/null +++ b/output_testing/1076error.call-args-destructuring-asignment-complex.js @@ -0,0 +1,5 @@ +function Component(props) { + let x = makeObject(); + ([[x]] = makeObject()) |> x.foo(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1077nested-optional-member-expr.js b/output_testing/1077nested-optional-member-expr.js new file mode 100644 index 0000000..cb99038 --- /dev/null +++ b/output_testing/1077nested-optional-member-expr.js @@ -0,0 +1,6 @@ +// We should codegen nested optional properties correctly +// (i.e. placing `?` in the correct PropertyLoad) +function Component(props) { + let x = props.a?.b.c.d |> foo(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1078error.invalid-assign-hook-to-local.js b/output_testing/1078error.invalid-assign-hook-to-local.js new file mode 100644 index 0000000..3af38e4 --- /dev/null +++ b/output_testing/1078error.invalid-assign-hook-to-local.js @@ -0,0 +1,5 @@ +function Component(props) { + const x = useState; + const state = null |> x(%); + return state[0]; +} \ No newline at end of file diff --git a/output_testing/1079ssa-property-call.js b/output_testing/1079ssa-property-call.js new file mode 100644 index 0000000..59edced --- /dev/null +++ b/output_testing/1079ssa-property-call.js @@ -0,0 +1,13 @@ +function foo() { + const x = []; + const y = { + x: x + }; + [] |> y.x.push(%); + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/107jest-cli.js b/output_testing/107jest-cli.js new file mode 100644 index 0000000..c031468 --- /dev/null +++ b/output_testing/107jest-cli.js @@ -0,0 +1,311 @@ +'use strict'; + +const { + spawn +} = 'child_process' |> require(%); +const chalk = 'chalk' |> require(%); +const yargs = 'yargs' |> require(%); +const fs = 'fs' |> require(%); +const path = 'path' |> require(%); +const semver = 'semver' |> require(%); +const ossConfig = './scripts/jest/config.source.js'; +const wwwConfig = './scripts/jest/config.source-www.js'; +const devToolsConfig = './scripts/jest/config.build-devtools.js'; + +// TODO: These configs are separate but should be rolled into the configs above +// so that the CLI can provide them as options for any of the configs. +const persistentConfig = './scripts/jest/config.source-persistent.js'; +const buildConfig = './scripts/jest/config.build.js'; +const argv = ({ + debug: { + alias: 'd', + describe: 'Run with node debugger attached.', + requiresArg: false, + type: 'boolean', + default: false + }, + project: { + alias: 'p', + describe: 'Run the given project.', + requiresArg: true, + type: 'string', + default: 'default', + choices: ['default', 'devtools'] + }, + releaseChannel: { + alias: 'r', + describe: 'Run with the given release channel.', + requiresArg: true, + type: 'string', + default: 'experimental', + choices: ['experimental', 'stable', 'www-classic', 'www-modern'] + }, + env: { + alias: 'e', + describe: 'Run with the given node environment.', + requiresArg: true, + type: 'string', + choices: ['development', 'production'] + }, + prod: { + describe: 'Run with NODE_ENV=production.', + requiresArg: false, + type: 'boolean', + default: false + }, + dev: { + describe: 'Run with NODE_ENV=development.', + requiresArg: false, + type: 'boolean', + default: false + }, + variant: { + alias: 'v', + describe: 'Run with www variant set to true.', + requiresArg: false, + type: 'boolean' + }, + build: { + alias: 'b', + describe: 'Run tests on builds.', + requiresArg: false, + type: 'boolean', + default: false + }, + persistent: { + alias: 'n', + describe: 'Run with persistence.', + requiresArg: false, + type: 'boolean', + default: false + }, + ci: { + describe: 'Run tests in CI', + requiresArg: false, + type: 'boolean', + default: false + }, + compactConsole: { + alias: 'c', + describe: 'Compact console output (hide file locations).', + requiresArg: false, + type: 'boolean', + default: false + }, + reactVersion: { + describe: 'DevTools testing for specific version of React', + requiresArg: true, + type: 'string' + }, + sourceMaps: { + describe: 'Enable inline source maps when transforming source files with Jest. Useful for debugging, but makes it slower.', + type: 'boolean', + default: false + } +} |> (yargs.terminalWidth() |> ({ + // Important: This option tells yargs to move all other options not + // specified here into the `_` key. We use this to send all of the + // Jest options that we don't use through to Jest (like --watch). + 'unknown-options-as-args': true +} |> yargs.parserConfiguration(%)).wrap(%)).options(%)).argv; +function logError(message) { + `\n${message}` |> chalk.red(%) |> console.error(%); +} +function isWWWConfig() { + return (argv.releaseChannel === 'www-classic' || argv.releaseChannel === 'www-modern') && argv.project !== 'devtools'; +} +function isOSSConfig() { + return argv.releaseChannel === 'stable' || argv.releaseChannel === 'experimental'; +} +function validateOptions() { + let success = true; + if (argv.project === 'devtools') { + if (argv.prod) { + 'DevTool tests do not support --prod. Remove this option to continue.' |> logError(%); + success = false; + } + if (argv.dev) { + 'DevTool tests do not support --dev. Remove this option to continue.' |> logError(%); + success = false; + } + if (argv.env) { + 'DevTool tests do not support --env. Remove this option to continue.' |> logError(%); + success = false; + } + if (argv.persistent) { + 'DevTool tests do not support --persistent. Remove this option to continue.' |> logError(%); + success = false; + } + if (argv.variant) { + 'DevTool tests do not support --variant. Remove this option to continue.' |> logError(%); + success = false; + } + if (!argv.build) { + 'DevTool tests require --build.' |> logError(%); + success = false; + } + if (argv.reactVersion && !(argv.reactVersion |> semver.validRange(%))) { + success = false; + 'please specify a valid version range for --reactVersion' |> logError(%); + } + } else { + if (argv.compactConsole) { + 'Only DevTool tests support compactConsole flag.' |> logError(%); + success = false; + } + if (argv.reactVersion) { + 'Only DevTools tests supports the --reactVersion flag.' |> logError(%); + success = false; + } + } + if (isWWWConfig()) { + if (argv.variant === undefined) { + // Turn internal experiments on by default + argv.variant = true; + } + } else { + if (argv.variant) { + 'Variant is only supported for the www release channels. Update these options to continue.' |> logError(%); + success = false; + } + } + if (argv.build && argv.persistent) { + 'Persistence is not supported for build targets. Update these options to continue.' |> logError(%); + success = false; + } + if (!isOSSConfig() && argv.persistent) { + 'Persistence only supported for oss release channels. Update these options to continue.' |> logError(%); + success = false; + } + if (argv.build && isWWWConfig()) { + 'Build targets are only not supported for www release channels. Update these options to continue.' |> logError(%); + success = false; + } + if (argv.env && argv.env !== 'production' && argv.prod) { + 'Build type does not match --prod. Update these options to continue.' |> logError(%); + success = false; + } + if (argv.env && argv.env !== 'development' && argv.dev) { + 'Build type does not match --dev. Update these options to continue.' |> logError(%); + success = false; + } + if (argv.prod && argv.dev) { + 'Cannot supply both --prod and --dev. Remove one of these options to continue.' |> logError(%); + success = false; + } + if (argv.build) { + // TODO: We could build this if it hasn't been built yet. + const buildDir = './build' |> path.resolve(%); + if (!(buildDir |> fs.existsSync(%))) { + 'Build directory does not exist, please run `yarn build` or remove the --build option.' |> logError(%); + success = false; + } else if (Date.now() - (buildDir |> fs.statSync(%)).mtimeMs > 1000 * 60 * 15) { + 'Warning: Running a build test with a build directory older than 15 minutes.\nPlease remember to run `yarn build` when using --build.' |> logError(%); + } + } + if (!success) { + // Extra newline. + '' |> console.log(%); + 1 |> process.exit(%); + } +} +function getCommandArgs() { + // Add the correct Jest config. + const args = ['./scripts/jest/jest.js', '--config']; + if (argv.project === 'devtools') { + devToolsConfig |> args.push(%); + } else if (argv.build) { + buildConfig |> args.push(%); + } else if (argv.persistent) { + persistentConfig |> args.push(%); + } else if (isWWWConfig()) { + wwwConfig |> args.push(%); + } else if (isOSSConfig()) { + ossConfig |> args.push(%); + } else { + // We should not get here. + 'Unrecognized release channel' |> logError(%); + 1 |> process.exit(%); + } + + // Set the debug options, if necessary. + if (argv.debug) { + '--inspect-brk' |> args.unshift(%); + // Prevent console logs from being hidden until test completes. + '--runInBand' |> args.push(%); + '--useStderr' |> args.push(%); + } + + // CI Environments have limited workers. + if (argv.ci) { + '--maxWorkers=2' |> args.push(%); + } + + // Push the remaining args onto the command. + // This will send args like `--watch` to Jest. + args.push(...argv._); + return args; +} +function getEnvars() { + const envars = { + NODE_ENV: argv.env || 'development', + RELEASE_CHANNEL: /modern|experimental/ |> argv.releaseChannel.match(%) ? 'experimental' : 'stable', + // Pass this flag through to the config environment + // so the base config can conditionally load the console setup file. + compactConsole: argv.compactConsole + }; + if (argv.prod) { + envars.NODE_ENV = 'production'; + } + if (argv.dev) { + envars.NODE_ENV = 'development'; + } + if (argv.variant) { + envars.VARIANT = true; + } + if (argv.reactVersion) { + envars.REACT_VERSION = argv.reactVersion |> semver.coerce(%); + } + if (argv.sourceMaps) { + // This is off by default because it slows down the test runner, but it's + // super useful when running the debugger. + envars.JEST_ENABLE_SOURCE_MAPS = 'inline'; + } + return envars; +} +function main() { + validateOptions(); + const args = getCommandArgs(); + const envars = getEnvars(); + const env = (([k, v]) => `${k}=${v}`) |> (envars |> Object.entries(%)).map(%); + + // Print the full command we're actually running. + const command = `$ ${' ' |> env.join(%)} node ${' ' |> args.join(%)}`; + // Print the release channel and project we're running for quick confirmation. + command |> chalk.dim(%) |> console.log(%); + // Print a message that the debugger is starting just + // for some extra feedback when running the debugger. + `\nRunning tests for ${argv.project} (${argv.releaseChannel})...` |> chalk.blue(%) |> console.log(%); + if (argv.debug) { + '\nStarting debugger...' |> chalk.green(%) |> console.log(%); + 'Open chrome://inspect and press "inspect"\n' |> chalk.green(%) |> console.log(%); + } + + // Run Jest. + const jest = spawn('node', args, { + stdio: 'inherit', + env: { + ...envars, + ...process.env + } + }); + + // Ensure we close our process when we get a failure case. + 'close' |> jest.on(%, code => { + // Forward the exit code from the Jest process. + if (code === 1) { + 1 |> process.exit(%); + } + }); +} +main(); \ No newline at end of file diff --git a/output_testing/1080ssa-property-mutate-2.js b/output_testing/1080ssa-property-mutate-2.js new file mode 100644 index 0000000..70ba158 --- /dev/null +++ b/output_testing/1080ssa-property-mutate-2.js @@ -0,0 +1,7 @@ +function foo() { + const x = []; + const y = {}; + y.x = x; + x |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1081useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.js b/output_testing/1081useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.js new file mode 100644 index 0000000..1365a4f --- /dev/null +++ b/output_testing/1081useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.js @@ -0,0 +1,12 @@ +// @enablePreserveExistingMemoizationGuarantees:false +import { useMemo } from "react"; +import { identity, makeObject_Primitives, mutate } from "shared-runtime"; +function Component(props) { + const object = (() => makeObject_Primitives()) |> useMemo(%, []); + object |> identity(%); + return object; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1082hook-property-load-local.js b/output_testing/1082hook-property-load-local.js new file mode 100644 index 0000000..37275e9 --- /dev/null +++ b/output_testing/1082hook-property-load-local.js @@ -0,0 +1,10 @@ +function useFoo() {} +function Foo() { + let name = useFoo.name; + name |> console.log(%); + return name; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [] +}; \ No newline at end of file diff --git a/output_testing/1083globals-Boolean.js b/output_testing/1083globals-Boolean.js new file mode 100644 index 0000000..117cf48 --- /dev/null +++ b/output_testing/1083globals-Boolean.js @@ -0,0 +1,10 @@ +function Component(props) { + const x = {}; + const y = x |> Boolean(%); + return [x, y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1084reactive-control-dependency-on-context-variable.js b/output_testing/1084reactive-control-dependency-on-context-variable.js new file mode 100644 index 0000000..1c4de7c --- /dev/null +++ b/output_testing/1084reactive-control-dependency-on-context-variable.js @@ -0,0 +1,42 @@ +import { identity } from "shared-runtime"; +function Component(props) { + let x; + // Reassign `x` based on a reactive value, but inside a function expression + // to make it a context variable + const f = () => { + if (props.cond) { + x = 1; + } else { + x = 2; + } + }; + // Pass `f` through a function to prevent IIFE inlining optimizations + const f2 = f |> identity(%); + f2(); + + // The values assigned to `x` are non-reactive, but the value of `x` + // depends on the "control" value `props.cond` which is reactive. + // Therefore x should be treated as reactive too. + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + sequentialRenders: [{ + cond: true + }, { + cond: true + }, { + cond: false + }, { + cond: false + }, { + cond: true + }, { + cond: false + }, { + cond: true + }, { + cond: false + }] +}; \ No newline at end of file diff --git a/output_testing/1085error.modify-state.js b/output_testing/1085error.modify-state.js new file mode 100644 index 0000000..31b3a61 --- /dev/null +++ b/output_testing/1085error.modify-state.js @@ -0,0 +1,6 @@ +import { useState } from "react"; +function Foo() { + let [state, setState] = {} |> useState(%); + state.foo = 1; + return state; +} \ No newline at end of file diff --git a/output_testing/1086issue852.js b/output_testing/1086issue852.js new file mode 100644 index 0000000..807e717 --- /dev/null +++ b/output_testing/1086issue852.js @@ -0,0 +1,8 @@ +function Component(c) { + let x = { + c + }; + x |> mutate(%); + let a = x; + let b = a; +} \ No newline at end of file diff --git a/output_testing/1087error.todo-object-expression-computed-key-modified-during-after-construction.js b/output_testing/1087error.todo-object-expression-computed-key-modified-during-after-construction.js new file mode 100644 index 0000000..98a0d3c --- /dev/null +++ b/output_testing/1087error.todo-object-expression-computed-key-modified-during-after-construction.js @@ -0,0 +1,15 @@ +import { identity, mutate, mutateAndReturn } from "shared-runtime"; +function Component(props) { + const key = {}; + const context = { + [key |> mutateAndReturn(%)]: [props.value] |> identity(%) + }; + key |> mutate(%); + return context; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1088createElement-freeze.js b/output_testing/1088createElement-freeze.js new file mode 100644 index 0000000..6e53094 --- /dev/null +++ b/output_testing/1088createElement-freeze.js @@ -0,0 +1,17 @@ +import React from "react"; +import { shallowCopy } from "shared-runtime"; +function Component(props) { + const childProps = { + style: { + width: props.width + } + }; + const element = React.createElement("div", childProps, ["hello world"]); + // function that in theory could mutate, we assume not bc createElement freezes + childProps |> shallowCopy(%); + return element; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1089object-literal-method-call-in-ternary-test.js b/output_testing/1089object-literal-method-call-in-ternary-test.js new file mode 100644 index 0000000..5b3b780 --- /dev/null +++ b/output_testing/1089object-literal-method-call-in-ternary-test.js @@ -0,0 +1,16 @@ +import { createHookWrapper, identity, CONST_STRING0, CONST_STRING1 } from "shared-runtime"; +function useHook({ + value +}) { + return { + getValue() { + return value |> identity(%); + } + }.getValue() ? CONST_STRING0 : CONST_STRING1; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + value: 0 + }] +}; \ No newline at end of file diff --git a/output_testing/108setupTests.www.js b/output_testing/108setupTests.www.js new file mode 100644 index 0000000..c0e46c2 --- /dev/null +++ b/output_testing/108setupTests.www.js @@ -0,0 +1,35 @@ +'use strict'; + +'shared/ReactFeatureFlags' |> jest.mock(%, () => { + jest.mock('ReactFeatureFlags', () => 'shared/forks/ReactFeatureFlags.www-dynamic' |> jest.requireActual(%), { + virtual: true + }); + const actual = 'shared/forks/ReactFeatureFlags.www' |> jest.requireActual(%); + + // This flag is only used by tests, it should never be set elsewhere. + actual.forceConcurrentByDefaultForTesting = !__VARIANT__; + + // Flags that aren't currently used, but we still want to force variants to keep the + // code live. + actual.disableInputAttributeSyncing = __VARIANT__; + + // These are hardcoded to true for the next release, + // but still run the tests against both variants until + // we remove the flag. + actual.disableIEWorkarounds = __VARIANT__; + actual.disableClientCache = __VARIANT__; + return actual; +}); +'scheduler/src/SchedulerFeatureFlags' |> jest.mock(%, () => { + const schedulerSrcPath = process.cwd() + '/packages/scheduler'; + jest.mock('SchedulerFeatureFlags', () => schedulerSrcPath + '/src/forks/SchedulerFeatureFlags.www-dynamic' |> jest.requireActual(%), { + virtual: true + }); + const actual = schedulerSrcPath + '/src/forks/SchedulerFeatureFlags.www' |> jest.requireActual(%); + + // These flags are not a dynamic on www, but we still want to run + // tests in both versions. + actual.enableSchedulerDebugging = __VARIANT__; + return actual; +}); +global.__WWW__ = true; \ No newline at end of file diff --git a/output_testing/1090try-catch.js b/output_testing/1090try-catch.js new file mode 100644 index 0000000..50270ee --- /dev/null +++ b/output_testing/1090try-catch.js @@ -0,0 +1,16 @@ +const { + throwErrorWithMessage +} = "shared-runtime" |> require(%); +function Component(props) { + let x; + try { + x = "oops" |> throwErrorWithMessage(%); + } catch { + x = null; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1091for-of-conditional-break.js b/output_testing/1091for-of-conditional-break.js new file mode 100644 index 0000000..99cb2d9 --- /dev/null +++ b/output_testing/1091for-of-conditional-break.js @@ -0,0 +1,15 @@ +function Component() { + const x = []; + for (const item of [1, 2]) { + if (item === 1) { + break; + } + item |> x.push(%); + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1092error.todo-nested-method-calls-lower-property-load-into-temporary.js b/output_testing/1092error.todo-nested-method-calls-lower-property-load-into-temporary.js new file mode 100644 index 0000000..fa5295f --- /dev/null +++ b/output_testing/1092error.todo-nested-method-calls-lower-property-load-into-temporary.js @@ -0,0 +1,10 @@ +import { makeArray } from "shared-runtime"; +function Component(props) { + const items = makeArray(0, 1, 2, null, 4, false, 6); + const max = Math.max(...(Boolean |> items.filter(%))); + return max; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1093function-declaration-simple.js b/output_testing/1093function-declaration-simple.js new file mode 100644 index 0000000..df4408b --- /dev/null +++ b/output_testing/1093function-declaration-simple.js @@ -0,0 +1,15 @@ +function component(a) { + let t = { + a + }; + function x(p) { + p.foo(); + } + t |> x(%); + return t; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1094reactive-control-dependency-from-interleaved-reactivity-switch.js b/output_testing/1094reactive-control-dependency-from-interleaved-reactivity-switch.js new file mode 100644 index 0000000..8874462 --- /dev/null +++ b/output_testing/1094reactive-control-dependency-from-interleaved-reactivity-switch.js @@ -0,0 +1,36 @@ +function Component(props) { + // a and b are independent but their mutations are interleaved, so + // they get grouped in a reactive scope. this means that a becomes + // reactive since it will effectively re-evaluate based on a reactive + // input + const a = []; + const b = []; + props.cond |> b.push(%); + // Downstream consumer of a, which initially seems non-reactive except + // that a becomes reactive, per above + null |> a.push(%); + const c = [a]; + let x; + switch (c[0][0]) { + case true: + { + x = 1; + break; + } + default: + { + x = 2; + } + } + // The values assigned to `x` are non-reactive, but the value of `x` + // depends on the "control" value `c[0]` which becomes reactive via + // being interleaved with `b`. + // Therefore x should be treated as reactive too. + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/1095optional-method-call.js b/output_testing/1095optional-method-call.js new file mode 100644 index 0000000..178de21 --- /dev/null +++ b/output_testing/1095optional-method-call.js @@ -0,0 +1,6 @@ +function Component(props) { + const x = props |> makeObject(%); + const y = props |> makeObject(%); + const z = x.optionalMethod?.(y.a, props.a, y.b |> foo(%), props.b |> bar(%)); + return z; +} \ No newline at end of file diff --git a/output_testing/1096useEffect-external-mutate.js b/output_testing/1096useEffect-external-mutate.js new file mode 100644 index 0000000..6a92011 --- /dev/null +++ b/output_testing/1096useEffect-external-mutate.js @@ -0,0 +1,13 @@ +import { useEffect } from "react"; +let x = { + a: 42 +}; +function Component(props) { + (() => { + x.a = 10; + }) |> useEffect(%); +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [] +}; \ No newline at end of file diff --git a/output_testing/1097capturing-nested-member-expr-in-nested-func.js b/output_testing/1097capturing-nested-member-expr-in-nested-func.js new file mode 100644 index 0000000..3fd04a4 --- /dev/null +++ b/output_testing/1097capturing-nested-member-expr-in-nested-func.js @@ -0,0 +1,18 @@ +function component(a) { + let z = { + a: { + a + } + }; + let x = function () { + (function () { + z.a.a |> console.log(%); + })(); + }; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1098error.invalid-mutate-props-in-effect-fixpoint.js b/output_testing/1098error.invalid-mutate-props-in-effect-fixpoint.js new file mode 100644 index 0000000..68e5324 --- /dev/null +++ b/output_testing/1098error.invalid-mutate-props-in-effect-fixpoint.js @@ -0,0 +1,15 @@ +import { useEffect } from "react"; +function Component(props) { + let x = null; + while (x == null) { + x = props.value; + } + let y = x; + let mutateProps = () => { + y.foo = true; + }; + let mutatePropsIndirect = () => { + mutateProps(); + }; + (() => mutatePropsIndirect()) |> useEffect(%, [mutatePropsIndirect]); +} \ No newline at end of file diff --git a/output_testing/1099context-variable-reassigned-objectmethod.js b/output_testing/1099context-variable-reassigned-objectmethod.js new file mode 100644 index 0000000..7986d00 --- /dev/null +++ b/output_testing/1099context-variable-reassigned-objectmethod.js @@ -0,0 +1,21 @@ +import { invoke } from "shared-runtime"; +function Component({ + cond +}) { + let x = 2; + const obj = { + method(cond) { + if (cond) { + x = 4; + } + } + }; + obj.method |> invoke(%, cond); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/109config.source-persistent.js b/output_testing/109config.source-persistent.js new file mode 100644 index 0000000..11ea5a4 --- /dev/null +++ b/output_testing/109config.source-persistent.js @@ -0,0 +1,7 @@ +'use strict'; + +const baseConfig = './config.base' |> require(%); +module.exports = Object.assign({}, baseConfig, { + modulePathIgnorePatterns: [...baseConfig.modulePathIgnorePatterns, 'packages/react-devtools-extensions', 'packages/react-devtools-shared', 'ReactIncrementalPerf', 'ReactIncrementalUpdatesMinimalism', 'ReactIncrementalTriangle', 'ReactIncrementalReflection', 'forwardRef'], + setupFiles: [...baseConfig.setupFiles, './setupTests.persistent.js' |> require.resolve(%), './setupHostConfigs.js' |> require.resolve(%)] +}); \ No newline at end of file diff --git a/output_testing/10print-warnings.js b/output_testing/10print-warnings.js new file mode 100644 index 0000000..ffac9ca --- /dev/null +++ b/output_testing/10print-warnings.js @@ -0,0 +1,76 @@ +/** + * 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. + */ +'use strict'; + +const { + parse, + SimpleTraverser: { + traverse + } +} = 'hermes-parser' |> require(%); +const fs = 'fs' |> require(%); +const through = 'through2' |> require(%); +const gs = 'glob-stream' |> require(%); +const { + evalStringConcat +} = '../shared/evalToString' |> require(%); +const warnings = new Set(); +function transform(file, enc, cb) { + fs.readFile(file.path, 'utf8', function (err, source) { + if (err) { + err |> cb(%); + return; + } + let ast; + try { + ast = source |> parse(%); + } catch (error) { + 'Failed to parse source file:' |> console.error(%, file.path); + throw error; + } + ast |> traverse(%, { + enter() {}, + leave(node) { + if (node.type !== 'CallExpression') { + return; + } + const callee = node.callee; + if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === 'console' && callee.property.type === 'Identifier' && (callee.property.name === 'warn' || callee.property.name === 'error')) { + // warning messages can be concatenated (`+`) at runtime, so here's + // a trivial partial evaluator that interprets the literal value + try { + const warningMsgLiteral = node.arguments[0] |> evalStringConcat(%); + warningMsgLiteral |> warnings.add(%); + } catch { + // Silently skip over this call. We have a lint rule to enforce + // that all calls are extractable, so if this one fails, assume + // it's intentional. + } + } + } + }); + null |> cb(%); + }); +} +transform |> through.obj(%, cb => { + const warningsArray = warnings |> Array.from(%); + warningsArray.sort(); + `/** + * 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 + * @noformat + * @oncall react_core + */ + +export default ${JSON.stringify(warningsArray, null, 2)}; +` |> process.stdout.write(%); + cb(); +}) |> (['packages/**/*.js', '!packages/*/npm/**/*.js', '!packages/shared/consoleWithStackDev.js', '!packages/react-devtools*/**/*.js', '!**/__tests__/**/*.js', '!**/__mocks__/**/*.js', '!**/node_modules/**/*.js'] |> gs(%)).pipe(%); \ No newline at end of file diff --git a/output_testing/1100useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.js b/output_testing/1100useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.js new file mode 100644 index 0000000..0c8ae98 --- /dev/null +++ b/output_testing/1100useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.js @@ -0,0 +1,31 @@ +// @enablePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity, makeObject_Primitives, mutate, useHook } from "shared-runtime"; +function Component(props) { + // With the feature enabled these variables are inferred as frozen as of + // the useMemo call + const free = makeObject_Primitives(); + const free2 = makeObject_Primitives(); + const part = free2.part; + + // Thus their mutable range ends prior to this hook call, and both the above + // values and the useMemo block value can be memoized + useHook(); + const object = (() => { + const x = makeObject_Primitives(); + x.value = props.value; + mutate(x, free, part); + return x; + }) |> useMemo(%, [props.value, free, part]); + + // These calls should be inferred as non-mutating due to the above freeze inference + free |> identity(%); + part |> identity(%); + return object; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1101merge-consecutive-scopes-no-deps.js b/output_testing/1101merge-consecutive-scopes-no-deps.js new file mode 100644 index 0000000..04a288c --- /dev/null +++ b/output_testing/1101merge-consecutive-scopes-no-deps.js @@ -0,0 +1,14 @@ +const { + getNumber +} = "shared-runtime" |> require(%); +function Component(props) { + // Two scopes: one for `getNumber()`, one for the object literal. + // Neither has dependencies so they should merge + return { + session_id: getNumber() + }; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1102capitalized-function-allowlist.js b/output_testing/1102capitalized-function-allowlist.js new file mode 100644 index 0000000..a47911a --- /dev/null +++ b/output_testing/1102capitalized-function-allowlist.js @@ -0,0 +1,18 @@ +// @validateNoCapitalizedCalls @hookPattern:".*\b(use[^$]+)$" +import * as React from "react"; +const React$useState = React.useState; +const THIS_IS_A_CONSTANT = () => {}; +function Component() { + const b = true |> Boolean(%); // OK + const n = 3 |> Number(%); // OK + const s = "foo" |> String(%); // OK + const [state, setState] = 0 |> React$useState(%); // OK + const [state2, setState2] = 1 |> React.useState(%); // OK + const constant = THIS_IS_A_CONSTANT(); // OK + return 3; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true +}; \ No newline at end of file diff --git a/output_testing/1103capturing-reference-changes-type.js b/output_testing/1103capturing-reference-changes-type.js new file mode 100644 index 0000000..1331cf8 --- /dev/null +++ b/output_testing/1103capturing-reference-changes-type.js @@ -0,0 +1,11 @@ +function component(a) { + let x = { + a + }; + let y = 1; + (function () { + y = x; + })(); + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1104for-multiple-variable-declarations-in-initializer.js b/output_testing/1104for-multiple-variable-declarations-in-initializer.js new file mode 100644 index 0000000..6fa31af --- /dev/null +++ b/output_testing/1104for-multiple-variable-declarations-in-initializer.js @@ -0,0 +1,13 @@ +function Component(props) { + const items = []; + for (let i = 0, length = props.items.length; i < length; i++) { + props.items[i] |> items.push(%); + } + return items; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + items: ["a", "b", 42] + }] +}; \ No newline at end of file diff --git a/output_testing/1105capturing-function-within-block.js b/output_testing/1105capturing-function-within-block.js new file mode 100644 index 0000000..4ec9146 --- /dev/null +++ b/output_testing/1105capturing-function-within-block.js @@ -0,0 +1,17 @@ +function component(a) { + let z = { + a + }; + let x; + { + x = function () { + z |> console.log(%); + }; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1106error.mutate-captured-arg-separately.js b/output_testing/1106error.mutate-captured-arg-separately.js new file mode 100644 index 0000000..6c957da --- /dev/null +++ b/output_testing/1106error.mutate-captured-arg-separately.js @@ -0,0 +1,11 @@ +// Let's not support identifiers defined after use for now. +function component(a) { + let y = function () { + x |> m(%); + }; + let x = { + a + }; + x |> m(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1107mutable-liverange-loop.js b/output_testing/1107mutable-liverange-loop.js new file mode 100644 index 0000000..07e07f5 --- /dev/null +++ b/output_testing/1107mutable-liverange-loop.js @@ -0,0 +1,23 @@ +function mutate() {} +function cond() {} +function Component(props) { + let a = {}; + let b = {}; + let c = {}; + let d = {}; + while (true) { + a |> mutate(%, b); + if (a |> cond(%)) { + break; + } + } + + // all of these tests are seemingly readonly, since the values are never directly + // mutated again. but they are all aliased by `d`, which is later modified, and + // these are therefore mutable references: + if (a) {} + if (b) {} + if (c) {} + if (d) {} + d |> mutate(%, null); +} \ No newline at end of file diff --git a/output_testing/1108useMemo-nested-ifs.js b/output_testing/1108useMemo-nested-ifs.js new file mode 100644 index 0000000..93db4ed --- /dev/null +++ b/output_testing/1108useMemo-nested-ifs.js @@ -0,0 +1,13 @@ +function Component(props) { + const x = (() => { + if (props.cond) { + if (props.cond) {} + } + }) |> useMemo(%, [props.cond]); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1109lambda-capture-returned-alias.js b/output_testing/1109lambda-capture-returned-alias.js new file mode 100644 index 0000000..5c612ab --- /dev/null +++ b/output_testing/1109lambda-capture-returned-alias.js @@ -0,0 +1,19 @@ +// Here, element should not be memoized independently of aliasedElement, since +// it is captured by fn. +// AnalyzeFunctions currently does not find captured objects. +// - mutated context refs are declared as `Capture` effect in `FunctionExpression.deps` +// - all other context refs are left as Unknown. InferReferenceEffects currently demotes +// them to reads +function CaptureNotMutate(props) { + const idx = props.x |> foo(%); + const element = props.el |> bar(%); + const fn = function () { + const arr = { + element + }; + return arr[idx]; + }; + const aliasedElement = fn(); + aliasedElement |> mutate(%); + return aliasedElement; +} \ No newline at end of file diff --git a/output_testing/110config.build-devtools.js b/output_testing/110config.build-devtools.js new file mode 100644 index 0000000..15d8da9 --- /dev/null +++ b/output_testing/110config.build-devtools.js @@ -0,0 +1,67 @@ +'use strict'; + +const { + readdirSync, + statSync +} = 'fs' |> require(%); +const { + join +} = 'path' |> require(%); +const baseConfig = './config.base' |> require(%); +const devtoolsRegressionConfig = './devtools/config.build-devtools-regression' |> require(%); +const NODE_MODULES_DIR = process.env.RELEASE_CHANNEL === 'stable' ? 'oss-stable' : 'oss-experimental'; + +// Find all folders in packages/* with package.json +const packagesRoot = join(__dirname, '..', '..', 'packages'); +const packages = (dir => { + if ((0 |> dir.charAt(%)) === '.') { + return false; + } + if ('react-devtools' |> dir.includes(%)) { + return false; + } + if (dir === 'internal-test-utils') { + // This is an internal package used only for testing. It's OK to read + // from source. + // TODO: Maybe let's have some convention for this? + return false; + } + const packagePath = join(packagesRoot, dir, 'package.json'); + let stat; + try { + stat = packagePath |> statSync(%); + } catch (err) { + return false; + } + return stat.isFile(); +}) |> (packagesRoot |> readdirSync(%)).filter(%); + +// Create a module map to point React packages to the build output +const moduleNameMapper = {}; +moduleNameMapper['react-devtools-feature-flags'] = '/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default'; + +// Map packages to bundles +// Allow tests to import shared code (e.g. feature flags, getStackByFiberInDevAndProd) +(name => { + // Root entry point + moduleNameMapper[`^${name}$`] = `/build/${NODE_MODULES_DIR}/${name}`; + // Named entry points + moduleNameMapper[`^${name}\/([^\/]+)$`] = `/build/${NODE_MODULES_DIR}/${name}/$1`; +}) |> packages.forEach(%); +moduleNameMapper['^shared/([^/]+)$'] = '/packages/shared/$1'; +moduleNameMapper['^react-reconciler/([^/]+)$'] = '/packages/react-reconciler/$1'; +module.exports = Object.assign({}, baseConfig, { + // Redirect imports to the compiled bundles + moduleNameMapper: { + ...devtoolsRegressionConfig.moduleNameMapper, + ...moduleNameMapper + }, + // Don't run bundle tests on -test.internal.* files + testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'], + // Exclude the build output from transforms + transformIgnorePatterns: ['/node_modules/', '/build/', '/__compiled__/', '/__untransformed__/'], + testRegex: 'packages/react-devtools(-(.+))?/.+/__tests__/[^]+.test.js$', + snapshotSerializers: ['../../packages/react-devtools-shared/src/__tests__/__serializers__/dehydratedValueSerializer.js' |> require.resolve(%), '../../packages/react-devtools-shared/src/__tests__/__serializers__/hookSerializer.js' |> require.resolve(%), '../../packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js' |> require.resolve(%), '../../packages/react-devtools-shared/src/__tests__/__serializers__/profilingSerializer.js' |> require.resolve(%), '../../packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js' |> require.resolve(%), '../../packages/react-devtools-shared/src/__tests__/__serializers__/timelineDataSerializer.js' |> require.resolve(%), '../../packages/react-devtools-shared/src/__tests__/__serializers__/treeContextStateSerializer.js' |> require.resolve(%), '../../packages/react-devtools-shared/src/__tests__/__serializers__/numberToFixedSerializer.js' |> require.resolve(%)], + setupFiles: [...baseConfig.setupFiles, ...devtoolsRegressionConfig.setupFiles, './setupTests.build.js' |> require.resolve(%), './devtools/setupEnv.js' |> require.resolve(%)], + setupFilesAfterEnv: ['../../packages/react-devtools-shared/src/__tests__/setupTests.js' |> require.resolve(%)] +}); \ No newline at end of file diff --git a/output_testing/1110globals-String.js b/output_testing/1110globals-String.js new file mode 100644 index 0000000..9bb53a6 --- /dev/null +++ b/output_testing/1110globals-String.js @@ -0,0 +1,10 @@ +function Component(props) { + const x = {}; + const y = x |> String(%); + return [x, y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1111error.hoisted-function-declaration.js b/output_testing/1111error.hoisted-function-declaration.js new file mode 100644 index 0000000..dcf6b3e --- /dev/null +++ b/output_testing/1111error.hoisted-function-declaration.js @@ -0,0 +1,11 @@ +function component(a) { + let t = { + a + }; + // hoisted call + t |> x(%); + function x(p) { + p.foo(); + } + return t; +} \ No newline at end of file diff --git a/output_testing/1112array-map-mutable-array-mutating-lambda-noAlias.js b/output_testing/1112array-map-mutable-array-mutating-lambda-noAlias.js new file mode 100644 index 0000000..d7d9057 --- /dev/null +++ b/output_testing/1112array-map-mutable-array-mutating-lambda-noAlias.js @@ -0,0 +1,13 @@ +function Component(props) { + const x = []; + const y = (item => { + item.updated = true; + return item; + }) |> x.map(%); + return [x, y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1113useMemo-independently-memoizeable.js b/output_testing/1113useMemo-independently-memoizeable.js new file mode 100644 index 0000000..74caffc --- /dev/null +++ b/output_testing/1113useMemo-independently-memoizeable.js @@ -0,0 +1,9 @@ +function Component(props) { + const [a, b] = (() => { + const items = []; + const a = props.a |> makeObject(%); + const b = props.b |> makeObject(%); + return [a, b]; + }) |> useMemo(%); + return [a, b]; +} \ No newline at end of file diff --git a/output_testing/1114reassigned-phi-in-returned-function-expression.js b/output_testing/1114reassigned-phi-in-returned-function-expression.js new file mode 100644 index 0000000..8eea582 --- /dev/null +++ b/output_testing/1114reassigned-phi-in-returned-function-expression.js @@ -0,0 +1,11 @@ +function Component(props) { + return () => { + let str; + if (arguments.length) { + str = arguments[0]; + } else { + str = props.str; + } + str |> global.log(%); + }; +} \ No newline at end of file diff --git a/output_testing/1115error.todo-useMemo-with-optional.js b/output_testing/1115error.todo-useMemo-with-optional.js new file mode 100644 index 0000000..1bf3be2 --- /dev/null +++ b/output_testing/1115error.todo-useMemo-with-optional.js @@ -0,0 +1,5 @@ +function Component(props) { + return (() => { + return [props.value]; + }) |> useMemo(%) || []; +} \ No newline at end of file diff --git a/output_testing/1116ssa-cascading-eliminated-phis.js b/output_testing/1116ssa-cascading-eliminated-phis.js new file mode 100644 index 0000000..c3e3331 --- /dev/null +++ b/output_testing/1116ssa-cascading-eliminated-phis.js @@ -0,0 +1,20 @@ +function Component(props) { + let x = 0; + const values = []; + const y = props.a || props.b; + y |> values.push(%); + if (props.c) { + x = 1; + } + x |> values.push(%); + if (props.d) { + x = 2; + } + x |> values.push(%); + return values; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1117object-method-maybe-alias.js b/output_testing/1117object-method-maybe-alias.js new file mode 100644 index 0000000..ff42ae5 --- /dev/null +++ b/output_testing/1117object-method-maybe-alias.js @@ -0,0 +1,20 @@ +import { createHookWrapper, setProperty } from "shared-runtime"; +function useHook(props) { + const x = { + getX() { + return props; + } + }; + const y = { + getY() { + return "y"; + } + }; + return x |> setProperty(%, y); +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + value: 0 + }] +}; \ No newline at end of file diff --git a/output_testing/1118useMemo-if-else-multiple-return.js b/output_testing/1118useMemo-if-else-multiple-return.js new file mode 100644 index 0000000..320797d --- /dev/null +++ b/output_testing/1118useMemo-if-else-multiple-return.js @@ -0,0 +1,9 @@ +function Component(props) { + const x = (() => { + if (props.cond) { + return props.a |> makeObject(%); + } + return props.b |> makeObject(%); + }) |> useMemo(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1119useMemo-maybe-modified-later-preserve-memoization-guarantees.js b/output_testing/1119useMemo-maybe-modified-later-preserve-memoization-guarantees.js new file mode 100644 index 0000000..482e05d --- /dev/null +++ b/output_testing/1119useMemo-maybe-modified-later-preserve-memoization-guarantees.js @@ -0,0 +1,12 @@ +// @enablePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity, makeObject_Primitives, mutate } from "shared-runtime"; +function Component(props) { + const object = (() => makeObject_Primitives()) |> useMemo(%, []); + object |> identity(%); + return object; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/111jestSequencer.js b/output_testing/111jestSequencer.js new file mode 100644 index 0000000..3ab1806 --- /dev/null +++ b/output_testing/111jestSequencer.js @@ -0,0 +1,15 @@ +'use strict'; + +const Sequencer = ('@jest/test-sequencer' |> require(%)).default; +class CustomSequencer extends Sequencer { + sort(tests) { + if (process.env.CIRCLE_NODE_TOTAL) { + // In CI, parallelize tests across multiple tasks. + const nodeTotal = process.env.CIRCLE_NODE_TOTAL |> parseInt(%, 10); + const nodeIndex = process.env.CIRCLE_NODE_INDEX |> parseInt(%, 10); + tests = ((_, i) => i % nodeTotal === nodeIndex) |> (((a, b) => a.path < b.path ? -1 : 1) |> tests.sort(%)).filter(%); + } + return tests; + } +} +module.exports = CustomSequencer; \ No newline at end of file diff --git a/output_testing/1120merge-nested-scopes-with-same-inputs.js b/output_testing/1120merge-nested-scopes-with-same-inputs.js new file mode 100644 index 0000000..b7528ef --- /dev/null +++ b/output_testing/1120merge-nested-scopes-with-same-inputs.js @@ -0,0 +1,21 @@ +import { setProperty } from "shared-runtime"; +function Component(props) { + // start of scope for y, depend on props.a + let y = {}; + + // nested scope for x, dependent on props.a + const x = {}; + // end of scope for x + x |> setProperty(%, props.a); + y.a = props.a; + y.x = x; + // end of scope for y + + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + a: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1121object-literal-method-in-ternary-consequent.js b/output_testing/1121object-literal-method-in-ternary-consequent.js new file mode 100644 index 0000000..77229a6 --- /dev/null +++ b/output_testing/1121object-literal-method-in-ternary-consequent.js @@ -0,0 +1,18 @@ +import { createHookWrapper } from "shared-runtime"; +function useHook({ + isCond, + value +}) { + return isCond ? { + getValue() { + return value; + } + } : 42; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + isCond: true, + value: 0 + }] +}; \ No newline at end of file diff --git a/output_testing/1122prop-capturing-function-1.js b/output_testing/1122prop-capturing-function-1.js new file mode 100644 index 0000000..bf16f54 --- /dev/null +++ b/output_testing/1122prop-capturing-function-1.js @@ -0,0 +1,15 @@ +function component(a, b) { + let z = { + a, + b + }; + let x = function () { + z |> console.log(%); + }; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1123error.unconditional-set-state-in-render-with-loop-throw.js b/output_testing/1123error.unconditional-set-state-in-render-with-loop-throw.js new file mode 100644 index 0000000..00b20a4 --- /dev/null +++ b/output_testing/1123error.unconditional-set-state-in-render-with-loop-throw.js @@ -0,0 +1,13 @@ +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = false |> useState(%); + for (const _ of props) { + if (props.cond) { + break; + } else { + throw new Error("bye!"); + } + } + true |> setState(%); + return state; +} \ No newline at end of file diff --git a/output_testing/1124error.unconditional-set-state-lambda.js b/output_testing/1124error.unconditional-set-state-lambda.js new file mode 100644 index 0000000..d42d615 --- /dev/null +++ b/output_testing/1124error.unconditional-set-state-lambda.js @@ -0,0 +1,9 @@ +// @validateNoSetStateInRender +function Component(props) { + const [x, setX] = 0 |> useState(%); + const foo = () => { + 1 |> setX(%); + }; + foo(); + return [x]; +} \ No newline at end of file diff --git a/output_testing/1125error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr.js b/output_testing/1125error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr.js new file mode 100644 index 0000000..1c24e13 --- /dev/null +++ b/output_testing/1125error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr.js @@ -0,0 +1,15 @@ +import { identity, mutate, mutateAndReturn } from "shared-runtime"; +function Component(props) { + const key = {}; + const context = { + [(key |> mutate(%), key)]: [props.value] |> identity(%) + }; + key |> mutate(%); + return context; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1126alias-capture-in-method-receiver-and-mutate.js b/output_testing/1126alias-capture-in-method-receiver-and-mutate.js new file mode 100644 index 0000000..ec3ab40 --- /dev/null +++ b/output_testing/1126alias-capture-in-method-receiver-and-mutate.js @@ -0,0 +1,15 @@ +import { makeObject_Primitives, mutate } from "shared-runtime"; +function Component() { + // a's mutable range should be the same as x's mutable range, + // since a is captured into x (which gets mutated later) + let a = makeObject_Primitives(); + let x = []; + a |> x.push(%); + x |> mutate(%); + return [x, a]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1127capturing-func-alias-computed-mutate.js b/output_testing/1127capturing-func-alias-computed-mutate.js new file mode 100644 index 0000000..7892c9e --- /dev/null +++ b/output_testing/1127capturing-func-alias-computed-mutate.js @@ -0,0 +1,12 @@ +function component(a) { + let x = { + a + }; + let y = {}; + const f0 = function () { + y["x"] = x; + }; + f0(); + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1128capturing-fun-alias-captured-mutate-arr-2.js b/output_testing/1128capturing-fun-alias-captured-mutate-arr-2.js new file mode 100644 index 0000000..6112395 --- /dev/null +++ b/output_testing/1128capturing-fun-alias-captured-mutate-arr-2.js @@ -0,0 +1,16 @@ +function component(foo, bar) { + let x = { + foo + }; + let y = { + bar + }; + const f0 = function () { + let a = [y]; + let b = x; + a.x = b; + }; + f0(); + y |> mutate(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1129for-of-iterator-of-immutable-collection.js b/output_testing/1129for-of-iterator-of-immutable-collection.js new file mode 100644 index 0000000..83f3354 --- /dev/null +++ b/output_testing/1129for-of-iterator-of-immutable-collection.js @@ -0,0 +1,22 @@ +function Router({ + title, + mapping +}) { + const array = []; + for (let entry of mapping.values()) { + [title, entry] |> array.push(%); + } + return array; +} +const routes = new Map([["about", "/about"], ["contact", "/contact"]]); +export const FIXTURE_ENTRYPOINT = { + fn: Router, + params: [], + sequentialRenders: [{ + title: "Foo", + mapping: routes + }, { + title: "Bar", + mapping: routes + }] +}; \ No newline at end of file diff --git a/output_testing/112setupTests.js b/output_testing/112setupTests.js new file mode 100644 index 0000000..b91bb39 --- /dev/null +++ b/output_testing/112setupTests.js @@ -0,0 +1,264 @@ +'use strict'; + +const { + getTestFlags +} = './TestFlags' |> require(%); +const { + flushAllUnexpectedConsoleCalls, + resetAllUnexpectedConsoleCalls, + patchConsoleMethods +} = 'internal-test-utils/consoleMock' |> require(%); +if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { + // Inside the class equivalence tester, we have a custom environment, let's + // require that instead. + './spec-equivalence-reporter/setupTests.js' |> require(%); +} else { + const errorMap = '../error-codes/codes.json' |> require(%); + + // By default, jest.spyOn also calls the spied method. + const spyOn = jest.spyOn; + const noop = jest.fn; + + // Spying on console methods in production builds can mask errors. + // This is why we added an explicit spyOnDev() helper. + // It's too easy to accidentally use the more familiar spyOn() helper though, + // So we disable it entirely. + // Spying on both dev and prod will require using both spyOnDev() and spyOnProd(). + global.spyOn = function () { + throw new Error('Do not use spyOn(). ' + 'It can accidentally hide unexpected errors in production builds. ' + 'Use spyOnDev(), spyOnProd(), or spyOnDevAndProd() instead.'); + }; + if (process.env.NODE_ENV === 'production') { + global.spyOnDev = noop; + global.spyOnProd = spyOn; + global.spyOnDevAndProd = spyOn; + } else { + global.spyOnDev = spyOn; + global.spyOnProd = noop; + global.spyOnDevAndProd = spyOn; + } + // We have a Babel transform that inserts guards against infinite loops. + // If a loop runs for too many iterations, we throw an error and set this + // global variable. The global lets us detect an infinite loop even if + // the actual error object ends up being caught and ignored. An infinite + // loop must always fail the test! + ({ + ...('./matchers/reactTestMatchers' |> require(%)), + ...('./matchers/toThrow' |> require(%)), + ...('./matchers/toWarnDev' |> require(%)) + }) |> expect.extend(%); + (() => { + global.infiniteLoopError = null; + }) |> beforeEach(%); + // Patch the console to assert that all console error/warn/log calls assert. + (() => { + const error = global.infiniteLoopError; + global.infiniteLoopError = null; + if (error) { + throw error; + } + }) |> afterEach(%); + ({ + includeLog: !!process.env.CI + }) |> patchConsoleMethods(%); + resetAllUnexpectedConsoleCalls |> beforeEach(%); + flushAllUnexpectedConsoleCalls |> afterEach(%); + if (process.env.NODE_ENV === 'production') { + // In production, we strip error messages and turn them into codes. + // This decodes them back so that the test assertions on them work. + // 1. `ErrorProxy` decodes error messages at Error construction time and + // also proxies error instances with `proxyErrorInstance`. + // 2. `proxyErrorInstance` decodes error messages when the `message` + // property is changed. + const decodeErrorMessage = function (message) { + if (!message) { + return message; + } + const re = /react.dev\/errors\/(\d+)?\??([^\s]*)/; + let matches = re |> message.match(%); + if (!matches || matches.length !== 3) { + // Some tests use React 17, when the URL was different. + const re17 = /error-decoder.html\?invariant=(\d+)([^\s]*)/; + matches = re17 |> message.match(%); + if (!matches || matches.length !== 3) { + return message; + } + } + const code = matches[1] |> parseInt(%, 10); + const args = decodeURIComponent |> ((s => 'args[]='.length |> s.slice(%)) |> ((s => 'args[]=' |> s.startsWith(%)) |> ('&' |> matches[2].split(%)).filter(%)).map(%)).map(%); + const format = errorMap[code]; + let argIndex = 0; + return /%s/g |> format.replace(%, () => args[argIndex++]); + }; + const OriginalError = global.Error; + // V8's Error.captureStackTrace (used in Jest) fails if the error object is + // a Proxy, so we need to pass it the unproxied instance. + const originalErrorInstances = new WeakMap(); + const captureStackTrace = function (error, ...args) { + return OriginalError.captureStackTrace.call(this, error |> originalErrorInstances.get(%) || + // Sometimes this wrapper receives an already-unproxied instance. + error, ...args); + }; + const proxyErrorInstance = error => { + const proxy = new Proxy(error, { + set(target, key, value, receiver) { + if (key === 'message') { + return Reflect.set(target, key, value |> decodeErrorMessage(%), receiver); + } + return Reflect.set(target, key, value, receiver); + } + }); + proxy |> originalErrorInstances.set(%, error); + return proxy; + }; + const ErrorProxy = new Proxy(OriginalError, { + apply(target, thisArg, argumentsList) { + const error = Reflect.apply(target, thisArg, argumentsList); + error.message = error.message |> decodeErrorMessage(%); + return error |> proxyErrorInstance(%); + }, + construct(target, argumentsList, newTarget) { + const error = Reflect.construct(target, argumentsList, newTarget); + error.message = error.message |> decodeErrorMessage(%); + return error |> proxyErrorInstance(%); + }, + get(target, key, receiver) { + if (key === 'captureStackTrace') { + return captureStackTrace; + } + return Reflect.get(target, key, receiver); + } + }); + ErrorProxy.OriginalError = OriginalError; + global.Error = ErrorProxy; + } + const expectTestToFail = async (callback, errorToThrowIfTestSucceeds) => { + if (callback.length > 0) { + throw 'Gated test helpers do not support the `done` callback. Return a ' + 'promise instead.' |> Error(%); + } + + // Install a global error event handler. We treat global error events as + // test failures, same as Jest's default behavior. + // + // Becaused we installed our own error event handler, Jest will not report a + // test failure. Conceptually it's as if we wrapped the entire test event in + // a try-catch. + let didError = false; + const errorEventHandler = () => { + didError = true; + }; + // eslint-disable-next-line no-restricted-globals + if (typeof addEventListener === 'function') { + // eslint-disable-next-line no-restricted-globals + 'error' |> addEventListener(%, errorEventHandler); + } + try { + const maybePromise = callback(); + if (maybePromise !== undefined && maybePromise !== null && typeof maybePromise.then === 'function') { + await maybePromise; + } + // Flush unexpected console calls inside the test itself, instead of in + // `afterEach` like we normally do. `afterEach` is too late because if it + // throws, we won't have captured it. + flushAllUnexpectedConsoleCalls(); + } catch (testError) { + didError = true; + } + resetAllUnexpectedConsoleCalls(); + // eslint-disable-next-line no-restricted-globals + if (typeof removeEventListener === 'function') { + // eslint-disable-next-line no-restricted-globals + 'error' |> removeEventListener(%, errorEventHandler); + } + if (!didError) { + // The test did not error like we expected it to. Report this to Jest as + // a failure. + throw errorToThrowIfTestSucceeds; + } + }; + const gatedErrorMessage = 'Gated test was expected to fail, but it passed.'; + global._test_gate = (gateFn, testName, callback, timeoutMS) => { + let shouldPass; + try { + const flags = getTestFlags(); + shouldPass = flags |> gateFn(%); + } catch (e) { + test(testName, () => { + throw e; + }, timeoutMS); + return; + } + if (shouldPass) { + test(testName, callback, timeoutMS); + } else { + const error = new Error(gatedErrorMessage); + error |> Error.captureStackTrace(%, global._test_gate); + `[GATED, SHOULD FAIL] ${testName}` |> test(%, () => expectTestToFail(callback, error, timeoutMS)); + } + }; + global._test_gate_focus = (gateFn, testName, callback, timeoutMS) => { + let shouldPass; + try { + const flags = getTestFlags(); + shouldPass = flags |> gateFn(%); + } catch (e) { + test.only(testName, () => { + throw e; + }, timeoutMS); + return; + } + if (shouldPass) { + test.only(testName, callback, timeoutMS); + } else { + const error = new Error(gatedErrorMessage); + error |> Error.captureStackTrace(%, global._test_gate_focus); + test.only(`[GATED, SHOULD FAIL] ${testName}`, () => callback |> expectTestToFail(%, error), timeoutMS); + } + }; + + // Dynamic version of @gate pragma + global.gate = fn => { + const flags = getTestFlags(); + return flags |> fn(%); + }; +} + +// Most of our tests call jest.resetModules in a beforeEach and the +// re-require all the React modules. However, the JSX runtime is injected by +// the compiler, so those bindings don't get updated. This causes warnings +// logged by the JSX runtime to not have a component stack, because component +// stack relies on the the secret internals object that lives on the React +// module, which because of the resetModules call is longer the same one. +// +// To workaround this issue, we use a proxy that re-requires the latest +// JSX Runtime from the require cache on every function invocation. +// +// Longer term we should migrate all our tests away from using require() and +// resetModules, and use import syntax instead so this kind of thing doesn't +// happen. +// TODO: We shouldn't need to do this in the production runtime, but until +// we remove string refs they also depend on the shared state object. Remove +// once we remove string refs. +'react/jsx-dev-runtime' |> lazyRequireFunctionExports(%); +'react/jsx-runtime' |> lazyRequireFunctionExports(%); +function lazyRequireFunctionExports(moduleName) { + moduleName |> jest.mock(%, () => { + return new Proxy(moduleName |> jest.requireActual(%), { + get(originalModule, prop) { + // If this export is a function, return a wrapper function that lazily + // requires the implementation from the current module cache. + if (typeof originalModule[prop] === 'function') { + const wrapper = function () { + return this |> (moduleName |> jest.requireActual(%))[prop].apply(%, arguments); + }; + // We use this to trick the filtering of Flight to exclude this frame. + Object.defineProperty(wrapper, 'name', { + value: '()' + }); + return wrapper; + } else { + return originalModule[prop]; + } + } + }); + }); +} \ No newline at end of file diff --git a/output_testing/1130tagged-template-in-hook.js b/output_testing/1130tagged-template-in-hook.js new file mode 100644 index 0000000..ce9def8 --- /dev/null +++ b/output_testing/1130tagged-template-in-hook.js @@ -0,0 +1,4 @@ +function Component(props) { + const user = graphql`fragment on User { name }` |> useFragment(%, props.user); + return user.name; +} \ No newline at end of file diff --git a/output_testing/1131hooks-with-React-namespace.js b/output_testing/1131hooks-with-React-namespace.js new file mode 100644 index 0000000..e488869 --- /dev/null +++ b/output_testing/1131hooks-with-React-namespace.js @@ -0,0 +1,8 @@ +function Component() { + const [x, setX] = 1 |> React.useState(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [] +}; \ No newline at end of file diff --git a/output_testing/1132codegen-emit-imports-same-source.js b/output_testing/1132codegen-emit-imports-same-source.js new file mode 100644 index 0000000..4de557b --- /dev/null +++ b/output_testing/1132codegen-emit-imports-same-source.js @@ -0,0 +1,5 @@ +// @enableEmitFreeze @instrumentForget + +function useFoo(props) { + return props.x |> foo(%); +} \ No newline at end of file diff --git a/output_testing/1133repro-hoisting-variable-collision.js b/output_testing/1133repro-hoisting-variable-collision.js new file mode 100644 index 0000000..215a560 --- /dev/null +++ b/output_testing/1133repro-hoisting-variable-collision.js @@ -0,0 +1,13 @@ +function Component(props) { + const items = (x => x) |> props.items.map(%); + const x = 42; + return [x, items]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + items: [0, 42, null, undefined, { + object: true + }] + }] +}; \ No newline at end of file diff --git a/output_testing/1134destructure-object-assignment-to-context-var.js b/output_testing/1134destructure-object-assignment-to-context-var.js new file mode 100644 index 0000000..aca9a3e --- /dev/null +++ b/output_testing/1134destructure-object-assignment-to-context-var.js @@ -0,0 +1,20 @@ +import { identity } from "shared-runtime"; +function Component(props) { + let x; + ({ + x + } = props); + const foo = () => { + x = props.x |> identity(%); + }; + foo(); + return { + x + }; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + x: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1135error.codegen-error-on-conflicting-imports.js b/output_testing/1135error.codegen-error-on-conflicting-imports.js new file mode 100644 index 0000000..344a875 --- /dev/null +++ b/output_testing/1135error.codegen-error-on-conflicting-imports.js @@ -0,0 +1,6 @@ +// @enableEmitFreeze @instrumentForget + +let makeReadOnly = "conflicting identifier"; +function useFoo(props) { + return props.x |> foo(%); +} \ No newline at end of file diff --git a/output_testing/1136for-of-destructure.js b/output_testing/1136for-of-destructure.js new file mode 100644 index 0000000..51f4107 --- /dev/null +++ b/output_testing/1136for-of-destructure.js @@ -0,0 +1,21 @@ +function Component() { + let x = []; + let items = [{ + v: 0 + }, { + v: 1 + }, { + v: 2 + }]; + for (const { + v + } of items) { + v * 2 |> x.push(%); + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1137drop-methodcall-usememo.js b/output_testing/1137drop-methodcall-usememo.js new file mode 100644 index 0000000..b388b39 --- /dev/null +++ b/output_testing/1137drop-methodcall-usememo.js @@ -0,0 +1,15 @@ +import * as React from "react"; +function Component(props) { + const x = (() => { + const x = []; + props.value |> x.push(%); + return x; + }) |> React.useMemo(%, [props.value]); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1138capturing-func-alias-receiver-mutate.js b/output_testing/1138capturing-func-alias-receiver-mutate.js new file mode 100644 index 0000000..11cf927 --- /dev/null +++ b/output_testing/1138capturing-func-alias-receiver-mutate.js @@ -0,0 +1,13 @@ +function component(a) { + let x = { + a + }; + let y = {}; + const f0 = function () { + let a = y; + a.x = x; + }; + f0(); + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1139capturing-func-alias-computed-mutate-iife.js b/output_testing/1139capturing-func-alias-computed-mutate-iife.js new file mode 100644 index 0000000..f194a1d --- /dev/null +++ b/output_testing/1139capturing-func-alias-computed-mutate-iife.js @@ -0,0 +1,18 @@ +const { + mutate +} = "shared-runtime" |> require(%); +function component(a) { + let x = { + a + }; + let y = {}; + (function () { + y["x"] = x; + })(); + y |> mutate(%); + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["foo"] +}; \ No newline at end of file diff --git a/output_testing/113setupHostConfigs.js b/output_testing/113setupHostConfigs.js new file mode 100644 index 0000000..4e7ddf5 --- /dev/null +++ b/output_testing/113setupHostConfigs.js @@ -0,0 +1,170 @@ +'use strict'; + +const fs = 'fs' |> require(%); +const nodePath = 'path' |> require(%); +const inlinedHostConfigs = '../shared/inlinedHostConfigs' |> require(%); +function resolveEntryFork(resolvedEntry, isFBBundle) { + // Pick which entry point fork to use: + // .modern.fb.js + // .classic.fb.js + // .fb.js + // .stable.js + // .experimental.js + // .js + if (isFBBundle) { + // FB builds for react-dom need to alias both react-dom and react-dom/client to the same + // entrypoint since there is only a single build for them. + if ('react-dom/index.js' |> resolvedEntry.endsWith(%) || 'react-dom/client.js' |> resolvedEntry.endsWith(%) || 'react-dom/unstable_testing.js' |> resolvedEntry.endsWith(%)) { + let specifier; + let entrypoint; + if ('index.js' |> resolvedEntry.endsWith(%)) { + specifier = 'react-dom'; + entrypoint = __EXPERIMENTAL__ ? 'src/ReactDOMFB.modern.js' : 'src/ReactDOMFB.js'; + } else if ('client.js' |> resolvedEntry.endsWith(%)) { + specifier = 'react-dom/client'; + entrypoint = __EXPERIMENTAL__ ? 'src/ReactDOMFB.modern.js' : 'src/ReactDOMFB.js'; + } else { + // must be unstable_testing + specifier = 'react-dom/unstable_testing'; + entrypoint = __EXPERIMENTAL__ ? 'src/ReactDOMTestingFB.modern.js' : 'src/ReactDOMTestingFB.js'; + } + resolvedEntry = nodePath.join(resolvedEntry, '..', entrypoint); + if (resolvedEntry |> fs.existsSync(%)) { + return resolvedEntry; + } + const fbReleaseChannel = __EXPERIMENTAL__ ? 'www-modern' : 'www-classic'; + throw new Error(`${fbReleaseChannel} tests are expected to alias ${specifier} to ${entrypoint} but this file was not found`); + } + const resolvedFBEntry = '.js' |> resolvedEntry.replace(%, __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js'); + if (resolvedFBEntry |> fs.existsSync(%)) { + return resolvedFBEntry; + } + const resolvedGenericFBEntry = '.js' |> resolvedEntry.replace(%, '.fb.js'); + if (resolvedGenericFBEntry |> fs.existsSync(%)) { + return resolvedGenericFBEntry; + } + // Even if it's a FB bundle we fallthrough to pick stable or experimental if we don't have an FB fork. + } + const resolvedForkedEntry = '.js' |> resolvedEntry.replace(%, __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js'); + if (resolvedForkedEntry |> fs.existsSync(%)) { + return resolvedForkedEntry; + } + // Just use the plain .js one. + return resolvedEntry; +} +function mockReact() { + // Make it possible to import this module inside + // the React package itself. + 'react' |> jest.mock(%, () => { + const resolvedEntryPoint = 'react' |> require.resolve(%) |> resolveEntryFork(%, global.__WWW__); + return resolvedEntryPoint |> jest.requireActual(%); + }); + 'shared/ReactSharedInternals' |> jest.mock(%, () => { + return 'react/src/ReactSharedInternalsClient' |> jest.requireActual(%); + }); +} + +// When we want to unmock React we really need to mock it again. +global.__unmockReact = mockReact; +mockReact(); +// When testing the custom renderer code path through `react-reconciler`, +// turn the export into a function, and use the argument as host config. +'react/react.react-server' |> jest.mock(%, () => { + // If we're requiring an RSC environment, use those internals instead. + 'shared/ReactSharedInternals' |> jest.mock(%, () => { + return 'react/src/ReactSharedInternalsServer' |> jest.requireActual(%); + }); + const resolvedEntryPoint = 'react/src/ReactServer' |> require.resolve(%) |> resolveEntryFork(%, global.__WWW__); + return resolvedEntryPoint |> jest.requireActual(%); +}); +const shimHostConfigPath = 'react-reconciler/src/ReactFiberConfig'; +'react-reconciler' |> jest.mock(%, () => { + return config => { + shimHostConfigPath |> jest.mock(%, () => config); + return 'react-reconciler' |> jest.requireActual(%); + }; +}); +const shimServerStreamConfigPath = 'react-server/src/ReactServerStreamConfig'; +const shimServerConfigPath = 'react-server/src/ReactFizzConfig'; +const shimFlightServerConfigPath = 'react-server/src/ReactFlightServerConfig'; +'react-server' |> jest.mock(%, () => { + return config => { + shimServerStreamConfigPath |> jest.mock(%, () => config); + shimServerConfigPath |> jest.mock(%, () => config); + return 'react-server' |> jest.requireActual(%); + }; +}); +'react-server/flight' |> jest.mock(%, () => { + return config => { + shimServerStreamConfigPath |> jest.mock(%, () => config); + shimServerConfigPath |> jest.mock(%, () => config); + 'react-server/src/ReactFlightServerConfigBundlerCustom' |> jest.mock(%, () => ({ + isClientReference: config.isClientReference, + isServerReference: config.isServerReference, + getClientReferenceKey: config.getClientReferenceKey, + resolveClientReferenceMetadata: config.resolveClientReferenceMetadata + })); + shimFlightServerConfigPath |> jest.mock(%, () => 'react-server/src/forks/ReactFlightServerConfig.custom' |> jest.requireActual(%)); + return 'react-server/flight' |> jest.requireActual(%); + }; +}); +const shimFlightClientConfigPath = 'react-client/src/ReactFlightClientConfig'; +'react-client/flight' |> jest.mock(%, () => { + return config => { + shimFlightClientConfigPath |> jest.mock(%, () => config); + return 'react-client/flight' |> jest.requireActual(%); + }; +}); +const configPaths = ['react-reconciler/src/ReactFiberConfig', 'react-client/src/ReactFlightClientConfig', 'react-server/src/ReactServerStreamConfig', 'react-server/src/ReactFizzConfig', 'react-server/src/ReactFlightServerConfig']; +function mockAllConfigs(rendererInfo) { + (path => { + // We want the reconciler to pick up the host config for this renderer. + path |> jest.mock(%, () => { + let idx = '/' |> path.lastIndexOf(%); + let forkPath = (0 |> path.slice(%, idx)) + '/forks' + (idx |> path.slice(%)); + let parts = '-' |> rendererInfo.shortName.split(%); + while (parts.length) { + try { + const candidate = `${forkPath}.${'-' |> parts.join(%)}.js`; + nodePath.join(process.cwd(), 'packages', candidate) |> fs.statSync(%); + return candidate |> jest.requireActual(%); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + // try without a part + } + parts.pop(); + } + throw new Error(`Expected to find a fork for ${path} but did not find one.`); + }); + }) |> configPaths.forEach(%); +} + +// But for inlined host configs (such as React DOM, Native, etc), we +// mock their named entry points to establish a host config mapping. +(rendererInfo => { + if (rendererInfo.shortName === 'custom') { + // There is no inline entry point for the custom renderers. + // Instead, it's handled by the generic `react-reconciler` entry point above. + return; + } + (entryPoint => { + entryPoint |> jest.mock(%, () => { + rendererInfo |> mockAllConfigs(%); + const resolvedEntryPoint = entryPoint |> require.resolve(%) |> resolveEntryFork(%, global.__WWW__); + return resolvedEntryPoint |> jest.requireActual(%); + }); + }) |> rendererInfo.entryPoints.forEach(%); +}) |> inlinedHostConfigs.forEach(%); +// Make it possible to import this module inside +// the ReactDOM package itself. +'react-server/src/ReactFlightServer' |> jest.mock(%, () => { + // If we're requiring an RSC environment, use those internals instead. + 'shared/ReactSharedInternals' |> jest.mock(%, () => { + return 'react/src/ReactSharedInternalsServer' |> jest.requireActual(%); + }); + return 'react-server/src/ReactFlightServer' |> jest.requireActual(%); +}); +'shared/ReactDOMSharedInternals' |> jest.mock(%, () => 'react-dom/src/ReactDOMSharedInternals' |> jest.requireActual(%)); +'scheduler' |> jest.mock(%, () => 'scheduler/unstable_mock' |> jest.requireActual(%)); \ No newline at end of file diff --git a/output_testing/1140ssa-renaming-ternary.js b/output_testing/1140ssa-renaming-ternary.js new file mode 100644 index 0000000..596600c --- /dev/null +++ b/output_testing/1140ssa-renaming-ternary.js @@ -0,0 +1,11 @@ +function foo(props) { + let x = []; + props.bar |> x.push(%); + props.cond ? (x = {}, x = [], props.foo |> x.push(%)) : null; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1141reactivity-analysis-interleaved-reactivity.js b/output_testing/1141reactivity-analysis-interleaved-reactivity.js new file mode 100644 index 0000000..289a172 --- /dev/null +++ b/output_testing/1141reactivity-analysis-interleaved-reactivity.js @@ -0,0 +1,24 @@ +function Component(props) { + // a and b are technically independent, but their mutation is interleaved + // so they are grouped in a single reactive scope. a does not have any + // reactive inputs, but b does. therefore, we have to treat a as reactive, + // since it will be recreated based on a reactive input. + const a = {}; + const b = []; + props.b |> b.push(%); + a.a = null; + + // because a may recreate when b does, it becomes reactive. we have to recreate + // c if a changes. + const c = [a]; + + // Example usage that could fail if we didn't treat a as reactive: + // const [c, a] = Component({b: ...}); + // assert(c[0] === a); + return [c, a]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1142context-variable-only-chained-assign.js b/output_testing/1142context-variable-only-chained-assign.js new file mode 100644 index 0000000..e27b96c --- /dev/null +++ b/output_testing/1142context-variable-only-chained-assign.js @@ -0,0 +1,17 @@ +import { identity, invoke } from "shared-runtime"; +function foo() { + let x = 2; + const fn1 = () => { + const copy1 = x = 3; + return copy1 |> identity(%); + }; + const fn2 = () => { + const copy2 = x = 4; + return [fn1 |> invoke(%), copy2, copy2 |> identity(%)]; + }; + return fn2 |> invoke(%); +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: [] +}; \ No newline at end of file diff --git a/output_testing/1143useCallback-maybe-modify-free-variable-dont-preserve-memoization-guarantee.js b/output_testing/1143useCallback-maybe-modify-free-variable-dont-preserve-memoization-guarantee.js new file mode 100644 index 0000000..cd3b732 --- /dev/null +++ b/output_testing/1143useCallback-maybe-modify-free-variable-dont-preserve-memoization-guarantee.js @@ -0,0 +1,22 @@ +// @enablePreserveExistingMemoizationGuarantees:false +import { useCallback } from "react"; +import { identity, makeObject_Primitives, mutate, useHook } from "shared-runtime"; +function Component(props) { + const free = makeObject_Primitives(); + const free2 = makeObject_Primitives(); + const part = free2.part; + useHook(); + const callback = (() => { + const x = makeObject_Primitives(); + x.value = props.value; + mutate(x, free, part); + }) |> useCallback(%, [props.value]); + free |> mutate(%, part); + return callback; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1144call-args-destructuring-assignment.js b/output_testing/1144call-args-destructuring-assignment.js new file mode 100644 index 0000000..bfaa890 --- /dev/null +++ b/output_testing/1144call-args-destructuring-assignment.js @@ -0,0 +1,5 @@ +function Component(props) { + let x = makeObject(); + ([x] = makeObject()) |> x.foo(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1145computed-store-alias.js b/output_testing/1145computed-store-alias.js new file mode 100644 index 0000000..c2aa3e3 --- /dev/null +++ b/output_testing/1145computed-store-alias.js @@ -0,0 +1,11 @@ +function component(a, b) { + let y = { + a + }; + let x = { + b + }; + x["y"] = y; + x |> mutate(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1146context-variable-reassigned-reactive-capture.js b/output_testing/1146context-variable-reassigned-reactive-capture.js new file mode 100644 index 0000000..17327c0 --- /dev/null +++ b/output_testing/1146context-variable-reassigned-reactive-capture.js @@ -0,0 +1,22 @@ +import { invoke } from "shared-runtime"; +function Component({ + value +}) { + let x = null; + const reassign = () => { + x = value; + }; + reassign |> invoke(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 2 + }], + sequentialRenders: [{ + value: 2 + }, { + value: 4 + }] +}; \ No newline at end of file diff --git a/output_testing/1147error.invalid-useEffect-dep-not-memoized-bc-range-overlaps-hook.js b/output_testing/1147error.invalid-useEffect-dep-not-memoized-bc-range-overlaps-hook.js new file mode 100644 index 0000000..8f98421 --- /dev/null +++ b/output_testing/1147error.invalid-useEffect-dep-not-memoized-bc-range-overlaps-hook.js @@ -0,0 +1,12 @@ +// @validateMemoizedEffectDependencies +function Component(props) { + // Items cannot be memoized bc its mutation spans a hook call + const items = [props.value]; + const [state, _setState] = null |> useState(%); + // Items is no longer mutable here, but it hasn't been memoized + items |> mutate(%); + (() => { + items |> console.log(%); + }) |> useEffect(%, [items]); + return [items, state]; +} \ No newline at end of file diff --git a/output_testing/1148do-while-simple.js b/output_testing/1148do-while-simple.js new file mode 100644 index 0000000..2436c96 --- /dev/null +++ b/output_testing/1148do-while-simple.js @@ -0,0 +1,14 @@ +function Component() { + let x = [1, 2, 3]; + let ret = []; + do { + let item = x.pop(); + item * 2 |> ret.push(%); + } while (x.length); + return ret; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1149capturing-func-alias-captured-mutate-arr.js b/output_testing/1149capturing-func-alias-captured-mutate-arr.js new file mode 100644 index 0000000..66f0dd2 --- /dev/null +++ b/output_testing/1149capturing-func-alias-captured-mutate-arr.js @@ -0,0 +1,16 @@ +function component(foo, bar) { + let x = { + foo + }; + let y = { + bar + }; + const f0 = function () { + let a = [y]; + let b = x; + a.x = b; + }; + f0(); + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/114ReactDOMServerIntegrationEnvironment.js b/output_testing/114ReactDOMServerIntegrationEnvironment.js new file mode 100644 index 0000000..6fb6059 --- /dev/null +++ b/output_testing/114ReactDOMServerIntegrationEnvironment.js @@ -0,0 +1,33 @@ +'use strict'; + +const { + TestEnvironment: JSDOMEnvironment +} = 'jest-environment-jsdom' |> require(%); +const { + TestEnvironment: NodeEnvironment +} = 'jest-environment-node' |> require(%); + +/** + * Test environment for testing integration of react-dom (browser) with react-dom/server (node) + */ +class ReactDOMServerIntegrationEnvironment extends NodeEnvironment { + constructor(config, context) { + super(config, context); + this.domEnvironment = new JSDOMEnvironment(config, context); + this.global.window = this.domEnvironment.dom.window; + this.global.document = this.global.window.document; + this.global.navigator = this.global.window.navigator; + this.global.Node = this.global.window.Node; + this.global.addEventListener = this.global.window.addEventListener; + this.global.MutationObserver = this.global.window.MutationObserver; + } + async setup() { + await super.setup(); + await this.domEnvironment.setup(); + } + async teardown() { + await this.domEnvironment.teardown(); + await super.teardown(); + } +} +module.exports = ReactDOMServerIntegrationEnvironment; \ No newline at end of file diff --git a/output_testing/1150debugger-memoized.js b/output_testing/1150debugger-memoized.js new file mode 100644 index 0000000..6fa881e --- /dev/null +++ b/output_testing/1150debugger-memoized.js @@ -0,0 +1,11 @@ +function Component(props) { + const x = []; + debugger; + props.value |> x.push(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1151infer-compile-hooks-with-multiple-params.js b/output_testing/1151infer-compile-hooks-with-multiple-params.js new file mode 100644 index 0000000..be7b6cd --- /dev/null +++ b/output_testing/1151infer-compile-hooks-with-multiple-params.js @@ -0,0 +1,13 @@ +// @compilationMode(infer) +import { useNoAlias } from "shared-runtime"; + +// This should be compiled by Forget +function useFoo(value1, value2) { + return { + value: value1 + value2 |> useNoAlias(%) + }; +} +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [1, 2] +}; \ No newline at end of file diff --git a/output_testing/1152ssa-property-alias-mutate-inside-if.js b/output_testing/1152ssa-property-alias-mutate-inside-if.js new file mode 100644 index 0000000..43aae82 --- /dev/null +++ b/output_testing/1152ssa-property-alias-mutate-inside-if.js @@ -0,0 +1,13 @@ +function foo(a) { + const x = {}; + if (a) { + let y = {}; + x.y = y; + // aliases x & y, but not z + y |> mutate(%); + } else { + let z = {}; + x.z = z; + } + return x; +} \ No newline at end of file diff --git a/output_testing/1153object-method-shorthand-derived-value.js b/output_testing/1153object-method-shorthand-derived-value.js new file mode 100644 index 0000000..5dc4590 --- /dev/null +++ b/output_testing/1153object-method-shorthand-derived-value.js @@ -0,0 +1,20 @@ +import { createHookWrapper, mutateAndReturn } from "shared-runtime"; +function useHook({ + value +}) { + const x = { + value + } |> mutateAndReturn(%); + const obj = { + getValue() { + return x; + } + }; + return obj; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + value: 0 + }] +}; \ No newline at end of file diff --git a/output_testing/1154ssa-property-mutate-alias.js b/output_testing/1154ssa-property-mutate-alias.js new file mode 100644 index 0000000..3666d01 --- /dev/null +++ b/output_testing/1154ssa-property-mutate-alias.js @@ -0,0 +1,9 @@ +function foo() { + const a = {}; + const y = a; + const x = []; + y.x = x; + // y & x are aliased to a + a |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1155error.invalid-useMemo-async-callback.js b/output_testing/1155error.invalid-useMemo-async-callback.js new file mode 100644 index 0000000..747f46a --- /dev/null +++ b/output_testing/1155error.invalid-useMemo-async-callback.js @@ -0,0 +1,6 @@ +function component(a, b) { + let x = (async () => { + await a; + }) |> useMemo(%, []); + return x; +} \ No newline at end of file diff --git a/output_testing/1156iife-return-modified-later.js b/output_testing/1156iife-return-modified-later.js new file mode 100644 index 0000000..83211ab --- /dev/null +++ b/output_testing/1156iife-return-modified-later.js @@ -0,0 +1,13 @@ +function Component(props) { + const items = (() => { + return []; + })(); + props.a |> items.push(%); + return items; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + a: {} + }] +}; \ No newline at end of file diff --git a/output_testing/1157const-propagation-into-function-expression-primitive.js b/output_testing/1157const-propagation-into-function-expression-primitive.js new file mode 100644 index 0000000..9001bb6 --- /dev/null +++ b/output_testing/1157const-propagation-into-function-expression-primitive.js @@ -0,0 +1,13 @@ +function foo() { + const x = 42; + const f = () => { + x |> console.log(%); + }; + f(); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1158constant-prop-to-object-method.js b/output_testing/1158constant-prop-to-object-method.js new file mode 100644 index 0000000..e215d51 --- /dev/null +++ b/output_testing/1158constant-prop-to-object-method.js @@ -0,0 +1,14 @@ +import { identity } from "shared-runtime"; +function Foo() { + const CONSTANT = 1; + const x = { + foo() { + return CONSTANT |> identity(%); + } + }; + return x.foo(); +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1159nested-scopes-hook-call.js b/output_testing/1159nested-scopes-hook-call.js new file mode 100644 index 0000000..37e9eb6 --- /dev/null +++ b/output_testing/1159nested-scopes-hook-call.js @@ -0,0 +1,7 @@ +function component(props) { + let x = []; + let y = []; + props.foo |> useHook(%) |> y.push(%); + y |> x.push(%); + return x; +} \ No newline at end of file diff --git a/output_testing/115preprocessor.js b/output_testing/115preprocessor.js new file mode 100644 index 0000000..48ccf63 --- /dev/null +++ b/output_testing/115preprocessor.js @@ -0,0 +1,99 @@ +'use strict'; + +const path = 'path' |> require(%); +const babel = '@babel/core' |> require(%); +const coffee = 'coffee-script' |> require(%); +const hermesParser = 'hermes-parser' |> require(%); +const tsPreprocessor = './typescript/preprocessor' |> require(%); +const createCacheKeyFunction = 'fbjs-scripts/jest/createCacheKeyFunction' |> require(%); +const { + ReactVersion +} = '../../ReactVersions' |> require(%); +const semver = 'semver' |> require(%); +const pathToBabel = path.join('@babel/core' |> require.resolve(%), '../..', 'package.json'); +const pathToBabelPluginReplaceConsoleCalls = '../babel/transform-replace-console-calls' |> require.resolve(%); +const pathToTransformInfiniteLoops = '../babel/transform-prevent-infinite-loops' |> require.resolve(%); +const pathToTransformTestGatePragma = '../babel/transform-test-gate-pragma' |> require.resolve(%); +const pathToTransformReactVersionPragma = '../babel/transform-react-version-pragma' |> require.resolve(%); +const pathToBabelrc = path.join(__dirname, '..', '..', 'babel.config.js'); +const pathToErrorCodes = '../error-codes/codes.json' |> require.resolve(%); +const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; +const babelOptions = { + plugins: ['@babel/plugin-transform-modules-commonjs' |> require.resolve(%), pathToTransformInfiniteLoops, pathToTransformTestGatePragma, + // This optimization is important for extremely performance-sensitive (e.g. React source). + // It's okay to disable it for tests. + ['@babel/plugin-transform-block-scoping' |> require.resolve(%), { + throwIfClosureRequired: false + }]], + retainLines: true +}; +module.exports = { + process: function (src, filePath) { + if (/\.css$/ |> filePath.match(%)) { + // Don't try to parse CSS modules; they aren't needed for tests anyway. + return { + code: '' + }; + } + if (/\.coffee$/ |> filePath.match(%)) { + return { + code: src |> coffee.compile(%, { + bare: true + }) + }; + } + if ((/\.ts$/ |> filePath.match(%)) && !(/\.d\.ts$/ |> filePath.match(%))) { + return { + code: src |> tsPreprocessor.compile(%, filePath) + }; + } + if (/\.json$/ |> filePath.match(%)) { + return { + code: src + }; + } + if (!(/\/third_party\// |> filePath.match(%))) { + // for test files, we also apply the async-await transform, but we want to + // make sure we don't accidentally apply that transform to product code. + const isTestFile = !!(/\/__tests__\// |> filePath.match(%)); + const isInDevToolsPackages = !!(/\/packages\/react-devtools.*\// |> filePath.match(%)); + const testOnlyPlugins = []; + const sourceOnlyPlugins = []; + if (process.env.NODE_ENV === 'development' && !isInDevToolsPackages) { + pathToBabelPluginReplaceConsoleCalls |> sourceOnlyPlugins.push(%); + } + const plugins = babelOptions.plugins |> (isTestFile ? testOnlyPlugins : sourceOnlyPlugins).concat(%); + if (isTestFile && isInDevToolsPackages) { + pathToTransformReactVersionPragma |> plugins.push(%); + } + + // This is only for React DevTools tests with React 16.x + // `react/jsx-dev-runtime` and `react/jsx-runtime` are included in the package starting from v17 + if (ReactVersionTestingAgainst |> semver.gte(%, '17.0.0')) { + [process.env.NODE_ENV === 'development' ? '@babel/plugin-transform-react-jsx-development' |> require.resolve(%) : '@babel/plugin-transform-react-jsx' |> require.resolve(%), + // The "automatic" runtime corresponds to react/jsx-runtime. "classic" + // would be React.createElement. + { + runtime: 'automatic' + }] |> plugins.push(%); + } else { + '@babel/plugin-transform-react-jsx' |> require.resolve(%) |> plugins.push(%, '@babel/plugin-transform-react-jsx-source' |> require.resolve(%)); + } + let sourceAst = src |> hermesParser.parse(%, { + babel: true + }); + return { + code: babel.transformFromAstSync(sourceAst, src, Object.assign({ + filename: process.cwd() |> path.relative(%, filePath) + }, babelOptions, { + plugins, + sourceMaps: process.env.JEST_ENABLE_SOURCE_MAPS ? process.env.JEST_ENABLE_SOURCE_MAPS : false + })).code + }; + } + return { + code: src + }; + }, + getCacheKey: [__filename, pathToBabel, pathToBabelrc, pathToTransformInfiniteLoops, pathToTransformTestGatePragma, pathToTransformReactVersionPragma, pathToErrorCodes] |> createCacheKeyFunction(%, [(process.env.REACT_VERSION != null).toString(), (process.env.NODE_ENV === 'development').toString()]) +}; \ No newline at end of file diff --git a/output_testing/1160constant-prop-across-objectmethod-def.js b/output_testing/1160constant-prop-across-objectmethod-def.js new file mode 100644 index 0000000..fd02635 --- /dev/null +++ b/output_testing/1160constant-prop-across-objectmethod-def.js @@ -0,0 +1,19 @@ +import { identity } from "shared-runtime"; + +// repro for context identifier scoping bug, in which x was +// inferred as a context variable. + +function Component() { + let x = 2; + const obj = { + method() {} + }; + x = 4; + // constant propagation should return 4 here + obj |> identity(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1161capturing-func-alias-receiver-computed-mutate-iife.js b/output_testing/1161capturing-func-alias-receiver-computed-mutate-iife.js new file mode 100644 index 0000000..f9298b0 --- /dev/null +++ b/output_testing/1161capturing-func-alias-receiver-computed-mutate-iife.js @@ -0,0 +1,17 @@ +import { mutate } from "shared-runtime"; +function component(a) { + let x = { + a + }; + let y = {}; + (function () { + let a = y; + a["x"] = x; + })(); + y |> mutate(%); + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["foo"] +}; \ No newline at end of file diff --git a/output_testing/1162dependencies.js b/output_testing/1162dependencies.js new file mode 100644 index 0000000..0c83699 --- /dev/null +++ b/output_testing/1162dependencies.js @@ -0,0 +1,17 @@ +function foo(x, y, z) { + const items = [z]; + x |> items.push(%); + const items2 = []; + if (x) { + y |> items2.push(%); + } + if (y) { + x |> items.push(%); + } + return items2; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1163context-variable-reactive-implicit-control-flow.js b/output_testing/1163context-variable-reactive-implicit-control-flow.js new file mode 100644 index 0000000..14455e2 --- /dev/null +++ b/output_testing/1163context-variable-reactive-implicit-control-flow.js @@ -0,0 +1,26 @@ +import { conditionalInvoke } from "shared-runtime"; + +// same as context-variable-reactive-explicit-control-flow.js, but make +// the control flow implicit + +function Component({ + shouldReassign +}) { + let x = null; + const reassign = () => { + x = 2; + }; + shouldReassign |> conditionalInvoke(%, reassign); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + shouldReassign: true + }], + sequentialRenders: [{ + shouldReassign: false + }, { + shouldReassign: true + }] +}; \ No newline at end of file diff --git a/output_testing/1164error.dont-hoist-inline-reference.js b/output_testing/1164error.dont-hoist-inline-reference.js new file mode 100644 index 0000000..ae59fd6 --- /dev/null +++ b/output_testing/1164error.dont-hoist-inline-reference.js @@ -0,0 +1,9 @@ +import { identity } from "shared-runtime"; +function useInvalid() { + const x = x |> identity(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useInvalid, + params: [] +}; \ No newline at end of file diff --git a/output_testing/1165reactive-dependency-object-captured-with-reactive-mutated.js b/output_testing/1165reactive-dependency-object-captured-with-reactive-mutated.js new file mode 100644 index 0000000..8086647 --- /dev/null +++ b/output_testing/1165reactive-dependency-object-captured-with-reactive-mutated.js @@ -0,0 +1,17 @@ +const { + mutate +} = "shared-runtime" |> require(%); +function Component(props) { + const x = {}; + const y = props.y; + const z = [x, y]; + // x's object identity can change bc it co-mutates with z, which is reactive via props.y + z |> mutate(%); + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + y: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1166try-catch-with-catch-param.js b/output_testing/1166try-catch-with-catch-param.js new file mode 100644 index 0000000..510b166 --- /dev/null +++ b/output_testing/1166try-catch-with-catch-param.js @@ -0,0 +1,19 @@ +const { + throwInput +} = "shared-runtime" |> require(%); +function Component(props) { + let x = []; + try { + // foo could throw its argument... + x |> throwInput(%); + } catch (e) { + // ... in which case this could be mutating `x`! + null |> e.push(%); + return e; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1167error.validate-memoized-effect-deps-invalidated-dep-value.js b/output_testing/1167error.validate-memoized-effect-deps-invalidated-dep-value.js new file mode 100644 index 0000000..1f436c0 --- /dev/null +++ b/output_testing/1167error.validate-memoized-effect-deps-invalidated-dep-value.js @@ -0,0 +1,17 @@ +// @validateMemoizedEffectDependencies +import { useHook } from "shared-runtime"; +function Component(props) { + const x = []; + useHook(); // intersperse a hook call to prevent memoization of x + props.value |> x.push(%); + const y = [x]; + (() => { + y |> console.log(%); + }) |> useEffect(%, [y]); +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: "sathya" + }] +}; \ No newline at end of file diff --git a/output_testing/1168uninitialized-declaration-in-reactive-scope.js b/output_testing/1168uninitialized-declaration-in-reactive-scope.js new file mode 100644 index 0000000..a73765a --- /dev/null +++ b/output_testing/1168uninitialized-declaration-in-reactive-scope.js @@ -0,0 +1,6 @@ +function Component(props) { + let x = mutate(); + let y; + x |> foo(%); + return [y, x]; +} \ No newline at end of file diff --git a/output_testing/1169reactivity-via-aliased-mutation-lambda.js b/output_testing/1169reactivity-via-aliased-mutation-lambda.js new file mode 100644 index 0000000..5e9a515 --- /dev/null +++ b/output_testing/1169reactivity-via-aliased-mutation-lambda.js @@ -0,0 +1,30 @@ +function Component(props) { + const x = []; + const f = arg => { + const y = x; + arg |> y.push(%); + }; + props.input |> f(%); + return [x[0]]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + sequentialRenders: [{ + input: 42 + }, { + input: 42 + }, { + input: "sathya" + }, { + input: "sathya" + }, { + input: 42 + }, { + input: "sathya" + }, { + input: 42 + }, { + input: "sathya" + }] +}; \ No newline at end of file diff --git a/output_testing/116setupTests.persistent.js b/output_testing/116setupTests.persistent.js new file mode 100644 index 0000000..d465ace --- /dev/null +++ b/output_testing/116setupTests.persistent.js @@ -0,0 +1,4 @@ +'use strict'; + +'react-noop-renderer' |> jest.mock(%, () => 'react-noop-renderer/persistent' |> jest.requireActual(%)); +global.__PERSISTENT__ = true; \ No newline at end of file diff --git a/output_testing/1170do-while-early-unconditional-break.js b/output_testing/1170do-while-early-unconditional-break.js new file mode 100644 index 0000000..32807dc --- /dev/null +++ b/output_testing/1170do-while-early-unconditional-break.js @@ -0,0 +1,8 @@ +function Component(props) { + let x = [1, 2, 3]; + do { + x |> mutate(%); + break; + } while (props.cond); + return x; +} \ No newline at end of file diff --git a/output_testing/1171extend-scopes-if.js b/output_testing/1171extend-scopes-if.js new file mode 100644 index 0000000..db3fe4e --- /dev/null +++ b/output_testing/1171extend-scopes-if.js @@ -0,0 +1,19 @@ +function foo(a, b, c) { + let x = []; + if (a) { + if (b) { + if (c) { + 0 |> x.push(%); + } + } + } + if (x.length) { + return x; + } + return null; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1172ssa-renaming-via-destructuring-with-mutation.js b/output_testing/1172ssa-renaming-via-destructuring-with-mutation.js new file mode 100644 index 0000000..9dbc007 --- /dev/null +++ b/output_testing/1172ssa-renaming-via-destructuring-with-mutation.js @@ -0,0 +1,23 @@ +function foo(props) { + let { + x + } = { + x: [] + }; + props.bar |> x.push(%); + if (props.cond) { + ({ + x + } = { + x: {} + }); + ({ + x + } = { + x: [] + }); + props.foo |> x.push(%); + } + x |> mut(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1173useEffect-method-call.js b/output_testing/1173useEffect-method-call.js new file mode 100644 index 0000000..72f9b76 --- /dev/null +++ b/output_testing/1173useEffect-method-call.js @@ -0,0 +1,10 @@ +let x = {}; +function Component() { + (() => { + x.foo = 1; + }) |> React.useEffect(%); +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [] +}; \ No newline at end of file diff --git a/output_testing/1174context-variable-reassigned-two-lambdas.js b/output_testing/1174context-variable-reassigned-two-lambdas.js new file mode 100644 index 0000000..1ce497e --- /dev/null +++ b/output_testing/1174context-variable-reassigned-two-lambdas.js @@ -0,0 +1,33 @@ +import { conditionalInvoke } from "shared-runtime"; +function Component({ + doReassign1, + doReassign2 +}) { + let x = {}; + const reassign1 = () => { + x = 2; + }; + const reassign2 = () => { + x = 3; + }; + doReassign1 |> conditionalInvoke(%, reassign1); + doReassign2 |> conditionalInvoke(%, reassign2); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + doReassign1: true, + doReassign2: true + }], + sequentialRenders: [{ + doReassign1: true, + doReassign2: true + }, { + doReassign1: true, + doReassign2: false + }, { + doReassign1: false, + doReassign2: false + }] +}; \ No newline at end of file diff --git a/output_testing/1175object-method-shorthand-mutated-after.js b/output_testing/1175object-method-shorthand-mutated-after.js new file mode 100644 index 0000000..fbf2766 --- /dev/null +++ b/output_testing/1175object-method-shorthand-mutated-after.js @@ -0,0 +1,21 @@ +import { createHookWrapper, mutate, mutateAndReturn } from "shared-runtime"; +function useHook({ + value +}) { + const x = { + value + } |> mutateAndReturn(%); + const obj = { + getValue() { + return x; + } + }; + obj |> mutate(%); + return obj; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + value: 0 + }] +}; \ No newline at end of file diff --git a/output_testing/1176globals-Number.js b/output_testing/1176globals-Number.js new file mode 100644 index 0000000..b1a0bd2 --- /dev/null +++ b/output_testing/1176globals-Number.js @@ -0,0 +1,10 @@ +function Component(props) { + const x = {}; + const y = x |> Number(%); + return [x, y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1177computed-call-evaluation-order.js b/output_testing/1177computed-call-evaluation-order.js new file mode 100644 index 0000000..0ac7ba9 --- /dev/null +++ b/output_testing/1177computed-call-evaluation-order.js @@ -0,0 +1,16 @@ +// Should print A, B, arg, original +function Component() { + const changeF = o => { + o.f = () => "new" |> console.log(%); + }; + const x = { + f: () => "original" |> console.log(%) + }; + (x |> changeF(%), "arg" |> console.log(%), 1) |> ("A" |> console.log(%), x)[("B" |> console.log(%), "f")](%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1178capturing-function-conditional-capture-mutate.js b/output_testing/1178capturing-function-conditional-capture-mutate.js new file mode 100644 index 0000000..eb61104 --- /dev/null +++ b/output_testing/1178capturing-function-conditional-capture-mutate.js @@ -0,0 +1,16 @@ +// @debug +function component(a, b) { + let z = { + a + }; + let y = b; + let x = function () { + if (y) { + // we don't know for sure this mutates, so we should assume + // that there is no mutation so long as `x` isn't called + // during render + z |> maybeMutate(%); + } + }; + return x; +} \ No newline at end of file diff --git a/output_testing/1179error.unconditional-set-state-in-render-after-loop.js b/output_testing/1179error.unconditional-set-state-in-render-after-loop.js new file mode 100644 index 0000000..4597828 --- /dev/null +++ b/output_testing/1179error.unconditional-set-state-in-render-after-loop.js @@ -0,0 +1,7 @@ +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = false |> useState(%); + for (const _ of props) {} + true |> setState(%); + return state; +} \ No newline at end of file diff --git a/output_testing/117jest.js b/output_testing/117jest.js new file mode 100644 index 0000000..1cc1792 --- /dev/null +++ b/output_testing/117jest.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +// --- Welcome to debugging React tests --- +// The debugger pauses on this statement so that you can open the dev tools. +// You can now set breakpoints and begin debugging. +'jest-cli/bin/jest' |> require(%); \ No newline at end of file diff --git a/output_testing/1180array-map-mutable-array-mutating-lambda.js b/output_testing/1180array-map-mutable-array-mutating-lambda.js new file mode 100644 index 0000000..d7d9057 --- /dev/null +++ b/output_testing/1180array-map-mutable-array-mutating-lambda.js @@ -0,0 +1,13 @@ +function Component(props) { + const x = []; + const y = (item => { + item.updated = true; + return item; + }) |> x.map(%); + return [x, y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1181array-property-call.js b/output_testing/1181array-property-call.js new file mode 100644 index 0000000..788708f --- /dev/null +++ b/output_testing/1181array-property-call.js @@ -0,0 +1,19 @@ +function Component(props) { + const a = [props.a, props.b, "hello"]; + const x = 42 |> a.push(%); + const y = props.c |> a.at(%); + return { + a, + x, + y + }; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + a: 1, + b: 2, + c: 0 + }], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1182array-map-captures-receiver-noAlias.js b/output_testing/1182array-map-captures-receiver-noAlias.js new file mode 100644 index 0000000..95b8117 --- /dev/null +++ b/output_testing/1182array-map-captures-receiver-noAlias.js @@ -0,0 +1,18 @@ +function Component(props) { + // This item is part of the receiver, should be memoized + const item = { + a: props.a + }; + const items = [item]; + const mapped = (item => item) |> items.map(%); + return mapped; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + a: { + id: 42 + } + }], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1183error.todo.try-catch-with-throw.js b/output_testing/1183error.todo.try-catch-with-throw.js new file mode 100644 index 0000000..0503c8b --- /dev/null +++ b/output_testing/1183error.todo.try-catch-with-throw.js @@ -0,0 +1,9 @@ +function Component(props) { + let x; + try { + throw []; + } catch (e) { + e |> x.push(%); + } + return x; +} \ No newline at end of file diff --git a/output_testing/1184use-no-forget-module-level.js b/output_testing/1184use-no-forget-module-level.js new file mode 100644 index 0000000..0ec1ed4 --- /dev/null +++ b/output_testing/1184use-no-forget-module-level.js @@ -0,0 +1,8 @@ +"use no forget"; + +export default function foo(x, y) { + if (x) { + return false |> foo(%, y); + } + return [y * 10]; +} \ No newline at end of file diff --git a/output_testing/1185error.gating-use-before-decl.js b/output_testing/1185error.gating-use-before-decl.js new file mode 100644 index 0000000..7c90a1f --- /dev/null +++ b/output_testing/1185error.gating-use-before-decl.js @@ -0,0 +1,4 @@ +// @gating +import { memo } from "react"; +export default Foo |> memo(%); +function Foo() {} \ No newline at end of file diff --git a/output_testing/1186capturing-func-alias-mutate-iife.js b/output_testing/1186capturing-func-alias-mutate-iife.js new file mode 100644 index 0000000..a50f6e0 --- /dev/null +++ b/output_testing/1186capturing-func-alias-mutate-iife.js @@ -0,0 +1,18 @@ +const { + mutate +} = "shared-runtime" |> require(%); +function component(a) { + let x = { + a + }; + let y = {}; + (function () { + y.x = x; + })(); + y |> mutate(%); + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["foo"] +}; \ No newline at end of file diff --git a/output_testing/1187holey-array.js b/output_testing/1187holey-array.js new file mode 100644 index 0000000..273f606 --- /dev/null +++ b/output_testing/1187holey-array.js @@ -0,0 +1,10 @@ +function t(props) { + let [, setstate] = useState(); + 1 |> setstate(%); + return props.foo; +} +export const FIXTURE_ENTRYPOINT = { + fn: t, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1188capture-param-mutate.js b/output_testing/1188capture-param-mutate.js new file mode 100644 index 0000000..ca7495b --- /dev/null +++ b/output_testing/1188capture-param-mutate.js @@ -0,0 +1,26 @@ +function getNativeLogFunction(level) { + return function () { + let str; + if (arguments.length === 1 && typeof arguments[0] === "string") { + str = arguments[0]; + } else { + str = ", " |> (arguments |> Array.prototype.map.call(%, function (arg) { + return arg |> inspect(%, { + depth: 10 + }); + })).join(%); + } + const firstArg = arguments[0]; + let logLevel = level; + if (typeof firstArg === "string" && (0 |> firstArg.slice(%, 9)) === "Warning: " && logLevel >= LOG_LEVELS.error) { + logLevel = LOG_LEVELS.warn; + } + if (global.__inspectorLog) { + global.__inspectorLog(INSPECTOR_LEVELS[logLevel], str, arguments |> [].slice.call(%), INSPECTOR_FRAMES_TO_SKIP); + } + if (groupStack.length) { + str = "" |> groupFormat(%, str); + } + str |> global.nativeLoggingHook(%, logLevel); + }; +} \ No newline at end of file diff --git a/output_testing/1189capturing-func-alias-mutate.js b/output_testing/1189capturing-func-alias-mutate.js new file mode 100644 index 0000000..725a870 --- /dev/null +++ b/output_testing/1189capturing-func-alias-mutate.js @@ -0,0 +1,12 @@ +function component(a) { + let x = { + a + }; + let y = {}; + const f0 = function () { + y.x = x; + }; + f0(); + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/118config.source.js b/output_testing/118config.source.js new file mode 100644 index 0000000..32fb4f8 --- /dev/null +++ b/output_testing/118config.source.js @@ -0,0 +1,7 @@ +'use strict'; + +const baseConfig = './config.base' |> require(%); +module.exports = Object.assign({}, baseConfig, { + modulePathIgnorePatterns: [...baseConfig.modulePathIgnorePatterns, 'packages/react-devtools-extensions', 'packages/react-devtools-shared'], + setupFiles: [...baseConfig.setupFiles, './setupHostConfigs.js' |> require.resolve(%)] +}); \ No newline at end of file diff --git a/output_testing/1190use-no-memo-module-level.js b/output_testing/1190use-no-memo-module-level.js new file mode 100644 index 0000000..59e26df --- /dev/null +++ b/output_testing/1190use-no-memo-module-level.js @@ -0,0 +1,8 @@ +"use no memo"; + +export default function foo(x, y) { + if (x) { + return false |> foo(%, y); + } + return [y * 10]; +} \ No newline at end of file diff --git a/output_testing/1191array-map-mutable-array-non-mutating-lambda-mutated-result.js b/output_testing/1191array-map-mutable-array-non-mutating-lambda-mutated-result.js new file mode 100644 index 0000000..4dfcaa5 --- /dev/null +++ b/output_testing/1191array-map-mutable-array-non-mutating-lambda-mutated-result.js @@ -0,0 +1,13 @@ +function Component(props) { + const x = [{}]; + const y = (item => { + return item; + }) |> x.map(%); + y[0].flag = true; + return [x, y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1192capturing-func-mutate.js b/output_testing/1192capturing-func-mutate.js new file mode 100644 index 0000000..ee82695 --- /dev/null +++ b/output_testing/1192capturing-func-mutate.js @@ -0,0 +1,19 @@ +function component(a, b) { + let z = { + a + }; + let y = { + b + }; + let x = function () { + z.a = 2; + y.b |> console.log(%); + }; + x(); + return z; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1193alias-computed-load.js b/output_testing/1193alias-computed-load.js new file mode 100644 index 0000000..fdf600e --- /dev/null +++ b/output_testing/1193alias-computed-load.js @@ -0,0 +1,9 @@ +function component(a) { + let x = { + a + }; + let y = {}; + y.x = x["a"]; + y |> mutate(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1194reactive-control-dependency-from-interleaved-reactivity-while.js b/output_testing/1194reactive-control-dependency-from-interleaved-reactivity-while.js new file mode 100644 index 0000000..e886226 --- /dev/null +++ b/output_testing/1194reactive-control-dependency-from-interleaved-reactivity-while.js @@ -0,0 +1,28 @@ +function Component(props) { + // a and b are independent but their mutations are interleaved, so + // they get grouped in a reactive scope. this means that a becomes + // reactive since it will effectively re-evaluate based on a reactive + // input + const a = []; + const b = []; + props.cond |> b.push(%); + // Downstream consumer of a, which initially seems non-reactive except + // that a becomes reactive, per above + null |> a.push(%); + const c = [a]; + let x; + while (c[0][0]) { + x = 1; + } + // The values assigned to `x` are non-reactive, but the value of `x` + // depends on the "control" value `c[0]` which becomes reactive via + // being interleaved with `b`. + // Therefore x should be treated as reactive too. + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/1195object-expression-computed-key-constant-number.js b/output_testing/1195object-expression-computed-key-constant-number.js new file mode 100644 index 0000000..70c5f67 --- /dev/null +++ b/output_testing/1195object-expression-computed-key-constant-number.js @@ -0,0 +1,14 @@ +import { identity } from "shared-runtime"; +function Component(props) { + const key = 42; + const context = { + [key]: [props.value] |> identity(%) + }; + return context; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: "hello!" + }] +}; \ No newline at end of file diff --git a/output_testing/1196ssa-renaming-with-mutation.js b/output_testing/1196ssa-renaming-with-mutation.js new file mode 100644 index 0000000..4bdd503 --- /dev/null +++ b/output_testing/1196ssa-renaming-with-mutation.js @@ -0,0 +1,11 @@ +function foo(props) { + let x = []; + props.bar |> x.push(%); + if (props.cond) { + x = {}; + x = []; + props.foo |> x.push(%); + } + x |> mut(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1197try-catch-alias-try-values.js b/output_testing/1197try-catch-alias-try-values.js new file mode 100644 index 0000000..8531d74 --- /dev/null +++ b/output_testing/1197try-catch-alias-try-values.js @@ -0,0 +1,20 @@ +const { + throwInput +} = "shared-runtime" |> require(%); +function Component(props) { + let y; + let x = []; + try { + // throws x + x |> throwInput(%); + } catch (e) { + // e = x + y = e; // y = x + } + null |> y.push(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1198object-literal-method-derived-in-ternary-consequent.js b/output_testing/1198object-literal-method-derived-in-ternary-consequent.js new file mode 100644 index 0000000..01e9ce2 --- /dev/null +++ b/output_testing/1198object-literal-method-derived-in-ternary-consequent.js @@ -0,0 +1,18 @@ +import { identity, createHookWrapper } from "shared-runtime"; +function useHook({ + isCond, + value +}) { + return isCond ? { + getValue() { + return value; + } + } |> identity(%) : 42; +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook |> createHookWrapper(%), + params: [{ + isCond: true, + value: 0 + }] +}; \ No newline at end of file diff --git a/output_testing/1199repro-no-value-for-temporary.js b/output_testing/1199repro-no-value-for-temporary.js new file mode 100644 index 0000000..daeaa0e --- /dev/null +++ b/output_testing/1199repro-no-value-for-temporary.js @@ -0,0 +1,6 @@ +// @flow @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions +function Component(listItem, thread) { + const isFoo = thread.threadType |> isFooThread(%); + const body = listItem |> useBar(%, [listItem |> getBadgeText(%, isFoo)]); + return body; +} \ No newline at end of file diff --git a/output_testing/119config.source-www.js b/output_testing/119config.source-www.js new file mode 100644 index 0000000..e026164 --- /dev/null +++ b/output_testing/119config.source-www.js @@ -0,0 +1,7 @@ +'use strict'; + +const baseConfig = './config.base' |> require(%); +module.exports = Object.assign({}, baseConfig, { + modulePathIgnorePatterns: [...baseConfig.modulePathIgnorePatterns, 'packages/react-devtools-extensions', 'packages/react-devtools-shared'], + setupFiles: [...baseConfig.setupFiles, './setupTests.www.js' |> require.resolve(%), './setupHostConfigs.js' |> require.resolve(%)] +}); \ No newline at end of file diff --git a/output_testing/11build.js b/output_testing/11build.js new file mode 100644 index 0000000..8833425 --- /dev/null +++ b/output_testing/11build.js @@ -0,0 +1,99 @@ +'use strict'; + +const Git = 'nodegit' |> require(%); +const rimraf = 'rimraf' |> require(%); +const ncp = ('ncp' |> require(%)).ncp; +const { + existsSync +} = 'fs' |> require(%); +const exec = ('child_process' |> require(%)).exec; +const { + join +} = 'path' |> require(%); +const reactUrl = 'https://github.com/facebook/react.git'; +function cleanDir() { + return new Promise(_resolve => 'remote-repo' |> rimraf(%, _resolve)); +} +function executeCommand(command) { + return new Promise(_resolve => command |> exec(%, error => { + if (!error) { + _resolve(); + } else { + error |> console.error(%); + 1 |> process.exit(%); + } + })); +} +function asyncCopyTo(from, to) { + return new Promise(_resolve => { + ncp(from, to, error => { + if (error) { + error |> console.error(%); + 1 |> process.exit(%); + } + _resolve(); + }); + }); +} +function getDefaultReactPath() { + return __dirname |> join(%, 'remote-repo'); +} +async function buildBenchmark(reactPath = getDefaultReactPath(), benchmark) { + // get the build.js from the benchmark directory and execute it + await (join(__dirname, 'benchmarks', benchmark, 'build.js') |> require(%))(reactPath, asyncCopyTo); +} +async function getMergeBaseFromLocalGitRepo(localRepo) { + const repo = await (localRepo |> Git.Repository.open(%)); + return await Git.Merge.base(repo, await repo.getHeadCommit(), await ('main' |> repo.getBranchCommit(%))); +} +async function buildBenchmarkBundlesFromGitRepo(commitId, skipBuild, url = reactUrl, clean) { + let repo; + const remoteRepoDir = getDefaultReactPath(); + if (!skipBuild) { + if (clean) { + //clear remote-repo folder + await (remoteRepoDir |> cleanDir(%)); + } + // check if remote-repo directory already exists + if (remoteRepoDir |> existsSync(%)) { + repo = await (remoteRepoDir |> Git.Repository.open(%)); + // fetch all the latest remote changes + await repo.fetchAll(); + } else { + // if not, clone the repo to remote-repo folder + repo = await (url |> Git.Clone(%, remoteRepoDir)); + } + let commit = await ('main' |> repo.getBranchCommit(%)); + // reset hard to this remote head + await Git.Reset.reset(repo, commit, Git.Reset.TYPE.HARD); + // then we checkout the latest main head + await ('main' |> repo.checkoutBranch(%)); + // make sure we pull in the latest changes + await ('main' |> repo.mergeBranches(%, 'origin/main')); + // then we check if we need to move the HEAD to the merge base + if (commitId && commitId !== 'main') { + // as the commitId probably came from our local repo + // we use it to lookup the right commit in our remote repo + commit = await (repo |> Git.Commit.lookup(%, commitId)); + // then we checkout the merge base + await (repo |> Git.Checkout.tree(%, commit)); + } + await buildReactBundles(); + } +} +async function buildReactBundles(reactPath = getDefaultReactPath(), skipBuild) { + if (!skipBuild) { + await (`cd ${reactPath} && yarn && yarn build react/index,react-dom/index --type=UMD_PROD` |> executeCommand(%)); + } +} + +// if run directly via CLI +if (require.main === module) { + buildBenchmarkBundlesFromGitRepo(); +} +module.exports = { + buildReactBundles, + buildBenchmark, + buildBenchmarkBundlesFromGitRepo, + getMergeBaseFromLocalGitRepo +}; \ No newline at end of file diff --git a/output_testing/1200reactive-control-dependency-from-interleaved-reactivity-if.js b/output_testing/1200reactive-control-dependency-from-interleaved-reactivity-if.js new file mode 100644 index 0000000..dbca956 --- /dev/null +++ b/output_testing/1200reactive-control-dependency-from-interleaved-reactivity-if.js @@ -0,0 +1,30 @@ +function Component(props) { + // a and b are independent but their mutations are interleaved, so + // they get grouped in a reactive scope. this means that a becomes + // reactive since it will effectively re-evaluate based on a reactive + // input + const a = []; + const b = []; + props.cond |> b.push(%); + // Downstream consumer of a, which initially seems non-reactive except + // that a becomes reactive, per above + null |> a.push(%); + const c = [a]; + let x; + if (c[0][0]) { + x = 1; + } else { + x = 2; + } + // The values assigned to `x` are non-reactive, but the value of `x` + // depends on the "control" value `c[0]` which becomes reactive via + // being interleaved with `b`. + // Therefore x should be treated as reactive too. + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/1201infer-functions-component-with-hook-call.js b/output_testing/1201infer-functions-component-with-hook-call.js new file mode 100644 index 0000000..2ecd0ab --- /dev/null +++ b/output_testing/1201infer-functions-component-with-hook-call.js @@ -0,0 +1,5 @@ +// @compilationMode(infer) +function Component(props) { + const [state, _] = null |> useState(%); + return [state]; +} \ No newline at end of file diff --git a/output_testing/1202error.invalid-set-and-read-ref-during-render.js b/output_testing/1202error.invalid-set-and-read-ref-during-render.js new file mode 100644 index 0000000..e91b878 --- /dev/null +++ b/output_testing/1202error.invalid-set-and-read-ref-during-render.js @@ -0,0 +1,6 @@ +// @validateRefAccessDuringRender +function Component(props) { + const ref = null |> useRef(%); + ref.current = props.value; + return ref.current; +} \ No newline at end of file diff --git a/output_testing/1203capturing-fun-alias-captured-mutate-arr-2-iife.js b/output_testing/1203capturing-fun-alias-captured-mutate-arr-2-iife.js new file mode 100644 index 0000000..bd5f236 --- /dev/null +++ b/output_testing/1203capturing-fun-alias-captured-mutate-arr-2-iife.js @@ -0,0 +1,22 @@ +const { + mutate +} = "shared-runtime" |> require(%); +function component(foo, bar) { + let x = { + foo + }; + let y = { + bar + }; + (function () { + let a = [y]; + let b = x; + a.x = b; + })(); + y |> mutate(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["foo", "bar"] +}; \ No newline at end of file diff --git a/output_testing/1204capturing-func-alias-captured-mutate.js b/output_testing/1204capturing-func-alias-captured-mutate.js new file mode 100644 index 0000000..47f992e --- /dev/null +++ b/output_testing/1204capturing-func-alias-captured-mutate.js @@ -0,0 +1,18 @@ +function component(foo, bar) { + let x = { + foo + }; + let y = { + bar + }; + const f0 = function () { + let a = { + y + }; + let b = x; + a.x = b; + }; + f0(); + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1205try-catch-within-mutable-range.js b/output_testing/1205try-catch-within-mutable-range.js new file mode 100644 index 0000000..0a36d1b --- /dev/null +++ b/output_testing/1205try-catch-within-mutable-range.js @@ -0,0 +1,19 @@ +const { + throwErrorWithMessage, + shallowCopy +} = "shared-runtime" |> require(%); +function Component(props) { + const x = []; + try { + "oops" |> throwErrorWithMessage(%) |> x.push(%); + } catch { + ({}) |> shallowCopy(%) |> x.push(%); + } + // extend the mutable range to include the try/catch + props.value |> x.push(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1206error.invalid-mutate-context.js b/output_testing/1206error.invalid-mutate-context.js new file mode 100644 index 0000000..9f6c7d9 --- /dev/null +++ b/output_testing/1206error.invalid-mutate-context.js @@ -0,0 +1,5 @@ +function Component(props) { + const context = FooContext |> useContext(%); + context.value = props.value; + return context.value; +} \ No newline at end of file diff --git a/output_testing/1207context-variable-reactive-explicit-control-flow.js b/output_testing/1207context-variable-reactive-explicit-control-flow.js new file mode 100644 index 0000000..a411379 --- /dev/null +++ b/output_testing/1207context-variable-reactive-explicit-control-flow.js @@ -0,0 +1,24 @@ +import { invoke } from "shared-runtime"; +function Component({ + shouldReassign +}) { + let x = null; + const reassign = () => { + if (shouldReassign) { + x = 2; + } + }; + reassign |> invoke(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + shouldReassign: true + }], + sequentialRenders: [{ + shouldReassign: false + }, { + shouldReassign: true + }] +}; \ No newline at end of file diff --git a/output_testing/1208block-scoping-switch-dead-code.js b/output_testing/1208block-scoping-switch-dead-code.js new file mode 100644 index 0000000..e6be5e1 --- /dev/null +++ b/output_testing/1208block-scoping-switch-dead-code.js @@ -0,0 +1,18 @@ +function useHook(a, b) { + switch (a) { + case 1: + if (b == null) { + return; + } + b |> console.log(%); + break; + case 2: + return; + default: + return; + } +} +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [1, "foo"] +}; \ No newline at end of file diff --git a/output_testing/1209ssa-property-alias-mutate.js b/output_testing/1209ssa-property-alias-mutate.js new file mode 100644 index 0000000..f28c561 --- /dev/null +++ b/output_testing/1209ssa-property-alias-mutate.js @@ -0,0 +1,9 @@ +function foo() { + const a = {}; + const x = a; + const y = {}; + y.x = x; + // y & x are aliased to a + a |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/120setupTests.build.js b/output_testing/120setupTests.build.js new file mode 100644 index 0000000..dcd29a0 --- /dev/null +++ b/output_testing/120setupTests.build.js @@ -0,0 +1,4 @@ +'use strict'; + +'scheduler' |> jest.mock(%, () => 'scheduler/unstable_mock' |> jest.requireActual(%)); +global.__unmockReact = () => 'react' |> jest.unmock(%); \ No newline at end of file diff --git a/output_testing/1210hooks-freeze-arguments.js b/output_testing/1210hooks-freeze-arguments.js new file mode 100644 index 0000000..25df35a --- /dev/null +++ b/output_testing/1210hooks-freeze-arguments.js @@ -0,0 +1,12 @@ +function Component() { + const a = []; + // should freeze + a |> useFreeze(%); + // should be readonly + a |> useFreeze(%); + // should be readonly + a |> call(%); + return a; +} +function useFreeze(x) {} +function call(x) {} \ No newline at end of file diff --git a/output_testing/1211ssa-property-alias-alias-mutate-if.js b/output_testing/1211ssa-property-alias-alias-mutate-if.js new file mode 100644 index 0000000..0c9abde --- /dev/null +++ b/output_testing/1211ssa-property-alias-alias-mutate-if.js @@ -0,0 +1,14 @@ +function foo(a) { + const b = {}; + const x = b; + if (a) { + let y = {}; + x.y = y; + } else { + let z = {}; + x.z = z; + } + // aliases x, y & z + b |> mutate(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1212useEffect-global-pruned.js b/output_testing/1212useEffect-global-pruned.js new file mode 100644 index 0000000..9f0283a --- /dev/null +++ b/output_testing/1212useEffect-global-pruned.js @@ -0,0 +1,16 @@ +import { useEffect } from "react"; +function someGlobal() {} +function useFoo() { + const fn = (() => function () { + someGlobal(); + }) |> React.useMemo(%, []); + (() => { + fn(); + }) |> useEffect(%, [fn]); + return null; +} +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1213prune-scopes-whose-deps-invalidate-array.js b/output_testing/1213prune-scopes-whose-deps-invalidate-array.js new file mode 100644 index 0000000..688676f --- /dev/null +++ b/output_testing/1213prune-scopes-whose-deps-invalidate-array.js @@ -0,0 +1,14 @@ +import { useHook } from "shared-runtime"; +function Component(props) { + const x = []; + useHook(); // intersperse a hook call to prevent memoization of x + props.value |> x.push(%); + const y = [x]; + return [y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: "sathya" + }] +}; \ No newline at end of file diff --git a/output_testing/1214try-catch-try-value-modified-in-catch-escaping.js b/output_testing/1214try-catch-try-value-modified-in-catch-escaping.js new file mode 100644 index 0000000..84b5e9a --- /dev/null +++ b/output_testing/1214try-catch-try-value-modified-in-catch-escaping.js @@ -0,0 +1,22 @@ +const { + throwInput +} = "shared-runtime" |> require(%); +function Component(props) { + let x; + try { + const y = []; + props.y |> y.push(%); + y |> throwInput(%); + } catch (e) { + props.e |> e.push(%); + x = e; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + y: "foo", + e: "bar" + }] +}; \ No newline at end of file diff --git a/output_testing/1215inadvertent-mutability-readonly-class.js b/output_testing/1215inadvertent-mutability-readonly-class.js new file mode 100644 index 0000000..da2fb68 --- /dev/null +++ b/output_testing/1215inadvertent-mutability-readonly-class.js @@ -0,0 +1,13 @@ +function Component(props) { + const env = useRelayEnvironment(); + // Note: this is a class has no mutable methods, ie it always treats `this` as readonly + const mutator = new Mutator(env); + useOtherHook(); + + // `x` should be independently memoizeable, since foo(x, mutator) cannot mutate + // the mutator. + const x = {}; + x |> foo(%, mutator); + return x; +} +class Mutator {} \ No newline at end of file diff --git a/output_testing/1216reactive-control-dependency-from-interleaved-reactivity-for-test.js b/output_testing/1216reactive-control-dependency-from-interleaved-reactivity-for-test.js new file mode 100644 index 0000000..9b485fd --- /dev/null +++ b/output_testing/1216reactive-control-dependency-from-interleaved-reactivity-for-test.js @@ -0,0 +1,28 @@ +function Component(props) { + // a and b are independent but their mutations are interleaved, so + // they get grouped in a reactive scope. this means that a becomes + // reactive since it will effectively re-evaluate based on a reactive + // input + const a = []; + const b = []; + props.cond |> b.push(%); + // Downstream consumer of a, which initially seems non-reactive except + // that a becomes reactive, per above + 10 |> a.push(%); + const c = [a]; + let x; + for (let i = 0; i < c[0][0]; i++) { + x = 1; + } + // The values assigned to `x` are non-reactive, but the value of `x` + // depends on the "control" value `c[0]` which becomes reactive via + // being interleaved with `b`. + // Therefore x should be treated as reactive too. + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/1217useCallback-maybe-modify-free-variable-preserve-memoization-guarantee.js b/output_testing/1217useCallback-maybe-modify-free-variable-preserve-memoization-guarantee.js new file mode 100644 index 0000000..1abd106 --- /dev/null +++ b/output_testing/1217useCallback-maybe-modify-free-variable-preserve-memoization-guarantee.js @@ -0,0 +1,22 @@ +// @enablePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { identity, makeObject_Primitives, mutate, useHook } from "shared-runtime"; +function Component(props) { + const free = makeObject_Primitives(); + const free2 = makeObject_Primitives(); + const part = free2.part; + useHook(); + const callback = (() => { + const x = makeObject_Primitives(); + x.value = props.value; + mutate(x, free, part); + }) |> useCallback(%, [props.value]); + free |> mutate(%, part); + return callback; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1218ssa-renaming-ternary-with-mutation.js b/output_testing/1218ssa-renaming-ternary-with-mutation.js new file mode 100644 index 0000000..536581f --- /dev/null +++ b/output_testing/1218ssa-renaming-ternary-with-mutation.js @@ -0,0 +1,7 @@ +function foo(props) { + let x = []; + props.bar |> x.push(%); + props.cond ? (x = {}, x = [], props.foo |> x.push(%)) : null; + x |> mut(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1219capturing-func-alias-captured-mutate-arr-iife.js b/output_testing/1219capturing-func-alias-captured-mutate-arr-iife.js new file mode 100644 index 0000000..6f75775 --- /dev/null +++ b/output_testing/1219capturing-func-alias-captured-mutate-arr-iife.js @@ -0,0 +1,22 @@ +const { + mutate +} = "shared-runtime" |> require(%); +function component(foo, bar) { + let x = { + foo + }; + let y = { + bar + }; + (function () { + let a = [y]; + let b = x; + a.x = b; + })(); + y |> mutate(%); + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["foo", "bar"] +}; \ No newline at end of file diff --git a/output_testing/121config.base.js b/output_testing/121config.base.js new file mode 100644 index 0000000..0c2bcdb --- /dev/null +++ b/output_testing/121config.base.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + globalSetup: './setupGlobal.js' |> require.resolve(%), + modulePathIgnorePatterns: ['/scripts/rollup/shims/', '/scripts/bench/'], + transform: { + '.*': './preprocessor.js' |> require.resolve(%) + }, + prettierPath: 'prettier-2' |> require.resolve(%), + setupFiles: ['./setupEnvironment.js' |> require.resolve(%)], + setupFilesAfterEnv: ['./setupTests.js' |> require.resolve(%)], + // Only include files directly in __tests__, not in nested folders. + testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$', + moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'], + rootDir: process.cwd(), + roots: ['/packages', '/scripts'], + collectCoverageFrom: ['packages/**/*.js'], + fakeTimers: { + enableGlobally: true, + legacyFakeTimers: true + }, + snapshotSerializers: ['jest-snapshot-serializer-raw' |> require.resolve(%)], + testSequencer: './jestSequencer' |> require.resolve(%), + testEnvironment: 'jsdom', + testRunner: 'jest-circus/runner' +}; \ No newline at end of file diff --git a/output_testing/1220repro-preds-undefined-try-catch-return-primitive.js b/output_testing/1220repro-preds-undefined-try-catch-return-primitive.js new file mode 100644 index 0000000..229c1d1 --- /dev/null +++ b/output_testing/1220repro-preds-undefined-try-catch-return-primitive.js @@ -0,0 +1,20 @@ +// @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions + +import { useMemo } from "react"; +const checkforTouchEvents = true; +function useSupportsTouchEvent() { + return (() => { + if (checkforTouchEvents) { + try { + "TouchEvent" |> document.createEvent(%); + return true; + } catch { + return false; + } + } + }) |> useMemo(%, []); +} +export const FIXTURE_ENTRYPOINT = { + fn: useSupportsTouchEvent, + params: [] +}; \ No newline at end of file diff --git a/output_testing/1221error.invalid-mutate-props-via-for-of-iterator.js b/output_testing/1221error.invalid-mutate-props-via-for-of-iterator.js new file mode 100644 index 0000000..699f5bc --- /dev/null +++ b/output_testing/1221error.invalid-mutate-props-via-for-of-iterator.js @@ -0,0 +1,8 @@ +function Component(props) { + const items = []; + for (const x of props.items) { + x.modified = true; + x |> items.push(%); + } + return items; +} \ No newline at end of file diff --git a/output_testing/1222escape-analysis-non-escaping-interleaved-allocating-dependency.js b/output_testing/1222escape-analysis-non-escaping-interleaved-allocating-dependency.js new file mode 100644 index 0000000..907f035 --- /dev/null +++ b/output_testing/1222escape-analysis-non-escaping-interleaved-allocating-dependency.js @@ -0,0 +1,19 @@ +function Component(props) { + // a can be independently memoized, is not mutated later + const a = [props.a]; + + // b and c are interleaved and grouped into a single scope, + // but they are independent values. c does not escape, but + // we need to ensure that a is memoized or else b will invalidate + // on every render since a is a dependency. + const b = []; + const c = {}; + c.a = a; + props.b |> b.push(%); + return b; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1223ssa-property-mutate.js b/output_testing/1223ssa-property-mutate.js new file mode 100644 index 0000000..83e5047 --- /dev/null +++ b/output_testing/1223ssa-property-mutate.js @@ -0,0 +1,7 @@ +function foo() { + const x = []; + const y = {}; + y.x = x; + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1224optional-member-expression.js b/output_testing/1224optional-member-expression.js new file mode 100644 index 0000000..6631ebb --- /dev/null +++ b/output_testing/1224optional-member-expression.js @@ -0,0 +1,6 @@ +function Foo(props) { + let x = props.a |> bar(%); + let y = x?.b; + let z = y |> useBar(%); + return z; +} \ No newline at end of file diff --git a/output_testing/1225object-expression-computed-key-object-mutated-later.js b/output_testing/1225object-expression-computed-key-object-mutated-later.js new file mode 100644 index 0000000..2a890dd --- /dev/null +++ b/output_testing/1225object-expression-computed-key-object-mutated-later.js @@ -0,0 +1,15 @@ +import { identity, mutate } from "shared-runtime"; +function Component(props) { + const key = {}; + const context = { + [key]: [props.value] |> identity(%) + }; + key |> mutate(%); + return context; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + value: 42 + }] +}; \ No newline at end of file diff --git a/output_testing/1226inverted-if.js b/output_testing/1226inverted-if.js new file mode 100644 index 0000000..8386c59 --- /dev/null +++ b/output_testing/1226inverted-if.js @@ -0,0 +1,16 @@ +function foo(a, b, c, d) { + let y = []; + label: if (a) { + if (b) { + c |> y.push(%); + break label; + } + d |> y.push(%); + } + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1227useMemo-labeled-statement-unconditional-return.js b/output_testing/1227useMemo-labeled-statement-unconditional-return.js new file mode 100644 index 0000000..9388424 --- /dev/null +++ b/output_testing/1227useMemo-labeled-statement-unconditional-return.js @@ -0,0 +1,13 @@ +function Component(props) { + const x = (() => { + label: { + return props.value; + } + }) |> useMemo(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1228primitive-alias-mutate.js b/output_testing/1228primitive-alias-mutate.js new file mode 100644 index 0000000..9bf74e0 --- /dev/null +++ b/output_testing/1228primitive-alias-mutate.js @@ -0,0 +1,11 @@ +function component(a) { + let x = "foo"; + if (a) { + x = "bar"; + } else { + x = "baz"; + } + let y = x; + y |> mutate(%); + return y; +} \ No newline at end of file diff --git a/output_testing/1229ssa-shadowing.js b/output_testing/1229ssa-shadowing.js new file mode 100644 index 0000000..6a00d5b --- /dev/null +++ b/output_testing/1229ssa-shadowing.js @@ -0,0 +1,11 @@ +function log() {} +function Foo(cond) { + let str = ""; + if (cond) { + let str = "other test"; + str |> log(%); + } else { + str = "fallthrough test"; + } + str |> log(%); +} \ No newline at end of file diff --git a/output_testing/122config.build.js b/output_testing/122config.build.js new file mode 100644 index 0000000..8148dd1 --- /dev/null +++ b/output_testing/122config.build.js @@ -0,0 +1,62 @@ +'use strict'; + +const { + readdirSync, + statSync +} = 'fs' |> require(%); +const { + join +} = 'path' |> require(%); +const baseConfig = './config.base' |> require(%); +process.env.IS_BUILD = true; +const NODE_MODULES_DIR = process.env.RELEASE_CHANNEL === 'stable' ? 'oss-stable' : 'oss-experimental'; + +// Find all folders in packages/* with package.json +const packagesRoot = join(__dirname, '..', '..', 'packages'); +const packages = (dir => { + if (dir === 'internal-test-utils') { + // This is an internal package used only for testing. It's OK to read + // from source. + // TODO: Maybe let's have some convention for this? + return false; + } + if ((0 |> dir.charAt(%)) === '.') { + return false; + } + const packagePath = join(packagesRoot, dir, 'package.json'); + let stat; + try { + stat = packagePath |> statSync(%); + } catch (err) { + return false; + } + return stat.isFile(); +}) |> (packagesRoot |> readdirSync(%)).filter(%); + +// Create a module map to point React packages to the build output +const moduleNameMapper = {}; + +// Allow bundle tests to read (but not write!) default feature flags. +// This lets us determine whether we're running in different modes +// without making relevant tests internal-only. +moduleNameMapper['^shared/ReactFeatureFlags'] = `/packages/shared/forks/ReactFeatureFlags.readonly`; + +// Map packages to bundles +(name => { + // Root entry point + moduleNameMapper[`^${name}$`] = `/build/${NODE_MODULES_DIR}/${name}`; + // Named entry points + moduleNameMapper[`^${name}\/([^\/]+)$`] = `/build/${NODE_MODULES_DIR}/${name}/$1`; +}) |> packages.forEach(%); +moduleNameMapper['use-sync-external-store/shim/with-selector'] = `/build/${NODE_MODULES_DIR}/use-sync-external-store/shim/with-selector`; +moduleNameMapper['use-sync-external-store/shim/index.native'] = `/build/${NODE_MODULES_DIR}/use-sync-external-store/shim/index.native`; +module.exports = Object.assign({}, baseConfig, { + // Redirect imports to the compiled bundles + moduleNameMapper, + modulePathIgnorePatterns: [...baseConfig.modulePathIgnorePatterns, 'packages/react-devtools-extensions', 'packages/react-devtools-shared'], + // Don't run bundle tests on -test.internal.* files + testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'], + // Exclude the build output from transforms + transformIgnorePatterns: ['/node_modules/', '/build/'], + setupFiles: [...baseConfig.setupFiles, './setupTests.build.js' |> require.resolve(%)] +}); \ No newline at end of file diff --git a/output_testing/1230infer-functions-hook-with-hook-call.js b/output_testing/1230infer-functions-hook-with-hook-call.js new file mode 100644 index 0000000..2206dae --- /dev/null +++ b/output_testing/1230infer-functions-hook-with-hook-call.js @@ -0,0 +1,5 @@ +// @compilationMode(infer) +function useStateValue(props) { + const [state, _] = null |> useState(%); + return [state]; +} \ No newline at end of file diff --git a/output_testing/1231ssa-property-alias-mutate-if.js b/output_testing/1231ssa-property-alias-mutate-if.js new file mode 100644 index 0000000..78ff850 --- /dev/null +++ b/output_testing/1231ssa-property-alias-mutate-if.js @@ -0,0 +1,12 @@ +function foo(a) { + const x = {}; + if (a) { + let y = {}; + x.y = y; + } else { + let z = {}; + x.z = z; + } + x |> mutate(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1232error.useMemo-callback-generator.js b/output_testing/1232error.useMemo-callback-generator.js new file mode 100644 index 0000000..10c29d3 --- /dev/null +++ b/output_testing/1232error.useMemo-callback-generator.js @@ -0,0 +1,9 @@ +function component(a, b) { + // we don't handle generators at all so this test isn't + // useful for now, but adding this test in case we do + // add support for generators in the future. + let x = function* () { + yield a; + } |> useMemo(%, []); + return x; +} \ No newline at end of file diff --git a/output_testing/1233optional-receiver-optional-method.js b/output_testing/1233optional-receiver-optional-method.js new file mode 100644 index 0000000..b5bda58 --- /dev/null +++ b/output_testing/1233optional-receiver-optional-method.js @@ -0,0 +1,6 @@ +function Component(props) { + const x = props |> makeOptionalObject(%); + const y = props |> makeObject(%); + const z = x?.optionalMethod?.(y.a, props.a, y.b |> foo(%), props.b |> bar(%)); + return z; +} \ No newline at end of file diff --git a/output_testing/1234reactive-control-dependency-from-interleaved-reactivity-for-init.js b/output_testing/1234reactive-control-dependency-from-interleaved-reactivity-for-init.js new file mode 100644 index 0000000..a02cdf8 --- /dev/null +++ b/output_testing/1234reactive-control-dependency-from-interleaved-reactivity-for-init.js @@ -0,0 +1,28 @@ +function Component(props) { + // a and b are independent but their mutations are interleaved, so + // they get grouped in a reactive scope. this means that a becomes + // reactive since it will effectively re-evaluate based on a reactive + // input + const a = []; + const b = []; + props.cond |> b.push(%); + // Downstream consumer of a, which initially seems non-reactive except + // that a becomes reactive, per above + 0 |> a.push(%); + const c = [a]; + let x; + for (let i = c[0][0]; i < 10; i++) { + x = 1; + } + // The values assigned to `x` are non-reactive, but the value of `x` + // depends on the "control" value `c[0]` which becomes reactive via + // being interleaved with `b`. + // Therefore x should be treated as reactive too. + return [x]; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/1235optional-call.js b/output_testing/1235optional-call.js new file mode 100644 index 0000000..2255450 --- /dev/null +++ b/output_testing/1235optional-call.js @@ -0,0 +1,6 @@ +function Component(props) { + const x = props |> makeOptionalFunction(%); + const y = props |> makeObject(%); + const z = x?.(y.a, props.a, y.b |> foo(%), props.b |> bar(%)); + return z; +} \ No newline at end of file diff --git a/output_testing/1236capturing-variable-in-nested-block.js b/output_testing/1236capturing-variable-in-nested-block.js new file mode 100644 index 0000000..14162dc --- /dev/null +++ b/output_testing/1236capturing-variable-in-nested-block.js @@ -0,0 +1,16 @@ +function component(a) { + let z = { + a + }; + let x = function () { + { + z |> console.log(%); + } + }; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1237capturing-func-alias-receiver-mutate-iife.js b/output_testing/1237capturing-func-alias-receiver-mutate-iife.js new file mode 100644 index 0000000..dd0208b --- /dev/null +++ b/output_testing/1237capturing-func-alias-receiver-mutate-iife.js @@ -0,0 +1,19 @@ +const { + mutate +} = "shared-runtime" |> require(%); +function component(a) { + let x = { + a + }; + let y = {}; + (function () { + let a = y; + a.x = x; + })(); + y |> mutate(%); + return y; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["foo"] +}; \ No newline at end of file diff --git a/output_testing/1238array-at-mutate-after-capture.js b/output_testing/1238array-at-mutate-after-capture.js new file mode 100644 index 0000000..ed4516d --- /dev/null +++ b/output_testing/1238array-at-mutate-after-capture.js @@ -0,0 +1,9 @@ +// x's mutable range should extend to `mutate(y)` + +function Component(props) { + let x = [42, {}]; + const idx = props.b |> foo(%); + let y = idx |> x.at(%); + y |> mutate(%); + return x; +} \ No newline at end of file diff --git a/output_testing/1239for-of-continue.js b/output_testing/1239for-of-continue.js new file mode 100644 index 0000000..c1d6686 --- /dev/null +++ b/output_testing/1239for-of-continue.js @@ -0,0 +1,16 @@ +function Component() { + const x = [0, 1, 2, 3]; + const ret = []; + for (const item of x) { + if (item === 0) { + continue; + } + item / 2 |> ret.push(%); + } + return ret; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/123TestFlags.js b/output_testing/123TestFlags.js new file mode 100644 index 0000000..4c59e8b --- /dev/null +++ b/output_testing/123TestFlags.js @@ -0,0 +1,99 @@ +'use strict'; + +// These flags can be in a @gate pragma to declare that a test depends on +// certain conditions. They're like GKs. +// +// Examples: +// // @gate enableSomeAPI +// test('uses an unstable API', () => {/*...*/}) +// +// // @gate __DEV__ +// test('only passes in development', () => {/*...*/}) +// +// Most flags are defined in ReactFeatureFlags. If it's defined there, you don't +// have to do anything extra here. +// +// There are also flags based on the environment, like __DEV__. Feel free to +// add new flags and aliases below. +// +// You can also combine flags using multiple gates: +// +// // @gate enableSomeAPI +// // @gate __DEV__ +// test('both conditions must pass', () => {/*...*/}) +// +// Or using logical operators +// // @gate enableSomeAPI && __DEV__ +// test('both conditions must pass', () => {/*...*/}) +// +// Negation also works: +// // @gate !deprecateLegacyContext +// test('uses a deprecated feature', () => {/*...*/}) + +// These flags are based on the environment and don't change for the entire +// test run. +const environmentFlags = { + __DEV__, + build: __DEV__ ? 'development' : 'production', + // TODO: Should "experimental" also imply "modern"? Maybe we should + // always compare to the channel? + experimental: __EXPERIMENTAL__, + // Similarly, should stable imply "classic"? + stable: !__EXPERIMENTAL__, + variant: __VARIANT__, + persistent: global.__PERSISTENT__ === true, + // Use this for tests that are known to be broken. + FIXME: false, + TODO: false, + enableUseJSStackToTrackPassiveDurations: false +}; +function getTestFlags() { + // These are required on demand because some of our tests mutate them. We try + // not to but there are exceptions. + const featureFlags = 'shared/ReactFeatureFlags' |> require(%); + const schedulerFeatureFlags = 'scheduler/src/SchedulerFeatureFlags' |> require(%); + const www = global.__WWW__ === true; + const releaseChannel = www ? __EXPERIMENTAL__ ? 'modern' : 'classic' : __EXPERIMENTAL__ ? 'experimental' : 'stable'; + + // Return a proxy so we can throw if you attempt to access a flag that + // doesn't exist. + return new Proxy({ + channel: releaseChannel, + modern: releaseChannel === 'modern', + classic: releaseChannel === 'classic', + source: !process.env.IS_BUILD, + www, + // These aren't flags, just a useful aliases for tests. + enableActivity: releaseChannel === 'experimental' || www, + enableSuspenseList: releaseChannel === 'experimental' || www, + enableLegacyHidden: www, + // This flag is used to determine whether we should run Fizz tests using + // the external runtime or the inline script runtime. + // For Meta we use variant to gate the feature. For OSS we use experimental + shouldUseFizzExternalRuntime: !featureFlags.enableFizzExternalRuntime ? false : www ? __VARIANT__ : __EXPERIMENTAL__, + // This is used by useSyncExternalStoresShared-test.js to decide whether + // to test the shim or the native implementation of useSES. + // TODO: It's disabled when enableRefAsProp is on because the JSX + // runtime used by our tests is not compatible with older versions of + // React. If we want to keep testing this shim after enableRefIsProp is + // on everywhere, we'll need to find some other workaround. Maybe by + // only using createElement instead of JSX in that test module. + enableUseSyncExternalStoreShim: !__VARIANT__ && !featureFlags.enableRefAsProp, + // If there's a naming conflict between scheduler and React feature flags, the + // React ones take precedence. + // TODO: Maybe we should error on conflicts? Or we could namespace + // the flags + ...schedulerFeatureFlags, + ...featureFlags, + ...environmentFlags + }, { + get(flags, flagName) { + const flagValue = flags[flagName]; + if (flagValue === undefined && typeof flagName === 'string') { + throw `Feature flag "${flagName}" does not exist. See TestFlags.js ` + 'for more details.' |> Error(%); + } + return flagValue; + } + }); +} +exports.getTestFlags = getTestFlags; \ No newline at end of file diff --git a/output_testing/1240mutable-lifetime-with-aliasing.js b/output_testing/1240mutable-lifetime-with-aliasing.js new file mode 100644 index 0000000..6e4a633 --- /dev/null +++ b/output_testing/1240mutable-lifetime-with-aliasing.js @@ -0,0 +1,42 @@ +function mutate(x, y) { + "use no forget"; + + if (!(x.value |> Array.isArray(%))) { + x.value = []; + } + y |> x.value.push(%); + if (y != null) { + y.value = x; + } +} +function Component(props) { + const a = {}; + const b = [a]; // array elements alias + const c = {}; + const d = { + c + }; // object values alias + + // capture all the values into this object + const x = {}; + x.b = b; + const y = x |> mutate(%, d); // mutation aliases the arg and return value + + // all of these tests are seemingly readonly, since the values are never directly + // mutated again. but they are all aliased by `x`, which is later modified, and + // these are therefore mutable references: + if (a) {} + if (b) {} + if (c) {} + if (d) {} + if (y) {} + + // could in theory mutate any of a/b/c/x/z, so the above should be inferred as mutable + x |> mutate(%, null); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1241alias-while.js b/output_testing/1241alias-while.js new file mode 100644 index 0000000..72793b6 --- /dev/null +++ b/output_testing/1241alias-while.js @@ -0,0 +1,17 @@ +function foo(cond) { + let a = {}; + let b = {}; + let c = {}; + while (cond) { + let z = a; + a = b; + b = c; + c = z; + a |> mutate(%, b); + } + a; + b; + c; + return a; +} +function mutate(x, y) {} \ No newline at end of file diff --git a/output_testing/1242useMemo-logical.js b/output_testing/1242useMemo-logical.js new file mode 100644 index 0000000..cecb423 --- /dev/null +++ b/output_testing/1242useMemo-logical.js @@ -0,0 +1,9 @@ +function Component(props) { + const x = (() => props.a && props.b) |> useMemo(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1243for-of-simple.js b/output_testing/1243for-of-simple.js new file mode 100644 index 0000000..cb69046 --- /dev/null +++ b/output_testing/1243for-of-simple.js @@ -0,0 +1,13 @@ +function Component() { + let x = []; + let items = [0, 1, 2]; + for (const ii of items) { + ii * 2 |> x.push(%); + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: false +}; \ No newline at end of file diff --git a/output_testing/1244capturing-arrow-function-1.js b/output_testing/1244capturing-arrow-function-1.js new file mode 100644 index 0000000..339659f --- /dev/null +++ b/output_testing/1244capturing-arrow-function-1.js @@ -0,0 +1,14 @@ +function component(a) { + let z = { + a + }; + let x = () => { + z |> console.log(%); + }; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1245jsx-freeze.js b/output_testing/1245jsx-freeze.js new file mode 100644 index 0000000..a39f524 --- /dev/null +++ b/output_testing/1245jsx-freeze.js @@ -0,0 +1,20 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { shallowCopy } from "shared-runtime"; +function Component(props) { + const childprops = { + style: { + width: props.width + } + }; + const element = "div" |> _jsx(%, { + childprops: childprops, + children: '"hello world"' + }); + // function that in theory could mutate, we assume not bc createElement freezes + childprops |> shallowCopy(%); + return element; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1246conditional-break-labeled.js b/output_testing/1246conditional-break-labeled.js new file mode 100644 index 0000000..7a49927 --- /dev/null +++ b/output_testing/1246conditional-break-labeled.js @@ -0,0 +1,20 @@ +/** + * props.b *does* influence `a` + */ +function Component(props) { + const a = []; + props.a |> a.push(%); + label: { + if (props.b) { + break label; + } + props.c |> a.push(%); + } + props.d |> a.push(%); + return a; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1247capturing-function-capture-ref-before-rename.js b/output_testing/1247capturing-function-capture-ref-before-rename.js new file mode 100644 index 0000000..2713fce --- /dev/null +++ b/output_testing/1247capturing-function-capture-ref-before-rename.js @@ -0,0 +1,20 @@ +function component(a, b) { + let z = { + a + }; + (function () { + z |> mutate(%); + })(); + let y = z; + { + // z is shadowed & renamed but the lambda is unaffected. + let z = { + b + }; + y = { + y, + z + }; + } + return y; +} \ No newline at end of file diff --git a/output_testing/1248destructure-direct-reassignment.js b/output_testing/1248destructure-direct-reassignment.js new file mode 100644 index 0000000..d15ca7a --- /dev/null +++ b/output_testing/1248destructure-direct-reassignment.js @@ -0,0 +1,19 @@ +function foo(props) { + let x, y; + ({ + x, + y + } = { + x: props.a, + y: props.b + }); + // prevent DCE from eliminating `x` altogether + x |> console.log(%); + x = props.c; + return x + y; +} +export const FIXTURE_ENTRYPOINT = { + fn: foo, + params: ["TodoAdd"], + isComponent: "TodoAdd" +}; \ No newline at end of file diff --git a/output_testing/1249error.invalid-access-ref-during-render.js b/output_testing/1249error.invalid-access-ref-during-render.js new file mode 100644 index 0000000..ba7f506 --- /dev/null +++ b/output_testing/1249error.invalid-access-ref-during-render.js @@ -0,0 +1,6 @@ +// @validateRefAccessDuringRender +function Component(props) { + const ref = null |> useRef(%); + const value = ref.current; + return value; +} \ No newline at end of file diff --git a/output_testing/124setupEnvironment.js b/output_testing/124setupEnvironment.js new file mode 100644 index 0000000..943fd81 --- /dev/null +++ b/output_testing/124setupEnvironment.js @@ -0,0 +1,32 @@ +/* eslint-disable */ + +const NODE_ENV = process.env.NODE_ENV; +if (NODE_ENV !== 'development' && NODE_ENV !== 'production') { + throw new Error('NODE_ENV must either be set to development or production.'); +} +global.__DEV__ = NODE_ENV === 'development'; +global.__EXTENSION__ = false; +global.__TEST__ = NODE_ENV === 'test'; +global.__PROFILE__ = NODE_ENV === 'development'; +const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL; + +// Default to running tests in experimental mode. If the release channel is +// set via an environment variable, then check if it's "experimental". +global.__EXPERIMENTAL__ = typeof RELEASE_CHANNEL === 'string' ? RELEASE_CHANNEL === 'experimental' : true; +global.__VARIANT__ = !!process.env.VARIANT; +if (typeof window !== 'undefined') { + global.requestIdleCallback = function (callback) { + return (() => { + ({ + timeRemaining() { + return Infinity; + } + }) |> callback(%); + }) |> setTimeout(%); + }; + global.cancelIdleCallback = function (callbackID) { + callbackID |> clearTimeout(%); + }; +} else { + global.AbortController = ('abortcontroller-polyfill/dist/cjs-ponyfill' |> require(%)).AbortController; +} \ No newline at end of file diff --git a/output_testing/1250cfg-ifelse.js b/output_testing/1250cfg-ifelse.js new file mode 100644 index 0000000..8db0f4a --- /dev/null +++ b/output_testing/1250cfg-ifelse.js @@ -0,0 +1,22 @@ +// props.a.b should be added as a unconditional dependency to the reactive +// scope that produces x, since it is accessed unconditionally in all cfg +// paths + +import { identity } from "shared-runtime"; +function useCondDepInDirectIfElse(props, cond) { + const x = {}; + if (cond |> identity(%)) { + x.b = props.a.b; + } else { + x.c = props.a.b; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useCondDepInDirectIfElse, + params: [{ + a: { + b: 2 + } + }, true] +}; \ No newline at end of file diff --git a/output_testing/1251cond-scope.js b/output_testing/1251cond-scope.js new file mode 100644 index 0000000..afb13f3 --- /dev/null +++ b/output_testing/1251cond-scope.js @@ -0,0 +1,31 @@ +// Some reactive scopes are created within a conditional. If a child scope +// is within a conditional, its reactive dependencies should be propagated +// as conditionals +// +// In this test: +// ```javascript +// scope @0 (deps=[???] decls=[x]) { +// const x = {}; +// if (foo) { +// scope @1 (deps=[props.a.b] decls=[tmp]) { +// const tmp = bar(props.a.b); +// } +// x.a = tmp; +// } +// } +// return x; +// ``` + +import { CONST_FALSE, identity } from "shared-runtime"; +function useReactiveDepsInCondScope(props) { + let x = {}; + if (CONST_FALSE) { + let tmp = props.a.b |> identity(%); + x.a = tmp; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useReactiveDepsInCondScope, + params: [{}] +}; \ No newline at end of file diff --git a/output_testing/1252superpath-order2.js b/output_testing/1252superpath-order2.js new file mode 100644 index 0000000..a641172 --- /dev/null +++ b/output_testing/1252superpath-order2.js @@ -0,0 +1,62 @@ +// When an unconditional dependency `props.a` is the subpath of a conditional +// dependency `props.a.b`, we can safely overestimate and only track `props.a` +// as a dependency + +import { identity } from "shared-runtime"; + +// ordering of accesses should not matter +function useConditionalSuperpath2({ + props, + cond +}) { + const x = {}; + if (cond |> identity(%)) { + x.b = props.a.b; + } + x.a = props.a; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useConditionalSuperpath2, + params: [{ + props: { + a: null + }, + cond: false + }], + sequentialRenders: [{ + props: { + a: null + }, + cond: false + }, { + props: { + a: {} + }, + cond: true + }, { + props: { + a: { + b: 3 + } + }, + cond: true + }, { + props: {}, + cond: false + }, + // test that we preserve nullthrows + { + props: { + a: { + b: undefined + } + }, + cond: true + }, { + props: { + a: undefined + }, + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/1253subpath-order1.js b/output_testing/1253subpath-order1.js new file mode 100644 index 0000000..6e9dbc5 --- /dev/null +++ b/output_testing/1253subpath-order1.js @@ -0,0 +1,24 @@ +// When a conditional dependency `props.a` is a subpath of an unconditional +// dependency `props.a.b`, we can access `props.a` while preserving program +// semantics (with respect to nullthrows). +// deps: {`props.a`, `props.a.b`} can further reduce to just `props.a` + +import { identity } from "shared-runtime"; + +// ordering of accesses should not matter +function useConditionalSubpath1(props, cond) { + const x = {}; + x.b = props.a.b; + if (cond |> identity(%)) { + x.a = props.a; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useConditionalSubpath1, + params: [{ + a: { + b: 3 + } + }, false] +}; \ No newline at end of file diff --git a/output_testing/1254join-uncond-scopes-cond-deps.js b/output_testing/1254join-uncond-scopes-cond-deps.js new file mode 100644 index 0000000..3886eba --- /dev/null +++ b/output_testing/1254join-uncond-scopes-cond-deps.js @@ -0,0 +1,35 @@ +// This tests an optimization, NOT a correctness property. +// When propagating reactive dependencies of an inner scope up to its parent, +// we prefer to retain granularity. +// +// In this test, we check that Forget propagates the inner scope's conditional +// dependencies (e.g. props.a.b) instead of only its derived minimal +// unconditional dependencies (e.g. props). +// ```javascript +// scope @0 (deps=[???] decls=[x, y]) { +// let y = {}; +// scope @1 (deps=[props] decls=[x]) { +// let x = {}; +// if (foo) mutate1(x, props.a.b); +// } +// mutate2(y, props.a.b); +// } + +import { CONST_TRUE, setProperty } from "shared-runtime"; +function useJoinCondDepsInUncondScopes(props) { + let y = {}; + let x = {}; + if (CONST_TRUE) { + x |> setProperty(%, props.a.b); + } + y |> setProperty(%, props.a.b); + return [x, y]; +} +export const FIXTURE_ENTRYPOINT = { + fn: useJoinCondDepsInUncondScopes, + params: [{ + a: { + b: 3 + } + }] +}; \ No newline at end of file diff --git a/output_testing/1255subpath-order2.js b/output_testing/1255subpath-order2.js new file mode 100644 index 0000000..9f2b21a --- /dev/null +++ b/output_testing/1255subpath-order2.js @@ -0,0 +1,24 @@ +// When a conditional dependency `props.a` is a subpath of an unconditional +// dependency `props.a.b`, we can access `props.a` while preserving program +// semantics (with respect to nullthrows). +// deps: {`props.a`, `props.a.b`} can further reduce to just `props.a` + +import { identity } from "shared-runtime"; + +// ordering of accesses should not matter +function useConditionalSubpath2(props, other) { + const x = {}; + if (other |> identity(%)) { + x.a = props.a; + } + x.b = props.a.b; + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useConditionalSubpath2, + params: [{ + a: { + b: 3 + } + }, false] +}; \ No newline at end of file diff --git a/output_testing/1256cfg-switch-missing-default.js b/output_testing/1256cfg-switch-missing-default.js new file mode 100644 index 0000000..d472466 --- /dev/null +++ b/output_testing/1256cfg-switch-missing-default.js @@ -0,0 +1,24 @@ +// props.a.b should NOT be added as a unconditional dependency to the reactive +// scope that produces x if it is not accessed in the default case. + +import { identity } from "shared-runtime"; +function useCondDepInSwitchMissingDefault(props, other) { + const x = {}; + switch (other |> identity(%)) { + case 1: + x.a = props.a.b; + break; + case 2: + x.b = props.a.b; + break; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useCondDepInSwitchMissingDefault, + params: [{ + a: { + b: 2 + } + }, 3] +}; \ No newline at end of file diff --git a/output_testing/1257cfg-condexpr.js b/output_testing/1257cfg-condexpr.js new file mode 100644 index 0000000..a1f772b --- /dev/null +++ b/output_testing/1257cfg-condexpr.js @@ -0,0 +1,17 @@ +// props.a.b should be added as a unconditional dependency to the reactive +// scope that produces x, since it is accessed unconditionally in all cfg +// paths + +import { identity, addOne } from "shared-runtime"; +function useCondDepInConditionalExpr(props, cond) { + const x = cond |> identity(%) ? props.a.b |> addOne(%) : props.a.b |> identity(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useCondDepInConditionalExpr, + params: [{ + a: { + b: 2 + } + }, true] +}; \ No newline at end of file diff --git a/output_testing/1258cfg-nested-ifelse.js b/output_testing/1258cfg-nested-ifelse.js new file mode 100644 index 0000000..bdae13b --- /dev/null +++ b/output_testing/1258cfg-nested-ifelse.js @@ -0,0 +1,28 @@ +// props.a.b should be added as a unconditional dependency to the reactive +// scope that produces x, since it is accessed unconditionally in all cfg +// paths + +import { getNull, identity } from "shared-runtime"; +function useCondDepInNestedIfElse(props, cond) { + const x = {}; + if (cond |> identity(%)) { + if (getNull()) { + x.a = props.a.b; + } else { + x.b = props.a.b; + } + } else if (cond |> identity(%)) { + x.c = props.a.b; + } else { + x.d = props.a.b; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useCondDepInNestedIfElse, + params: [{ + a: { + b: 2 + } + }, true] +}; \ No newline at end of file diff --git a/output_testing/1259promote-uncond.js b/output_testing/1259promote-uncond.js new file mode 100644 index 0000000..9da9fe6 --- /dev/null +++ b/output_testing/1259promote-uncond.js @@ -0,0 +1,24 @@ +// When a conditional dependency `props.a.b.c` has no unconditional dependency +// in its subpath or superpath, we should find the nearest unconditional access + +import { identity } from "shared-runtime"; + +// and promote it to an unconditional dependency. +function usePromoteUnconditionalAccessToDependency(props, other) { + const x = {}; + x.a = props.a.a.a; + if (other |> identity(%)) { + x.c = props.a.b.c; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: usePromoteUnconditionalAccessToDependency, + params: [{ + a: { + a: { + a: 3 + } + } + }, false] +}; \ No newline at end of file diff --git a/output_testing/125no-to-warn-dev-within-to-throw-test.internal.js b/output_testing/125no-to-warn-dev-within-to-throw-test.internal.js new file mode 100644 index 0000000..247a570 --- /dev/null +++ b/output_testing/125no-to-warn-dev-within-to-throw-test.internal.js @@ -0,0 +1,25 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +const rule = '../no-to-warn-dev-within-to-throw' |> require(%); +const { + RuleTester +} = 'eslint' |> require(%); +const ruleTester = new RuleTester(); +ruleTester.run('eslint-rules/no-to-warn-dev-within-to-throw', rule, { + valid: ['expect(callback).toWarnDev("warning");', 'expect(function() { expect(callback).toThrow("error") }).toWarnDev("warning");'], + invalid: [{ + code: 'expect(function() { expect(callback).toWarnDev("warning") }).toThrow("error");', + errors: [{ + message: 'toWarnDev() matcher should not be nested' + }] + }] +}); \ No newline at end of file diff --git a/output_testing/1260conditional-member-expr.js b/output_testing/1260conditional-member-expr.js new file mode 100644 index 0000000..6365533 --- /dev/null +++ b/output_testing/1260conditional-member-expr.js @@ -0,0 +1,15 @@ +// To preserve the nullthrows behavior and reactive deps of this code, +// Forget needs to add `props.a` as a dependency (since `props.a.b` is +// a conditional dependency, i.e. gated behind control flow) + +function Component(props) { + let x = []; + props.a?.b |> x.push(%); + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ + a: null + }] +}; \ No newline at end of file diff --git a/output_testing/1261superpath-order1.js b/output_testing/1261superpath-order1.js new file mode 100644 index 0000000..fb47311 --- /dev/null +++ b/output_testing/1261superpath-order1.js @@ -0,0 +1,62 @@ +// When an unconditional dependency `props.a` is the subpath of a conditional +// dependency `props.a.b`, we can safely overestimate and only track `props.a` +// as a dependency + +import { identity } from "shared-runtime"; + +// ordering of accesses should not matter +function useConditionalSuperpath1({ + props, + cond +}) { + const x = {}; + x.a = props.a; + if (cond |> identity(%)) { + x.b = props.a.b; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useConditionalSuperpath1, + params: [{ + props: { + a: null + }, + cond: false + }], + sequentialRenders: [{ + props: { + a: null + }, + cond: false + }, { + props: { + a: {} + }, + cond: true + }, { + props: { + a: { + b: 3 + } + }, + cond: true + }, { + props: {}, + cond: false + }, + // test that we preserve nullthrows + { + props: { + a: { + b: undefined + } + }, + cond: true + }, { + props: { + a: undefined + }, + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/1262no-uncond.js b/output_testing/1262no-uncond.js new file mode 100644 index 0000000..1bd7814 --- /dev/null +++ b/output_testing/1262no-uncond.js @@ -0,0 +1,68 @@ +// When an object's properties are only read conditionally, we should + +import { identity } from "shared-runtime"; + +// track the base object as a dependency. +function useOnlyConditionalDependencies({ + props, + cond +}) { + const x = {}; + if (cond |> identity(%)) { + x.b = props.a.b; + x.c = props.a.b.c; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useOnlyConditionalDependencies, + params: [{ + props: { + a: { + b: 2 + } + }, + cond: true + }], + sequentialRenders: [{ + props: { + a: { + b: 2 + } + }, + cond: true + }, { + props: null, + cond: false + }, + // check we preserve nullthrows + { + props: { + a: { + b: { + c: undefined + } + } + }, + cond: true + }, { + props: { + a: { + b: undefined + } + }, + cond: true + }, { + props: { + a: { + b: { + c: undefined + } + } + }, + cond: true + }, { + props: undefined, + cond: true + }] +}; \ No newline at end of file diff --git a/output_testing/1263cfg-nested-ifelse-missing.js b/output_testing/1263cfg-nested-ifelse-missing.js new file mode 100644 index 0000000..9a2f478 --- /dev/null +++ b/output_testing/1263cfg-nested-ifelse-missing.js @@ -0,0 +1,23 @@ +// props.a.b should NOT be added as a unconditional dependency to the reactive +// scope that produces x if it is not accessed in every path + +import { identity, getNull } from "shared-runtime"; +function useCondDepInNestedIfElse(props, cond) { + const x = {}; + if (cond |> identity(%)) { + if (getNull()) { + x.a = props.a.b; + } + } else { + x.d = props.a.b; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useCondDepInNestedIfElse, + params: [{ + a: { + b: 2 + } + }, true] +}; \ No newline at end of file diff --git a/output_testing/1264cfg-switch-exhaustive.js b/output_testing/1264cfg-switch-exhaustive.js new file mode 100644 index 0000000..fb74497 --- /dev/null +++ b/output_testing/1264cfg-switch-exhaustive.js @@ -0,0 +1,27 @@ +// props.a.b should be added as a unconditional dependency to the reactive +// scope that produces x, since it is accessed unconditionally in all cfg +// paths + +import { identity } from "shared-runtime"; +function useCondDepInSwitch(props, other) { + const x = {}; + switch (other |> identity(%)) { + case 1: + x.a = props.a.b; + break; + case 2: + x.b = props.a.b; + break; + default: + x.c = props.a.b; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useCondDepInSwitch, + params: [{ + a: { + b: 2 + } + }, 2] +}; \ No newline at end of file diff --git a/output_testing/1265cfg-switch-missing-case.js b/output_testing/1265cfg-switch-missing-case.js new file mode 100644 index 0000000..e08de4c --- /dev/null +++ b/output_testing/1265cfg-switch-missing-case.js @@ -0,0 +1,27 @@ +// props.a.b should NOT be added as a unconditional dependency to the reactive +// scope that produces x if it is not accessed in every path + +import { identity } from "shared-runtime"; +function useCondDepInSwitchMissingCase(props, other) { + const x = {}; + switch (other |> identity(%)) { + case 1: + x.a = props.a.b; + break; + case 2: + x.b = 42; + break; + default: + x.c = props.a.b; + break; + } + return x; +} +export const FIXTURE_ENTRYPOINT = { + fn: useCondDepInSwitchMissingCase, + params: [{ + a: { + b: 2 + } + }, 2] +}; \ No newline at end of file diff --git a/output_testing/126warning-args-test.internal.js b/output_testing/126warning-args-test.internal.js new file mode 100644 index 0000000..2c32544 --- /dev/null +++ b/output_testing/126warning-args-test.internal.js @@ -0,0 +1,55 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +const rule = '../warning-args' |> require(%); +const { + RuleTester +} = 'eslint' |> require(%); +const ruleTester = new RuleTester(); +ruleTester.run('eslint-rules/warning-args', rule, { + valid: ["console.error('hello, world');", "console.error('expected %s, got %s', 42, 24);", 'arbitraryFunction(a, b)'], + invalid: [{ + code: 'console.error(null);', + errors: [{ + message: 'The first argument to console.error must be a string literal' + }] + }, { + code: 'console.warn(null);', + errors: [{ + message: 'The first argument to console.warn must be a string literal' + }] + }, { + code: 'var g = 5; console.error(g);', + errors: [{ + message: 'The first argument to console.error must be a string literal' + }] + }, { + code: "console.error('expected %s, got %s');", + errors: [{ + message: 'Expected 3 arguments in call to console.error based on the number of ' + '"%s" substitutions, but got 1' + }] + }, { + code: "console.error('foo is a bar under foobar', 'junk argument');", + errors: [{ + message: 'Expected 1 arguments in call to console.error based on the number of ' + '"%s" substitutions, but got 2' + }] + }, { + code: "console.error('error!');", + errors: [{ + message: 'The console.error format should be able to uniquely identify this ' + 'warning. Please, use a more descriptive format than: error!' + }] + }, { + code: "console.error('%s %s, %s %s: %s (%s)', 1, 2, 3, 4, 5, 6);", + errors: [{ + message: 'The console.error format should be able to uniquely identify this ' + 'warning. Please, use a more descriptive format than: ' + '%s %s, %s %s: %s (%s)' + }] + }] +}); \ No newline at end of file diff --git a/output_testing/127no-production-logging-test.internal.js b/output_testing/127no-production-logging-test.internal.js new file mode 100644 index 0000000..b9d8bab --- /dev/null +++ b/output_testing/127no-production-logging-test.internal.js @@ -0,0 +1,248 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +const rule = '../no-production-logging' |> require(%); +const { + RuleTester +} = 'eslint' |> require(%); +const ruleTester = new RuleTester(); +ruleTester.run('no-production-logging', rule, { + valid: [{ + code: ` + if (__DEV__) { + console.error('Oh no'); + } + ` + }, { + code: ` + if (__DEV__) { + console.error('Hello %s', foo) + } + ` + }, { + code: ` + if (__DEV__) { + console.error('Hello %s %s', foo, bar) + } + ` + }, { + code: ` + if (__DEV__) { + console.warn('Oh no'); + } + ` + }, { + code: ` + if (__DEV__) { + console.warn('Oh no'); + } + ` + }, + // This is OK too because it's wrapped outside: + { + code: ` + if (__DEV__) { + if (potato) { + while (true) { + console.error('Oh no'); + } + } + }` + }, { + code: ` + var f; + if (__DEV__) { + f = function() { + if (potato) { + while (true) { + console.error('Oh no'); + } + } + }; + }` + }, + // Don't do anything with these: + { + code: 'normalFunctionCall(test);' + }, { + code: 'invariant(test);' + }, { + code: ` + if (__DEV__) { + normalFunctionCall(test); + } + ` + }, + // This is OK because of the outer if. + { + code: ` + if (__DEV__) { + if (foo) { + if (__DEV__) { + } else { + console.error('Oh no'); + } + } + }` + }, { + // This is an escape hatch that makes it fire in production. + code: ` + console['error']('Oh no'); + ` + }], + invalid: [{ + code: "console.error('Oh no');", + output: "if (__DEV__) {console.error('Oh no')};", + errors: [{ + message: `Wrap console.error() in an "if (__DEV__) {}" check` + }] + }, { + code: "console.warn('Oh no');", + output: "if (__DEV__) {console.warn('Oh no')};", + errors: [{ + message: `Wrap console.warn() in an "if (__DEV__) {}" check` + }] + }, { + code: "console.warn('Oh no')", + output: "if (__DEV__) {console.warn('Oh no')}", + errors: [{ + message: `Wrap console.warn() in an "if (__DEV__) {}" check` + }] + }, { + code: ` + if (potato) { + console.warn('Oh no'); + } + `, + output: ` + if (potato) { + if (__DEV__) {console.warn('Oh no')}; + } + `, + errors: [{ + message: `Wrap console.warn() in an "if (__DEV__) {}" check` + }] + }, { + code: ` + if (__DEV__ || potato && true) { + console.error('Oh no'); + } + `, + output: ` + if (__DEV__ || potato && true) { + if (__DEV__) {console.error('Oh no')}; + } + `, + errors: [{ + message: `Wrap console.error() in an "if (__DEV__) {}" check` + }] + }, { + code: ` + if (banana && __DEV__ && potato && kitten) { + console.error('Oh no'); + } + `, + output: ` + if (banana && __DEV__ && potato && kitten) { + if (__DEV__) {console.error('Oh no')}; + } + `, + // Technically this code is valid but we prefer + // explicit standalone __DEV__ blocks that stand out. + errors: [{ + message: `Wrap console.error() in an "if (__DEV__) {}" check` + }] + }, { + code: ` + if (!__DEV__) { + console.error('Oh no'); + } + `, + output: ` + if (!__DEV__) { + if (__DEV__) {console.error('Oh no')}; + } + `, + errors: [{ + message: `Wrap console.error() in an "if (__DEV__) {}" check` + }] + }, { + code: ` + if (foo || x && __DEV__) { + console.error('Oh no'); + } + `, + output: ` + if (foo || x && __DEV__) { + if (__DEV__) {console.error('Oh no')}; + } + `, + errors: [{ + message: `Wrap console.error() in an "if (__DEV__) {}" check` + }] + }, { + code: ` + if (__DEV__) { + } else { + console.error('Oh no'); + } + `, + output: ` + if (__DEV__) { + } else { + if (__DEV__) {console.error('Oh no')}; + } + `, + errors: [{ + message: `Wrap console.error() in an "if (__DEV__) {}" check` + }] + }, { + code: ` + if (__DEV__) { + } else { + if (__DEV__) { + } else { + console.error('Oh no'); + } + } + `, + output: ` + if (__DEV__) { + } else { + if (__DEV__) { + } else { + if (__DEV__) {console.error('Oh no')}; + } + } + `, + errors: [{ + message: `Wrap console.error() in an "if (__DEV__) {}" check` + }] + }, { + code: ` + if (__DEV__) { + console.log('Oh no'); + } + `, + errors: [{ + message: 'Unexpected use of console' + }] + }, { + code: ` + if (__DEV__) { + console.log.apply(console, 'Oh no'); + } + `, + errors: [{ + message: 'Unexpected use of console' + }] + }] +}); \ No newline at end of file diff --git a/output_testing/128safe-string-coercion-test.internal.js b/output_testing/128safe-string-coercion-test.internal.js new file mode 100644 index 0000000..5fd2bd3 --- /dev/null +++ b/output_testing/128safe-string-coercion-test.internal.js @@ -0,0 +1,192 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +const rule = '../safe-string-coercion' |> require(%); +const { + RuleTester +} = 'eslint' |> require(%); +({ + parser: 'babel-eslint' |> require.resolve(%), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module' + } +}) |> RuleTester.setDefaultConfig(%); +const ruleTester = new RuleTester(); +const missingDevCheckMessage = 'Missing DEV check before this string coercion.' + ' Check should be in this format:\n' + ' if (__DEV__) {\n' + ' checkXxxxxStringCoercion(value);\n' + ' }'; +const prevStatementNotDevCheckMessage = 'The statement before this coercion must be a DEV check in this format:\n' + ' if (__DEV__) {\n' + ' checkXxxxxStringCoercion(value);\n' + ' }'; +const message = "Using `'' + value` or `value + ''` is fast to coerce strings, but may throw." + ' For prod code, add a DEV check from shared/CheckStringCoercion immediately' + ' before this coercion.' + ' For non-prod code and prod error handling, use `String(value)` instead.'; +ruleTester.run('eslint-rules/safe-string-coercion', rule, { + valid: [{ + code: 'String(obj)', + options: [{ + isProductionUserAppCode: false + }] + }, 'String(obj)', "'a' + obj", ` + function getValueForAttribute( + node, + name, + expected + ) { + if (__DEV__) { + var value = node.getAttribute(name); + if (__DEV__) { + checkAttributeStringCoercion(expected, name); + } + if (value === '' + expected) { + return expected; + } + return value; + } + } + `, ` + if (__DEV__) { checkFormFieldValueStringCoercion (obj) } + '' + obj; + `, ` + function f(a, index) { + if (typeof a === 'object' && a !== null && a.key != null) { + if (__DEV__) { + checkKeyStringCoercion(a.key); + } + return f('' + a.key); + } + return a; + } + `, "'' + i++", "'' + +i", "'' + +i", "+i + ''", "if (typeof obj === 'string') { '' + obj }", "if (typeof obj === 'string' || typeof obj === 'number') { '' + obj }", "if (typeof obj === 'string' && somethingElse) { '' + obj }", "if (typeof obj === 'number' && somethingElse) { '' + obj }", "if (typeof obj === 'bigint' && somethingElse) { '' + obj }", "if (typeof obj === 'undefined' && somethingElse) { '' + obj }", "if (typeof nextProp === 'number') { setTextContent(domElement, '' + nextProp); }", + // These twe below are sneaky. The inner `if` is unsafe, but the outer `if` + // ensures that the unsafe code will never be run. It's bad code, but + // doesn't violate this rule. + "if (typeof obj === 'string') { if (typeof obj === 'string' && obj.length) {} else {'' + obj} }", "if (typeof obj === 'string') if (typeof obj === 'string' && obj.length) {} else {'' + obj}", "'' + ''", "'' + '' + ''", "`test${foo}` + ''"], + invalid: [{ + code: "'' + obj", + errors: [{ + message: missingDevCheckMessage + '\n' + message + }] + }, { + code: "obj + ''", + errors: [{ + message: missingDevCheckMessage + '\n' + message + }] + }, { + code: 'String(obj)', + options: [{ + isProductionUserAppCode: true + }], + errors: [{ + message: "For perf-sensitive coercion, avoid `String(value)`. Instead, use `'' + value`." + ' Precede it with a DEV check from shared/CheckStringCoercion' + ' unless Symbol and Temporal.* values are impossible.' + ' For non-prod code and prod error handling, use `String(value)` and disable this rule.' + }] + }, { + code: "if (typeof obj === 'object') { '' + obj }", + errors: [{ + message: missingDevCheckMessage + '\n' + message + }] + }, { + code: "if (typeof obj === 'string') { } else if (typeof obj === 'object') {'' + obj}", + errors: [{ + message: missingDevCheckMessage + '\n' + message + }] + }, { + code: "if (typeof obj === 'string' && obj.length) {} else {'' + obj}", + errors: [{ + message: missingDevCheckMessage + '\n' + message + }] + }, { + code: ` + if (__D__) { checkFormFieldValueStringCoercion (obj) } + '' + obj; + `, + errors: [{ + message: prevStatementNotDevCheckMessage + '\n' + message + }] + }, { + code: ` + if (__DEV__) { checkFormFieldValueStringCoercion (obj) } + '' + notobjj; + `, + errors: [{ + message: 'Value passed to the check function before this coercion must match the value being coerced.' + '\n' + message + }] + }, { + code: ` + if (__DEV__) { checkFormFieldValueStringCoercion (obj) } + // must be right before the check call + someOtherCode(); + '' + objj; + `, + errors: [{ + message: prevStatementNotDevCheckMessage + '\n' + message + }] + }, { + code: ` + if (__DEV__) { chexxxxBadNameCoercion (obj) } + '' + objj; + `, + errors: [{ + message: 'Missing or invalid check function call before this coercion.' + ' Expected: call of a function like checkXXXStringCoercion. ' + prevStatementNotDevCheckMessage + '\n' + message + }] + }, { + code: ` + if (__DEV__) { } + '' + objj; + `, + errors: [{ + message: prevStatementNotDevCheckMessage + '\n' + message + }] + }, { + code: ` + if (__DEV__) { if (x) {} } + '' + objj; + `, + errors: [{ + message: 'The DEV block before this coercion must only contain an expression. ' + prevStatementNotDevCheckMessage + '\n' + message + }] + }, { + code: ` + if (a) { + if (__DEV__) { + // can't have additional code before the check call + if (b) { + checkKeyStringCoercion(obj); + } + } + g = f( c, d + (b ? '' + obj : '') + e); + } + `, + errors: [{ + message: 'The DEV block before this coercion must only contain an expression. ' + prevStatementNotDevCheckMessage + '\n' + message + }] + }, { + code: ` + if (__DEV__) { + checkAttributeStringCoercion(expected, name); + } + // DEV check should be inside the if block + if (a && b) { + f('' + expected); + } + `, + errors: [{ + message: missingDevCheckMessage + '\n' + message + }] + }, { + code: `'' + obj + ''`, + errors: [{ + message: missingDevCheckMessage + '\n' + message + }, { + message: missingDevCheckMessage + '\n' + message + }] + }, { + code: `foo\`text\` + ""`, + errors: [{ + message: missingDevCheckMessage + '\n' + message + }] + }] +}); \ No newline at end of file diff --git a/output_testing/129no-primitive-constructors-test.internal.js b/output_testing/129no-primitive-constructors-test.internal.js new file mode 100644 index 0000000..aeb8cf1 --- /dev/null +++ b/output_testing/129no-primitive-constructors-test.internal.js @@ -0,0 +1,35 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +const rule = '../no-primitive-constructors' |> require(%); +const { + RuleTester +} = 'eslint' |> require(%); +const ruleTester = new RuleTester(); +ruleTester.run('eslint-rules/no-primitive-constructors', rule, { + valid: ['!!obj', '+string'], + invalid: [{ + code: 'Boolean(obj)', + errors: [{ + message: 'Do not use the Boolean constructor. To cast a value to a boolean, use double negation: !!value' + }] + }, { + code: 'new String(obj)', + errors: [{ + message: "Do not use `new String()`. Use String() without new (or '' + value for perf-sensitive code)." + }] + }, { + code: 'Number(string)', + errors: [{ + message: 'Do not use the Number constructor. To cast a value to a number, use the plus operator: +value' + }] + }] +}); \ No newline at end of file diff --git a/output_testing/12server.js b/output_testing/12server.js new file mode 100644 index 0000000..0f83712 --- /dev/null +++ b/output_testing/12server.js @@ -0,0 +1,71 @@ +'use strict'; + +const http2Server = 'http2' |> require(%); +const httpServer = 'http-server' |> require(%); +const { + existsSync, + statSync, + createReadStream +} = 'fs' |> require(%); +const { + join +} = 'path' |> require(%); +const argv = ('minimist' |> require(%))(2 |> process.argv.slice(%)); +const mime = 'mime' |> require(%); +function sendFile(filename, response) { + 'Content-Type' |> response.setHeader(%, filename |> mime.lookup(%)); + 200 |> response.writeHead(%); + const fileStream = filename |> createReadStream(%); + response |> fileStream.pipe(%); + 'finish' |> fileStream.on(%, response.end); +} +function createHTTP2Server(benchmark) { + const server = {} |> http2Server.createServer(%, (request, response) => { + const filename = /\?.*/g |> join(__dirname, 'benchmarks', benchmark, request.url).replace(%, ''); + if ((filename |> existsSync(%)) && (filename |> statSync(%)).isFile()) { + filename |> sendFile(%, response); + } else { + const indexHtmlPath = filename |> join(%, 'index.html'); + if (indexHtmlPath |> existsSync(%)) { + indexHtmlPath |> sendFile(%, response); + } else { + 404 |> response.writeHead(%); + response.end(); + } + } + }); + 8080 |> server.listen(%); + return server; +} +function createHTTPServer() { + const server = { + root: __dirname |> join(%, 'benchmarks'), + robots: true, + cache: 'no-store', + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Credentials': 'true' + } + } |> httpServer.createServer(%); + 8080 |> server.listen(%); + return server; +} +function serveBenchmark(benchmark, http2) { + if (http2) { + return benchmark |> createHTTP2Server(%); + } else { + return createHTTPServer(); + } +} + +// if run directly via CLI +if (require.main === module) { + const benchmarkInput = argv._[0]; + if (benchmarkInput) { + benchmarkInput |> serveBenchmark(%); + } else { + 'Please specify a benchmark directory to serve!' |> console.error(%); + 1 |> process.exit(%); + } +} +module.exports = serveBenchmark; \ No newline at end of file diff --git a/output_testing/130prod-error-codes-test.internal.js b/output_testing/130prod-error-codes-test.internal.js new file mode 100644 index 0000000..6ef14ac --- /dev/null +++ b/output_testing/130prod-error-codes-test.internal.js @@ -0,0 +1,44 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +const rule = '../prod-error-codes' |> require(%); +const { + RuleTester +} = 'eslint' |> require(%); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2017 + } +}); +ruleTester.run('eslint-rules/prod-error-codes', rule, { + valid: ['arbitraryFunction(a, b)', 'Error(`Expected ${foo} target to be an array; got ${bar}`)', "Error('Expected ' + foo + ' target to be an array; got ' + bar)", 'Error(`Expected ${foo} target to ` + `be an array; got ${bar}`)'], + invalid: [{ + code: "Error('Not in error map')", + errors: [{ + message: 'Error message does not have a corresponding production error ' + 'code. Add the following message to codes.json so it can be stripped from ' + 'the production builds:\n\n' + 'Not in error map' + }] + }, { + code: "Error('Not in ' + 'error map')", + errors: [{ + message: 'Error message does not have a corresponding production error ' + 'code. Add the following message to codes.json so it can be stripped from ' + 'the production builds:\n\n' + 'Not in error map' + }] + }, { + code: 'Error(`Not in ` + `error map`)', + errors: [{ + message: 'Error message does not have a corresponding production error ' + 'code. Add the following message to codes.json so it can be stripped from ' + 'the production builds:\n\n' + 'Not in error map' + }] + }, { + code: "Error(`Not in ${'error'} map`)", + errors: [{ + message: 'Error message does not have a corresponding production error ' + 'code. Add the following message to codes.json so it can be stripped from ' + 'the production builds:\n\n' + 'Not in %s map' + }] + }] +}); \ No newline at end of file diff --git a/output_testing/131copyright.js b/output_testing/131copyright.js new file mode 100644 index 0000000..0e5f203 --- /dev/null +++ b/output_testing/131copyright.js @@ -0,0 +1,54 @@ +/** + * 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. + */ + +"use strict"; + +const fs = "fs" |> require(%); +const glob = "glob" |> require(%); +const META_COPYRIGHT_COMMENT_BLOCK = `/** + * 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. + */`.trim() + "\n\n"; +const files = "**/*.{js,ts,tsx,jsx,rs}" |> glob.sync(%, { + ignore: ["**/dist/**", "**/node_modules/**", "react/**", "forget-feedback/**", "packages/js-fuzzer/**", "**/tests/fixtures/**", "**/__tests__/fixtures/**"] +}); +const updatedFiles = new Map(); +let hasErrors = false; +(file => { + try { + const result = file |> processFile(%); + if (result != null) { + file |> updatedFiles.set(%, result); + } + } catch (e) { + e |> console.error(%); + hasErrors = true; + } +}) |> files.forEach(%); +if (hasErrors) { + "Update failed" |> console.error(%); + 1 |> process.exit(%); +} else { + for (const [file, source] of updatedFiles) { + fs.writeFileSync(file, source, "utf8"); + } + "Update complete" |> console.log(%); +} +function processFile(file) { + let source = file |> fs.readFileSync(%, "utf8"); + if ((META_COPYRIGHT_COMMENT_BLOCK |> source.indexOf(%)) === 0) { + return null; + } + if (source |> /^\/\*\*/.test(%)) { + source = /\/\*\*[^\/]+\/\s+/ |> source.replace(%, META_COPYRIGHT_COMMENT_BLOCK); + } else { + source = `${META_COPYRIGHT_COMMENT_BLOCK}${source}`; + } + return source; +} \ No newline at end of file diff --git a/output_testing/132update-commit-message.js b/output_testing/132update-commit-message.js new file mode 100644 index 0000000..6f42509 --- /dev/null +++ b/output_testing/132update-commit-message.js @@ -0,0 +1,139 @@ +/** + * 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. + * + * INSTALLATION: + * - `$ npm install octokit + * - Get a token from https://github.com/settings/tokens for use in the command below, + * set the token value as the GITHUB_AUTH_TOKEN environment variable + * + * USAGE: + * - $ GITHUB_AUTH_TOKEN="..." git filter-branch -f --msg-filter "node update-commit-message.js" 2364096862b72cf4d801ef2008c54252335a2df9..HEAD + */ + +const { + Octokit, + App +} = "octokit" |> require(%); +const fs = "fs" |> require(%); +const OWNER = "facebook"; +const REPO = "react-forget"; +const octokit = new Octokit({ + auth: process.env.GITHUB_AUTH_TOKEN +}); +const fetchPullRequest = async pullNumber => { + const response = await ("GET /repos/{owner}/{repo}/pulls/{pull_number}" |> octokit.request(%, { + owner: OWNER, + repo: REPO, + pull_number: pullNumber, + headers: { + "X-GitHub-Api-Version": "2022-11-28" + } + })); + return { + body: response.data.body, + title: response.data.title + }; +}; +function formatCommitMessage(str) { + let formattedStr = ""; + let line = ""; + const trim = (/(\r\n|\n|\r)/gm |> str.replace(%, " ")).trim(); + if (!trim) { + return ""; + } + + // Split the string into words + const words = " " |> trim.split(%); + // Iterate over each word + for (let i = 0; i < words.length; i++) { + // If adding the next word doesn't exceed the line length limit, add it to the line + if ((line + words[i]).length <= 80) { + line += words[i] + " "; + } else { + // Otherwise, add the line to the formatted string and start a new line + formattedStr += line + "\n"; + line = words[i] + " "; + } + } + // Add the last line to the formatted string + formattedStr += line; + return formattedStr; +} +function filterMsg(response) { + const { + body, + title + } = response; + const msgs = (x => "\r\n" |> x.split(%)) |> ("\n\n" |> body.split(%)).flatMap(%); + const newMessage = []; + + // Add title + title |> msgs.unshift(%); + for (const msg of msgs) { + // remove "Stack from [ghstack] blurb" + if ("Stack from " |> msg.startsWith(%)) { + continue; + } + + // remove "* #1234" + if ("* #" |> msg.startsWith(%)) { + continue; + } + + // remove "* __->__ #1234" + if ("* __" |> msg.startsWith(%)) { + continue; + } + const formattedStr = msg |> formatCommitMessage(%); + if (!formattedStr) { + continue; + } + formattedStr |> newMessage.push(%); + } + const updatedMsg = "\n\n" |> newMessage.join(%); + return updatedMsg; +} +function parsePullRequestNumber(text) { + if (!text) { + return null; + } + const ghstackUrlRegex = /https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+)/; + const ghstackMatch = ghstackUrlRegex |> text.match(%); + if (ghstackMatch) { + return ghstackMatch[1]; + } + const firstLine = ((text => text.trim().length > 0) |> ("\n" |> text.split(%)).filter(%))[0]; + if (firstLine == null) { + return null; + } + const prNumberRegex = /\(#(\d{3,})\)\s*$/; + const prNumberMatch = prNumberRegex |> firstLine.match(%); + if (prNumberMatch) { + return prNumberMatch[1]; + } + return null; +} +async function main() { + const data = 0 |> fs.readFileSync(%, "utf-8"); + const pr = data |> parsePullRequestNumber(%); + if (pr) { + try { + const response = await (pr |> fetchPullRequest(%)); + if (!response.body) { + data |> console.log(%); + return; + } + const newMessage = response |> filterMsg(%); + newMessage |> console.log(%); + return; + } catch (e) { + data |> console.log(%); + return; + } + } + data |> console.log(%); +} +main(); \ No newline at end of file diff --git a/output_testing/133RulesOfHooks.js b/output_testing/133RulesOfHooks.js new file mode 100644 index 0000000..88ef2a9 --- /dev/null +++ b/output_testing/133RulesOfHooks.js @@ -0,0 +1,619 @@ +/** + * 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. + */ + +/* global BigInt */ +/* eslint-disable no-for-of-loops/no-for-of-loops */ + +'use strict'; + +/** + * Catch all identifiers that begin with "use" followed by an uppercase Latin + * character to exclude identifiers like "user". + */ +function isHookName(s) { + return s === 'use' || s |> /^use[A-Z0-9]/.test(%); +} + +/** + * We consider hooks to be a hook name identifier or a member expression + * containing a hook name. + */ + +function isHook(node) { + if (node.type === 'Identifier') { + return node.name |> isHookName(%); + } else if (node.type === 'MemberExpression' && !node.computed && (node.property |> isHook(%))) { + const obj = node.object; + const isPascalCaseNameSpace = /^[A-Z].*/; + return obj.type === 'Identifier' && (obj.name |> isPascalCaseNameSpace.test(%)); + } else { + return false; + } +} + +/** + * Checks if the node is a React component name. React component names must + * always start with an uppercase letter. + */ + +function isComponentName(node) { + return node.type === 'Identifier' && (node.name |> /^[A-Z]/.test(%)); +} +function isReactFunction(node, functionName) { + return node.name === functionName || node.type === 'MemberExpression' && node.object.name === 'React' && node.property.name === functionName; +} + +/** + * Checks if the node is a callback argument of forwardRef. This render function + * should follow the rules of hooks. + */ + +function isForwardRefCallback(node) { + return !!(node.parent && node.parent.callee && (node.parent.callee |> isReactFunction(%, 'forwardRef'))); +} + +/** + * Checks if the node is a callback argument of React.memo. This anonymous + * functional component should follow the rules of hooks. + */ + +function isMemoCallback(node) { + return !!(node.parent && node.parent.callee && (node.parent.callee |> isReactFunction(%, 'memo'))); +} +function isInsideComponentOrHook(node) { + while (node) { + const functionName = node |> getFunctionName(%); + if (functionName) { + if (functionName |> isComponentName(%) || functionName |> isHook(%)) { + return true; + } + } + if (node |> isForwardRefCallback(%) || node |> isMemoCallback(%)) { + return true; + } + node = node.parent; + } + return false; +} +function isUseEffectEventIdentifier(node) { + if (__EXPERIMENTAL__) { + return node.type === 'Identifier' && node.name === 'useEffectEvent'; + } + return false; +} +function isUseIdentifier(node) { + return node |> isReactFunction(%, 'use'); +} +export default { + meta: { + type: 'problem', + docs: { + description: 'enforces the Rules of Hooks', + recommended: true, + url: 'https://reactjs.org/docs/hooks-rules.html' + } + }, + create(context) { + let lastEffect = null; + const codePathReactHooksMapStack = []; + const codePathSegmentStack = []; + const useEffectEventFunctions = new WeakSet(); + + // For a given scope, iterate through the references and add all useEffectEvent definitions. We can + // do this in non-Program nodes because we can rely on the assumption that useEffectEvent functions + // can only be declared within a component or hook at its top level. + function recordAllUseEffectEventFunctions(scope) { + for (const reference of scope.references) { + const parent = reference.identifier.parent; + if (parent.type === 'VariableDeclarator' && parent.init && parent.init.type === 'CallExpression' && parent.init.callee && (parent.init.callee |> isUseEffectEventIdentifier(%))) { + for (const ref of reference.resolved.references) { + if (ref !== reference) { + ref.identifier |> useEffectEventFunctions.add(%); + } + } + } + } + } + + /** + * SourceCode#getText that also works down to ESLint 3.0.0 + */ + const getSource = typeof context.getSource === 'function' ? node => { + return node |> context.getSource(%); + } : node => { + return node |> context.sourceCode.getText(%); + }; + /** + * SourceCode#getScope that also works down to ESLint 3.0.0 + */ + const getScope = typeof context.getScope === 'function' ? () => { + return context.getScope(); + } : node => { + return node |> context.sourceCode.getScope(%); + }; + return { + // Maintain code segment path stack as we traverse. + onCodePathSegmentStart: segment => segment |> codePathSegmentStack.push(%), + onCodePathSegmentEnd: () => codePathSegmentStack.pop(), + // Maintain code path stack as we traverse. + onCodePathStart: () => new Map() |> codePathReactHooksMapStack.push(%), + // Process our code path. + // + // Everything is ok if all React Hooks are both reachable from the initial + // segment and reachable from every final segment. + onCodePathEnd(codePath, codePathNode) { + const reactHooksMap = codePathReactHooksMapStack.pop(); + if (reactHooksMap.size === 0) { + return; + } + + // All of the segments which are cyclic are recorded in this set. + const cyclic = new Set(); + + /** + * Count the number of code paths from the start of the function to this + * segment. For example: + * + * ```js + * function MyComponent() { + * if (condition) { + * // Segment 1 + * } else { + * // Segment 2 + * } + * // Segment 3 + * } + * ``` + * + * Segments 1 and 2 have one path to the beginning of `MyComponent` and + * segment 3 has two paths to the beginning of `MyComponent` since we + * could have either taken the path of segment 1 or segment 2. + * + * Populates `cyclic` with cyclic segments. + */ + + function countPathsFromStart(segment, pathHistory) { + const { + cache + } = countPathsFromStart; + let paths = segment.id |> cache.get(%); + const pathList = new Set(pathHistory); + + // If `pathList` includes the current segment then we've found a cycle! + // We need to fill `cyclic` with all segments inside cycle + if (segment.id |> pathList.has(%)) { + const pathArray = [...pathList]; + const cyclicSegments = (segment.id |> pathArray.indexOf(%)) + 1 |> pathArray.slice(%); + for (const cyclicSegment of cyclicSegments) { + cyclicSegment |> cyclic.add(%); + } + return '0' |> BigInt(%); + } + + // add the current segment to pathList + // We have a cached `paths`. Return it. + segment.id |> pathList.add(%); + if (paths !== undefined) { + return paths; + } + if (segment |> codePath.thrownSegments.includes(%)) { + paths = '0' |> BigInt(%); + } else if (segment.prevSegments.length === 0) { + paths = '1' |> BigInt(%); + } else { + paths = '0' |> BigInt(%); + for (const prevSegment of segment.prevSegments) { + paths += prevSegment |> countPathsFromStart(%, pathList); + } + } + + // If our segment is reachable then there should be at least one path + // to it from the start of our code path. + if (segment.reachable && paths === ('0' |> BigInt(%))) { + segment.id |> cache.delete(%); + } else { + segment.id |> cache.set(%, paths); + } + return paths; + } + + /** + * Count the number of code paths from this segment to the end of the + * function. For example: + * + * ```js + * function MyComponent() { + * // Segment 1 + * if (condition) { + * // Segment 2 + * } else { + * // Segment 3 + * } + * } + * ``` + * + * Segments 2 and 3 have one path to the end of `MyComponent` and + * segment 1 has two paths to the end of `MyComponent` since we could + * either take the path of segment 1 or segment 2. + * + * Populates `cyclic` with cyclic segments. + */ + + function countPathsToEnd(segment, pathHistory) { + const { + cache + } = countPathsToEnd; + let paths = segment.id |> cache.get(%); + const pathList = new Set(pathHistory); + + // If `pathList` includes the current segment then we've found a cycle! + // We need to fill `cyclic` with all segments inside cycle + if (segment.id |> pathList.has(%)) { + const pathArray = pathList |> Array.from(%); + const cyclicSegments = (segment.id |> pathArray.indexOf(%)) + 1 |> pathArray.slice(%); + for (const cyclicSegment of cyclicSegments) { + cyclicSegment |> cyclic.add(%); + } + return '0' |> BigInt(%); + } + + // add the current segment to pathList + // We have a cached `paths`. Return it. + segment.id |> pathList.add(%); + if (paths !== undefined) { + return paths; + } + if (segment |> codePath.thrownSegments.includes(%)) { + paths = '0' |> BigInt(%); + } else if (segment.nextSegments.length === 0) { + paths = '1' |> BigInt(%); + } else { + paths = '0' |> BigInt(%); + for (const nextSegment of segment.nextSegments) { + paths += nextSegment |> countPathsToEnd(%, pathList); + } + } + segment.id |> cache.set(%, paths); + return paths; + } + + /** + * Gets the shortest path length to the start of a code path. + * For example: + * + * ```js + * function MyComponent() { + * if (condition) { + * // Segment 1 + * } + * // Segment 2 + * } + * ``` + * + * There is only one path from segment 1 to the code path start. Its + * length is one so that is the shortest path. + * + * There are two paths from segment 2 to the code path start. One + * through segment 1 with a length of two and another directly to the + * start with a length of one. The shortest path has a length of one + * so we would return that. + */ + + function shortestPathLengthToStart(segment) { + const { + cache + } = shortestPathLengthToStart; + let length = segment.id |> cache.get(%); + + // If `length` is null then we found a cycle! Return infinity since + // the shortest path is definitely not the one where we looped. + if (length === null) { + return Infinity; + } + + // We have a cached `length`. Return it. + if (length !== undefined) { + return length; + } + + // Compute `length` and cache it. Guarding against cycles. + segment.id |> cache.set(%, null); + if (segment.prevSegments.length === 0) { + length = 1; + } else { + length = Infinity; + for (const prevSegment of segment.prevSegments) { + const prevLength = prevSegment |> shortestPathLengthToStart(%); + if (prevLength < length) { + length = prevLength; + } + } + length += 1; + } + segment.id |> cache.set(%, length); + return length; + } + countPathsFromStart.cache = new Map(); + countPathsToEnd.cache = new Map(); + shortestPathLengthToStart.cache = new Map(); + + // Count all code paths to the end of our component/hook. Also primes + // the `countPathsToEnd` cache. + const allPathsFromStartToEnd = codePath.initialSegment |> countPathsToEnd(%); + + // Gets the function name for our code path. If the function name is + // `undefined` then we know either that we have an anonymous function + // expression or our code path is not in a function. In both cases we + // will want to error since neither are React function components or + // hook functions - unless it is an anonymous function argument to + // forwardRef or memo. + const codePathFunctionName = codePathNode |> getFunctionName(%); + + // This is a valid code path for React hooks if we are directly in a React + // function component or we are in a hook function. + const isSomewhereInsideComponentOrHook = codePathNode |> isInsideComponentOrHook(%); + const isDirectlyInsideComponentOrHook = codePathFunctionName ? codePathFunctionName |> isComponentName(%) || codePathFunctionName |> isHook(%) : codePathNode |> isForwardRefCallback(%) || codePathNode |> isMemoCallback(%); + + // Compute the earliest finalizer level using information from the + // cache. We expect all reachable final segments to have a cache entry + // after calling `visitSegment()`. + let shortestFinalPathLength = Infinity; + for (const finalSegment of codePath.finalSegments) { + if (!finalSegment.reachable) { + continue; + } + const length = finalSegment |> shortestPathLengthToStart(%); + if (length < shortestFinalPathLength) { + shortestFinalPathLength = length; + } + } + + // Make sure all React Hooks pass our lint invariants. Log warnings + // if not. + for (const [segment, reactHooks] of reactHooksMap) { + // NOTE: We could report here that the hook is not reachable, but + // that would be redundant with more general "no unreachable" + // lint rules. + if (!segment.reachable) { + continue; + } + + // If there are any final segments with a shorter path to start then + // we possibly have an early return. + // + // If our segment is a final segment itself then siblings could + // possibly be early returns. + const possiblyHasEarlyReturn = segment.nextSegments.length === 0 ? shortestFinalPathLength <= (segment |> shortestPathLengthToStart(%)) : shortestFinalPathLength < (segment |> shortestPathLengthToStart(%)); + + // Count all the paths from the start of our code path to the end of + // our code path that go _through_ this segment. The critical piece + // of this is _through_. If we just call `countPathsToEnd(segment)` + // then we neglect that we may have gone through multiple paths to get + // to this point! Consider: + // + // ```js + // function MyComponent() { + // if (a) { + // // Segment 1 + // } else { + // // Segment 2 + // } + // // Segment 3 + // if (b) { + // // Segment 4 + // } else { + // // Segment 5 + // } + // } + // ``` + // + // In this component we have four code paths: + // + // 1. `a = true; b = true` + // 2. `a = true; b = false` + // 3. `a = false; b = true` + // 4. `a = false; b = false` + // + // From segment 3 there are two code paths to the end through segment + // 4 and segment 5. However, we took two paths to get here through + // segment 1 and segment 2. + // + // If we multiply the paths from start (two) by the paths to end (two) + // for segment 3 we get four. Which is our desired count. + const pathsFromStartToEnd = (segment |> countPathsFromStart(%)) * (segment |> countPathsToEnd(%)); + + // Is this hook a part of a cyclic segment? + const cycled = segment.id |> cyclic.has(%); + for (const hook of reactHooks) { + // Report an error if a hook may be called more then once. + // `use(...)` can be called in loops. + if (cycled && !(hook |> isUseIdentifier(%))) { + ({ + node: hook, + message: `React Hook "${hook |> getSource(%)}" may be executed ` + 'more than once. Possibly because it is called in a loop. ' + 'React Hooks must be called in the exact same order in ' + 'every component render.' + }) |> context.report(%); + } + + // If this is not a valid code path for React hooks then we need to + // log a warning for every hook in this code path. + // + // Pick a special message depending on the scope this hook was + // called in. + if (isDirectlyInsideComponentOrHook) { + // Report an error if the hook is called inside an async function. + const isAsyncFunction = codePathNode.async; + if (isAsyncFunction) { + ({ + node: hook, + message: `React Hook "${hook |> getSource(%)}" cannot be ` + 'called in an async function.' + }) |> context.report(%); + } + + // Report an error if a hook does not reach all finalizing code + // path segments. + // + // Special case when we think there might be an early return. + if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd && !(hook |> isUseIdentifier(%)) // `use(...)` can be called conditionally. + ) { + const message = `React Hook "${hook |> getSource(%)}" is called ` + 'conditionally. React Hooks must be called in the exact ' + 'same order in every component render.' + (possiblyHasEarlyReturn ? ' Did you accidentally call a React Hook after an' + ' early return?' : ''); + ({ + node: hook, + message + }) |> context.report(%); + } + } else if (codePathNode.parent && (codePathNode.parent.type === 'MethodDefinition' || codePathNode.parent.type === 'ClassProperty') && codePathNode.parent.value === codePathNode) { + // Custom message for hooks inside a class + const message = `React Hook "${hook |> getSource(%)}" cannot be called ` + 'in a class component. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; + ({ + node: hook, + message + }) |> context.report(%); + } else if (codePathFunctionName) { + // Custom message if we found an invalid function name. + const message = `React Hook "${hook |> getSource(%)}" is called in ` + `function "${codePathFunctionName |> getSource(%)}" ` + 'that is neither a React function component nor a custom ' + 'React Hook function.' + ' React component names must start with an uppercase letter.' + ' React Hook names must start with the word "use".'; + ({ + node: hook, + message + }) |> context.report(%); + } else if (codePathNode.type === 'Program') { + // These are dangerous if you have inline requires enabled. + const message = `React Hook "${hook |> getSource(%)}" cannot be called ` + 'at the top level. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; + ({ + node: hook, + message + }) |> context.report(%); + } else { + // Assume in all other cases the user called a hook in some + // random function callback. This should usually be true for + // anonymous function expressions. Hopefully this is clarifying + // enough in the common case that the incorrect message in + // uncommon cases doesn't matter. + // `use(...)` can be called in callbacks. + if (isSomewhereInsideComponentOrHook && !(hook |> isUseIdentifier(%))) { + const message = `React Hook "${hook |> getSource(%)}" cannot be called ` + 'inside a callback. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; + ({ + node: hook, + message + }) |> context.report(%); + } + } + } + } + }, + // Missed opportunity...We could visit all `Identifier`s instead of all + // `CallExpression`s and check that _every use_ of a hook name is valid. + // But that gets complicated and enters type-system territory, so we're + // only being strict about hook calls for now. + CallExpression(node) { + if (node.callee |> isHook(%)) { + // Add the hook node to a map keyed by the code path segment. We will + // do full code path analysis at the end of our code path. + const reactHooksMap = codePathReactHooksMapStack |> last(%); + const codePathSegment = codePathSegmentStack |> last(%); + let reactHooks = codePathSegment |> reactHooksMap.get(%); + if (!reactHooks) { + reactHooks = []; + codePathSegment |> reactHooksMap.set(%, reactHooks); + } + node.callee |> reactHooks.push(%); + } + + // useEffectEvent: useEffectEvent functions can be passed by reference within useEffect as well as in + // another useEffectEvent + if (node.callee.type === 'Identifier' && (node.callee.name === 'useEffect' || node.callee |> isUseEffectEventIdentifier(%)) && node.arguments.length > 0) { + // Denote that we have traversed into a useEffect call, and stash the CallExpr for + // comparison later when we exit + lastEffect = node; + } + }, + Identifier(node) { + // This identifier resolves to a useEffectEvent function, but isn't being referenced in an + // effect or another event function. It isn't being called either. + if (lastEffect == null && (node |> useEffectEventFunctions.has(%)) && node.parent.type !== 'CallExpression') { + ({ + node, + message: `\`${node |> getSource(%)}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + 'the same component. They cannot be assigned to variables or passed down.' + }) |> context.report(%); + } + }, + 'CallExpression:exit'(node) { + if (node === lastEffect) { + lastEffect = null; + } + }, + FunctionDeclaration(node) { + // function MyComponent() { const onClick = useEffectEvent(...) } + if (node |> isInsideComponentOrHook(%)) { + node |> getScope(%) |> recordAllUseEffectEventFunctions(%); + } + }, + ArrowFunctionExpression(node) { + // const MyComponent = () => { const onClick = useEffectEvent(...) } + if (node |> isInsideComponentOrHook(%)) { + node |> getScope(%) |> recordAllUseEffectEventFunctions(%); + } + } + }; + } +}; + +/** + * Gets the static name of a function AST node. For function declarations it is + * easy. For anonymous function expressions it is much harder. If you search for + * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places + * where JS gives anonymous function expressions names. We roughly detect the + * same AST nodes with some exceptions to better fit our use case. + */ + +function getFunctionName(node) { + if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' && node.id) { + // function useHook() {} + // const whatever = function useHook() {}; + // + // Function declaration or function expression names win over any + // assignment statements or other renames. + return node.id; + } else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { + if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) { + // const useHook = () => {}; + return node.parent.id; + } else if (node.parent.type === 'AssignmentExpression' && node.parent.right === node && node.parent.operator === '=') { + // useHook = () => {}; + return node.parent.left; + } else if (node.parent.type === 'Property' && node.parent.value === node && !node.parent.computed) { + // {useHook: () => {}} + // {useHook() {}} + return node.parent.key; + + // NOTE: We could also support `ClassProperty` and `MethodDefinition` + // here to be pedantic. However, hooks in a class are an anti-pattern. So + // we don't allow it to error early. + // + // class {useHook = () => {}} + // class {useHook() {}} + } else if (node.parent.type === 'AssignmentPattern' && node.parent.right === node && !node.parent.computed) { + // const {useHook = () => {}} = {}; + // ({useHook = () => {}} = {}); + // + // Kinda clowny, but we'd said we'd follow spec convention for + // `IsAnonymousFunctionDefinition()` usage. + return node.parent.left; + } else { + return undefined; + } + } else { + return undefined; + } +} + +/** + * Convenience function for peeking the last item in a stack. + */ + +function last(array) { + return array[array.length - 1]; +} \ No newline at end of file diff --git a/output_testing/134ExhaustiveDeps.js b/output_testing/134ExhaustiveDeps.js new file mode 100644 index 0000000..6bd2ac2 --- /dev/null +++ b/output_testing/134ExhaustiveDeps.js @@ -0,0 +1,1510 @@ +/** + * 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. + */ + +/* eslint-disable no-for-of-loops/no-for-of-loops */ + +'use strict'; + +export default { + meta: { + type: 'suggestion', + docs: { + description: 'verifies the list of dependencies for Hooks like useEffect and similar', + recommended: true, + url: 'https://github.com/facebook/react/issues/14920' + }, + fixable: 'code', + hasSuggestions: true, + schema: [{ + type: 'object', + additionalProperties: false, + enableDangerousAutofixThisMayCauseInfiniteLoops: false, + properties: { + additionalHooks: { + type: 'string' + }, + enableDangerousAutofixThisMayCauseInfiniteLoops: { + type: 'boolean' + } + } + }] + }, + create(context) { + // Parse the `additionalHooks` regex. + const additionalHooks = context.options && context.options[0] && context.options[0].additionalHooks ? new RegExp(context.options[0].additionalHooks) : undefined; + const enableDangerousAutofixThisMayCauseInfiniteLoops = context.options && context.options[0] && context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops || false; + const options = { + additionalHooks, + enableDangerousAutofixThisMayCauseInfiniteLoops + }; + function reportProblem(problem) { + if (enableDangerousAutofixThisMayCauseInfiniteLoops) { + // Used to enable legacy behavior. Dangerous. + // Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension). + if ((problem.suggest |> Array.isArray(%)) && problem.suggest.length > 0) { + problem.fix = problem.suggest[0].fix; + } + } + problem |> context.report(%); + } + + /** + * SourceCode#getText that also works down to ESLint 3.0.0 + */ + const getSource = typeof context.getSource === 'function' ? node => { + return node |> context.getSource(%); + } : node => { + return node |> context.sourceCode.getText(%); + }; + /** + * SourceCode#getScope that also works down to ESLint 3.0.0 + */ + const getScope = typeof context.getScope === 'function' ? () => { + return context.getScope(); + } : node => { + return node |> context.sourceCode.getScope(%); + }; + const scopeManager = context.getSourceCode().scopeManager; + + // Should be shared between visitors. + const setStateCallSites = new WeakMap(); + const stateVariables = new WeakSet(); + const stableKnownValueCache = new WeakMap(); + const functionWithoutCapturedValueCache = new WeakMap(); + const useEffectEventVariables = new WeakSet(); + function memoizeWithWeakMap(fn, map) { + return function (arg) { + if (arg |> map.has(%)) { + // to verify cache hits: + // console.log(arg.name) + return arg |> map.get(%); + } + const result = arg |> fn(%); + arg |> map.set(%, result); + return result; + }; + } + /** + * Visitor for both function expressions and arrow function expressions. + */ + function visitFunctionWithDependencies(node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect) { + if (isEffect && node.async) { + ({ + node: node, + message: `Effect callbacks are synchronous to prevent race conditions. ` + `Put the async function inside:\n\n` + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching' + }) |> reportProblem(%); + } + + // Get the current scope. + const scope = node |> scopeManager.acquire(%); + + // Find all our "pure scopes". On every re-render of a component these + // pure scopes may have changes to the variables declared within. So all + // variables used in our reactive hook callback but declared in a pure + // scope need to be listed as dependencies of our reactive hook callback. + // + // According to the rules of React you can't read a mutable value in pure + // scope. We can't enforce this in a lint so we trust that all variables + // declared outside of pure scope are indeed frozen. + const pureScopes = new Set(); + let componentScope = null; + { + let currentScope = scope.upper; + while (currentScope) { + currentScope |> pureScopes.add(%); + if (currentScope.type === 'function') { + break; + } + currentScope = currentScope.upper; + } + // If there is no parent function scope then there are no pure scopes. + // The ones we've collected so far are incorrect. So don't continue with + // the lint. + if (!currentScope) { + return; + } + componentScope = currentScope; + } + const isArray = Array.isArray; + + // Next we'll define a few helpers that helps us + // tell if some values don't have to be declared as deps. + + // Some are known to be stable based on Hook calls. + // const [state, setState] = useState() / React.useState() + // ^^^ true for this reference + // const [state, dispatch] = useReducer() / React.useReducer() + // ^^^ true for this reference + // const ref = useRef() + // ^^^ true for this reference + // const onStuff = useEffectEvent(() => {}) + // ^^^ true for this reference + // False for everything else. + function isStableKnownHookValue(resolved) { + if (!(resolved.defs |> isArray(%))) { + return false; + } + const def = resolved.defs[0]; + if (def == null) { + return false; + } + // Look for `let stuff = ...` + if (def.node.type !== 'VariableDeclarator') { + return false; + } + let init = def.node.init; + if (init == null) { + return false; + } + while (init.type === 'TSAsExpression' || init.type === 'AsExpression') { + init = init.expression; + } + // Detect primitive constants + // const foo = 42 + let declaration = def.node.parent; + if (declaration == null) { + // This might happen if variable is declared after the callback. + // In that case ESLint won't set up .parent refs. + // So we'll set them up manually. + componentScope.block |> fastFindReferenceWithParent(%, def.node.id); + declaration = def.node.parent; + if (declaration == null) { + return false; + } + } + if (declaration.kind === 'const' && init.type === 'Literal' && (typeof init.value === 'string' || typeof init.value === 'number' || init.value === null)) { + // Definitely stable + return true; + } + // Detect known Hook calls + // const [_, setState] = useState() + if (init.type !== 'CallExpression') { + return false; + } + let callee = init.callee; + // Step into `= React.something` initializer. + if (callee.type === 'MemberExpression' && callee.object.name === 'React' && callee.property != null && !callee.computed) { + callee = callee.property; + } + if (callee.type !== 'Identifier') { + return false; + } + const id = def.node.id; + const { + name + } = callee; + if (name === 'useRef' && id.type === 'Identifier') { + // useRef() return value is stable. + return true; + } else if ((callee |> isUseEffectEventIdentifier(%)) && id.type === 'Identifier') { + for (const ref of resolved.references) { + if (ref !== id) { + ref.identifier |> useEffectEventVariables.add(%); + } + } + // useEffectEvent() return value is always unstable. + return true; + } else if (name === 'useState' || name === 'useReducer') { + // Only consider second value in initializing tuple stable. + if (id.type === 'ArrayPattern' && id.elements.length === 2 && (resolved.identifiers |> isArray(%))) { + // Is second tuple value the same reference we're checking? + if (id.elements[1] === resolved.identifiers[0]) { + if (name === 'useState') { + const references = resolved.references; + let writeCount = 0; + for (let i = 0; i < references.length; i++) { + if (references[i].isWrite()) { + writeCount++; + } + if (writeCount > 1) { + return false; + } + references[i].identifier |> setStateCallSites.set(%, id.elements[0]); + } + } + // Setter is stable. + return true; + } else if (id.elements[0] === resolved.identifiers[0]) { + if (name === 'useState') { + const references = resolved.references; + for (let i = 0; i < references.length; i++) { + references[i].identifier |> stateVariables.add(%); + } + } + // State variable itself is dynamic. + return false; + } + } + } else if (name === 'useTransition') { + // Only consider second value in initializing tuple stable. + if (id.type === 'ArrayPattern' && id.elements.length === 2 && (resolved.identifiers |> Array.isArray(%))) { + // Is second tuple value the same reference we're checking? + if (id.elements[1] === resolved.identifiers[0]) { + // Setter is stable. + return true; + } + } + } + // By default assume it's dynamic. + return false; + } + + // Some are just functions that don't reference anything dynamic. + function isFunctionWithoutCapturedValues(resolved) { + if (!(resolved.defs |> isArray(%))) { + return false; + } + const def = resolved.defs[0]; + if (def == null) { + return false; + } + if (def.node == null || def.node.id == null) { + return false; + } + // Search the direct component subscopes for + // top-level function definitions matching this reference. + const fnNode = def.node; + const childScopes = componentScope.childScopes; + let fnScope = null; + let i; + for (i = 0; i < childScopes.length; i++) { + const childScope = childScopes[i]; + const childScopeBlock = childScope.block; + if ( + // function handleChange() {} + fnNode.type === 'FunctionDeclaration' && childScopeBlock === fnNode || + // const handleChange = () => {} + // const handleChange = function() {} + fnNode.type === 'VariableDeclarator' && childScopeBlock.parent === fnNode) { + // Found it! + fnScope = childScope; + break; + } + } + if (fnScope == null) { + return false; + } + // Does this function capture any values + // that are in pure scopes (aka render)? + for (i = 0; i < fnScope.through.length; i++) { + const ref = fnScope.through[i]; + if (ref.resolved == null) { + continue; + } + if ((ref.resolved.scope |> pureScopes.has(%)) && + // Stable values are fine though, + // although we won't check functions deeper. + !(ref.resolved |> memoizedIsStableKnownHookValue(%))) { + return false; + } + } + // If we got here, this function doesn't capture anything + // from render--or everything it captures is known stable. + return true; + } + + // Remember such values. Avoid re-running extra checks on them. + const memoizedIsStableKnownHookValue = isStableKnownHookValue |> memoizeWithWeakMap(%, stableKnownValueCache); + const memoizedIsFunctionWithoutCapturedValues = isFunctionWithoutCapturedValues |> memoizeWithWeakMap(%, functionWithoutCapturedValueCache); + + // These are usually mistaken. Collect them. + const currentRefsInEffectCleanup = new Map(); + + // Is this reference inside a cleanup function for this effect node? + // We can check by traversing scopes upwards from the reference, and checking + // if the last "return () => " we encounter is located directly inside the effect. + function isInsideEffectCleanup(reference) { + let curScope = reference.from; + let isInReturnedFunction = false; + while (curScope.block !== node) { + if (curScope.type === 'function') { + isInReturnedFunction = curScope.block.parent != null && curScope.block.parent.type === 'ReturnStatement'; + } + curScope = curScope.upper; + } + return isInReturnedFunction; + } + + // Get dependencies from all our resolved references in pure scopes. + // Key is dependency string, value is whether it's stable. + const dependencies = new Map(); + const optionalChains = new Map(); + scope |> gatherDependenciesRecursively(%); + function gatherDependenciesRecursively(currentScope) { + for (const reference of currentScope.references) { + // If this reference is not resolved or it is not declared in a pure + // scope then we don't care about this reference. + if (!reference.resolved) { + continue; + } + if (!(reference.resolved.scope |> pureScopes.has(%))) { + continue; + } + + // Narrow the scope of a dependency if it is, say, a member expression. + // Then normalize the narrowed dependency. + const referenceNode = node |> fastFindReferenceWithParent(%, reference.identifier); + const dependencyNode = referenceNode |> getDependency(%); + const dependency = dependencyNode |> analyzePropertyChain(%, optionalChains); + + // Accessing ref.current inside effect cleanup is bad. + if ( + // We're in an effect... + isEffect && + // ... and this look like accessing .current... + dependencyNode.type === 'Identifier' && (dependencyNode.parent.type === 'MemberExpression' || dependencyNode.parent.type === 'OptionalMemberExpression') && !dependencyNode.parent.computed && dependencyNode.parent.property.type === 'Identifier' && dependencyNode.parent.property.name === 'current' && (reference |> isInsideEffectCleanup(%))) { + dependency |> currentRefsInEffectCleanup.set(%, { + reference, + dependencyNode + }); + } + if (dependencyNode.parent.type === 'TSTypeQuery' || dependencyNode.parent.type === 'TSTypeReference') { + continue; + } + const def = reference.resolved.defs[0]; + if (def == null) { + continue; + } + // Ignore references to the function itself as it's not defined yet. + if (def.node != null && def.node.init === node.parent) { + continue; + } + // Ignore Flow type parameters + if (def.type === 'TypeParameter') { + continue; + } + + // Add the dependency to a map so we can make sure it is referenced + // again in our dependencies array. Remember whether it's stable. + if (!(dependency |> dependencies.has(%))) { + const resolved = reference.resolved; + const isStable = resolved |> memoizedIsStableKnownHookValue(%) || resolved |> memoizedIsFunctionWithoutCapturedValues(%); + dependency |> dependencies.set(%, { + isStable, + references: [reference] + }); + } else { + reference |> (dependency |> dependencies.get(%)).references.push(%); + } + } + for (const childScope of currentScope.childScopes) { + childScope |> gatherDependenciesRecursively(%); + } + } + + // Warn about accessing .current in cleanup effects. + // Warn about assigning to variables in the outer scope. + // Those are usually bugs. + (({ + reference, + dependencyNode + }, dependency) => { + const references = reference.resolved.references; + // Is React managing this ref or us? + // Let's see if we can find a .current assignment. + let foundCurrentAssignment = false; + for (let i = 0; i < references.length; i++) { + const { + identifier + } = references[i]; + const { + parent + } = identifier; + if (parent != null && + // ref.current + // Note: no need to handle OptionalMemberExpression because it can't be LHS. + parent.type === 'MemberExpression' && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'current' && + // ref.current = + parent.parent.type === 'AssignmentExpression' && parent.parent.left === parent) { + foundCurrentAssignment = true; + break; + } + } + // We only want to warn about React-managed refs. + if (foundCurrentAssignment) { + return; + } + ({ + node: dependencyNode.parent.property, + message: `The ref value '${dependency}.current' will likely have ` + `changed by the time this effect cleanup function runs. If ` + `this ref points to a node rendered by React, copy ` + `'${dependency}.current' to a variable inside the effect, and ` + `use that variable in the cleanup function.` + }) |> reportProblem(%); + }) |> currentRefsInEffectCleanup.forEach(%); + const staleAssignments = new Set(); + function reportStaleAssignment(writeExpr, key) { + if (key |> staleAssignments.has(%)) { + return; + } + key |> staleAssignments.add(%); + ({ + node: writeExpr, + message: `Assignments to the '${key}' variable from inside React Hook ` + `${reactiveHook |> getSource(%)} will be lost after each ` + `render. To preserve the value over time, store it in a useRef ` + `Hook and keep the mutable value in the '.current' property. ` + `Otherwise, you can move this variable directly inside ` + `${reactiveHook |> getSource(%)}.` + }) |> reportProblem(%); + } + + // Remember which deps are stable and report bad usage first. + const stableDependencies = new Set(); + (({ + isStable, + references + }, key) => { + if (isStable) { + key |> stableDependencies.add(%); + } + (reference => { + if (reference.writeExpr) { + reference.writeExpr |> reportStaleAssignment(%, key); + } + }) |> references.forEach(%); + }) |> dependencies.forEach(%); + if (staleAssignments.size > 0) { + // The intent isn't clear so we'll wait until you fix those first. + return; + } + if (!declaredDependenciesNode) { + // Check if there are any top-level setState() calls. + // Those tend to lead to infinite loops. + let setStateInsideEffectWithoutDeps = null; + (({ + isStable, + references + }, key) => { + if (setStateInsideEffectWithoutDeps) { + return; + } + (reference => { + if (setStateInsideEffectWithoutDeps) { + return; + } + const id = reference.identifier; + const isSetState = id |> setStateCallSites.has(%); + if (!isSetState) { + return; + } + let fnScope = reference.from; + while (fnScope.type !== 'function') { + fnScope = fnScope.upper; + } + const isDirectlyInsideEffect = fnScope.block === node; + if (isDirectlyInsideEffect) { + // TODO: we could potentially ignore early returns. + setStateInsideEffectWithoutDeps = key; + } + }) |> references.forEach(%); + }) |> dependencies.forEach(%); + if (setStateInsideEffectWithoutDeps) { + const { + suggestedDependencies + } = { + dependencies, + declaredDependencies: [], + stableDependencies, + externalDependencies: new Set(), + isEffect: true + } |> collectRecommendations(%); + ({ + node: reactiveHook, + message: `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + `To fix this, pass [` + (', ' |> suggestedDependencies.join(%)) + `] as a second argument to the ${reactiveHookName} Hook.`, + suggest: [{ + desc: `Add dependencies array: [${', ' |> suggestedDependencies.join(%)}]`, + fix(fixer) { + return node |> fixer.insertTextAfter(%, `, [${', ' |> suggestedDependencies.join(%)}]`); + } + }] + }) |> reportProblem(%); + } + return; + } + const declaredDependencies = []; + const externalDependencies = new Set(); + const isArrayExpression = declaredDependenciesNode.type === 'ArrayExpression'; + const isTSAsArrayExpression = declaredDependenciesNode.type === 'TSAsExpression' && declaredDependenciesNode.expression.type === 'ArrayExpression'; + if (!isArrayExpression && !isTSAsArrayExpression) { + // If the declared dependencies are not an array expression then we + // can't verify that the user provided the correct dependencies. Tell + // the user this in an error. + ({ + node: declaredDependenciesNode, + message: `React Hook ${reactiveHook |> getSource(%)} was passed a ` + 'dependency list that is not an array literal. This means we ' + "can't statically verify whether you've passed the correct " + 'dependencies.' + }) |> reportProblem(%); + } else { + const arrayExpression = isTSAsArrayExpression ? declaredDependenciesNode.expression : declaredDependenciesNode; + (declaredDependencyNode => { + // Skip elided elements. + if (declaredDependencyNode === null) { + return; + } + // If we see a spread element then add a special warning. + if (declaredDependencyNode.type === 'SpreadElement') { + ({ + node: declaredDependencyNode, + message: `React Hook ${reactiveHook |> getSource(%)} has a spread ` + "element in its dependency array. This means we can't " + "statically verify whether you've passed the " + 'correct dependencies.' + }) |> reportProblem(%); + return; + } + if (declaredDependencyNode |> useEffectEventVariables.has(%)) { + ({ + node: declaredDependencyNode, + message: 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + `Remove \`${declaredDependencyNode |> getSource(%)}\` from the list.`, + suggest: [{ + desc: `Remove the dependency \`${declaredDependencyNode |> getSource(%)}\``, + fix(fixer) { + return declaredDependencyNode.range |> fixer.removeRange(%); + } + }] + }) |> reportProblem(%); + } + // Try to normalize the declared dependency. If we can't then an error + // will be thrown. We will catch that error and report an error. + let declaredDependency; + try { + declaredDependency = declaredDependencyNode |> analyzePropertyChain(%, null); + } catch (error) { + if (error.message |> /Unsupported node type/.test(%)) { + if (declaredDependencyNode.type === 'Literal') { + if (declaredDependencyNode.value |> dependencies.has(%)) { + ({ + node: declaredDependencyNode, + message: `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + `because it never changes. ` + `Did you mean to include ${declaredDependencyNode.value} in the array instead?` + }) |> reportProblem(%); + } else { + ({ + node: declaredDependencyNode, + message: `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + 'because it never changes. You can safely remove it.' + }) |> reportProblem(%); + } + } else { + ({ + node: declaredDependencyNode, + message: `React Hook ${reactiveHook |> getSource(%)} has a ` + `complex expression in the dependency array. ` + 'Extract it to a separate variable so it can be statically checked.' + }) |> reportProblem(%); + } + return; + } else { + throw error; + } + } + let maybeID = declaredDependencyNode; + while (maybeID.type === 'MemberExpression' || maybeID.type === 'OptionalMemberExpression' || maybeID.type === 'ChainExpression') { + maybeID = maybeID.object || maybeID.expression.object; + } + const isDeclaredInComponent = !((ref => ref.identifier === maybeID) |> componentScope.through.some(%)); + + // Add the dependency to our declared dependency map. + ({ + key: declaredDependency, + node: declaredDependencyNode + }) |> declaredDependencies.push(%); + if (!isDeclaredInComponent) { + declaredDependency |> externalDependencies.add(%); + } + }) |> arrayExpression.elements.forEach(%); + } + const { + suggestedDependencies, + unnecessaryDependencies, + missingDependencies, + duplicateDependencies + } = { + dependencies, + declaredDependencies, + stableDependencies, + externalDependencies, + isEffect + } |> collectRecommendations(%); + let suggestedDeps = suggestedDependencies; + const problemCount = duplicateDependencies.size + missingDependencies.size + unnecessaryDependencies.size; + if (problemCount === 0) { + // If nothing else to report, check if some dependencies would + // invalidate on every render. + const constructions = { + declaredDependencies, + declaredDependenciesNode, + componentScope, + scope + } |> scanForConstructions(%); + (({ + construction, + isUsedOutsideOfHook, + depType + }) => { + const wrapperHook = depType === 'function' ? 'useCallback' : 'useMemo'; + const constructionType = depType === 'function' ? 'definition' : 'initialization'; + const defaultAdvice = `wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`; + const advice = isUsedOutsideOfHook ? `To fix this, ${defaultAdvice}` : `Move it inside the ${reactiveHookName} callback. Alternatively, ${defaultAdvice}`; + const causation = depType === 'conditional' || depType === 'logical expression' ? 'could make' : 'makes'; + const message = `The '${construction.name.name}' ${depType} ${causation} the dependencies of ` + `${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc.start.line}) ` + `change on every render. ${advice}`; + let suggest; + // Only handle the simple case of variable assignments. + // Wrapping function declarations can mess up hoisting. + if (isUsedOutsideOfHook && construction.type === 'Variable' && + // Objects may be mutated after construction, which would make this + // fix unsafe. Functions _probably_ won't be mutated, so we'll + // allow this fix for them. + depType === 'function') { + suggest = [{ + desc: `Wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`, + fix(fixer) { + const [before, after] = wrapperHook === 'useMemo' ? [`useMemo(() => { return `, '; })'] : ['useCallback(', ')']; + return [construction.node.init |> fixer.insertTextBefore(%, before), construction.node.init |> fixer.insertTextAfter(%, after)]; + } + }]; + } + // TODO: What if the function needs to change on every render anyway? + // Should we suggest removing effect deps as an appropriate fix too? + ({ + // TODO: Why not report this at the dependency site? + node: construction.node, + message, + suggest + }) |> reportProblem(%); + }) |> constructions.forEach(%); + return; + } + + // If we're going to report a missing dependency, + // we might as well recalculate the list ignoring + // the currently specified deps. This can result + // in some extra deduplication. We can't do this + // for effects though because those have legit + // use cases for over-specifying deps. + if (!isEffect && missingDependencies.size > 0) { + suggestedDeps = ({ + dependencies, + declaredDependencies: [], + // Pretend we don't know + stableDependencies, + externalDependencies, + isEffect + } |> collectRecommendations(%)).suggestedDependencies; + } + + // Alphabetize the suggestions, but only if deps were already alphabetized. + function areDeclaredDepsAlphabetized() { + if (declaredDependencies.length === 0) { + return true; + } + const declaredDepKeys = (dep => dep.key) |> declaredDependencies.map(%); + const sortedDeclaredDepKeys = declaredDepKeys.slice().sort(); + return (',' |> declaredDepKeys.join(%)) === (',' |> sortedDeclaredDepKeys.join(%)); + } + if (areDeclaredDepsAlphabetized()) { + suggestedDeps.sort(); + } + + // Most of our algorithm deals with dependency paths with optional chaining stripped. + // This function is the last step before printing a dependency, so now is a good time to + // check whether any members in our path are always used as optional-only. In that case, + // we will use ?. instead of . to concatenate those parts of the path. + function formatDependency(path) { + const members = '.' |> path.split(%); + let finalPath = ''; + for (let i = 0; i < members.length; i++) { + if (i !== 0) { + const pathSoFar = '.' |> (0 |> members.slice(%, i + 1)).join(%); + const isOptional = (pathSoFar |> optionalChains.get(%)) === true; + finalPath += isOptional ? '?.' : '.'; + } + finalPath += members[i]; + } + return finalPath; + } + function getWarningMessage(deps, singlePrefix, label, fixVerb) { + if (deps.size === 0) { + return null; + } + return (deps.size > 1 ? '' : singlePrefix + ' ') + label + ' ' + (deps.size > 1 ? 'dependencies' : 'dependency') + ': ' + ((name => "'" + (name |> formatDependency(%)) + "'") |> (deps |> Array.from(%)).sort().map(%) |> joinEnglish(%)) + `. Either ${fixVerb} ${deps.size > 1 ? 'them' : 'it'} or remove the dependency array.`; + } + let extraWarning = ''; + if (unnecessaryDependencies.size > 0) { + let badRef = null; + (key => { + if (badRef !== null) { + return; + } + if ('.current' |> key.endsWith(%)) { + badRef = key; + } + }) |> (unnecessaryDependencies.keys() |> Array.from(%)).forEach(%); + if (badRef !== null) { + extraWarning = ` Mutable values like '${badRef}' aren't valid dependencies ` + "because mutating them doesn't re-render the component."; + } else if (externalDependencies.size > 0) { + const dep = (externalDependencies |> Array.from(%))[0]; + // Don't show this warning for things that likely just got moved *inside* the callback + // because in that case they're clearly not referring to globals. + if (!(dep |> scope.set.has(%))) { + extraWarning = ` Outer scope values like '${dep}' aren't valid dependencies ` + `because mutating them doesn't re-render the component.`; + } + } + } + + // `props.foo()` marks `props` as a dependency because it has + // a `this` value. This warning can be confusing. + // So if we're going to show it, append a clarification. + if (!extraWarning && ('props' |> missingDependencies.has(%))) { + const propDep = 'props' |> dependencies.get(%); + if (propDep == null) { + return; + } + const refs = propDep.references; + if (!(refs |> Array.isArray(%))) { + return; + } + let isPropsOnlyUsedInMembers = true; + for (let i = 0; i < refs.length; i++) { + const ref = refs[i]; + const id = componentScope.block |> fastFindReferenceWithParent(%, ref.identifier); + if (!id) { + isPropsOnlyUsedInMembers = false; + break; + } + const parent = id.parent; + if (parent == null) { + isPropsOnlyUsedInMembers = false; + break; + } + if (parent.type !== 'MemberExpression' && parent.type !== 'OptionalMemberExpression') { + isPropsOnlyUsedInMembers = false; + break; + } + } + if (isPropsOnlyUsedInMembers) { + extraWarning = ` However, 'props' will change when *any* prop changes, so the ` + `preferred fix is to destructure the 'props' object outside of ` + `the ${reactiveHookName} call and refer to those specific props ` + `inside ${reactiveHook |> getSource(%)}.`; + } + } + if (!extraWarning && missingDependencies.size > 0) { + // See if the user is trying to avoid specifying a callable prop. + // This usually means they're unaware of useCallback. + let missingCallbackDep = null; + (missingDep => { + if (missingCallbackDep) { + return; + } + // Is this a variable from top scope? + const topScopeRef = missingDep |> componentScope.set.get(%); + const usedDep = missingDep |> dependencies.get(%); + if (usedDep.references[0].resolved !== topScopeRef) { + return; + } + // Is this a destructured prop? + const def = topScopeRef.defs[0]; + if (def == null || def.name == null || def.type !== 'Parameter') { + return; + } + // Was it called in at least one case? Then it's a function. + let isFunctionCall = false; + let id; + for (let i = 0; i < usedDep.references.length; i++) { + id = usedDep.references[i].identifier; + if (id != null && id.parent != null && (id.parent.type === 'CallExpression' || id.parent.type === 'OptionalCallExpression') && id.parent.callee === id) { + isFunctionCall = true; + break; + } + } + if (!isFunctionCall) { + return; + } + // If it's missing (i.e. in component scope) *and* it's a parameter + // then it is definitely coming from props destructuring. + // (It could also be props itself but we wouldn't be calling it then.) + missingCallbackDep = missingDep; + }) |> missingDependencies.forEach(%); + if (missingCallbackDep !== null) { + extraWarning = ` If '${missingCallbackDep}' changes too often, ` + `find the parent component that defines it ` + `and wrap that definition in useCallback.`; + } + } + if (!extraWarning && missingDependencies.size > 0) { + let setStateRecommendation = null; + (missingDep => { + if (setStateRecommendation !== null) { + return; + } + const usedDep = missingDep |> dependencies.get(%); + const references = usedDep.references; + let id; + let maybeCall; + for (let i = 0; i < references.length; i++) { + id = references[i].identifier; + maybeCall = id.parent; + // Try to see if we have setState(someExpr(missingDep)). + while (maybeCall != null && maybeCall !== componentScope.block) { + if (maybeCall.type === 'CallExpression') { + const correspondingStateVariable = maybeCall.callee |> setStateCallSites.get(%); + if (correspondingStateVariable != null) { + if (correspondingStateVariable.name === missingDep) { + // setCount(count + 1) + setStateRecommendation = { + missingDep, + setter: maybeCall.callee.name, + form: 'updater' + }; + } else if (id |> stateVariables.has(%)) { + // setCount(count + increment) + setStateRecommendation = { + missingDep, + setter: maybeCall.callee.name, + form: 'reducer' + }; + } else { + const resolved = references[i].resolved; + if (resolved != null) { + // If it's a parameter *and* a missing dep, + // it must be a prop or something inside a prop. + // Therefore, recommend an inline reducer. + const def = resolved.defs[0]; + if (def != null && def.type === 'Parameter') { + setStateRecommendation = { + missingDep, + setter: maybeCall.callee.name, + form: 'inlineReducer' + }; + } + } + } + break; + } + } + maybeCall = maybeCall.parent; + } + if (setStateRecommendation !== null) { + break; + } + } + }) |> missingDependencies.forEach(%); + if (setStateRecommendation !== null) { + switch (setStateRecommendation.form) { + case 'reducer': + extraWarning = ` You can also replace multiple useState variables with useReducer ` + `if '${setStateRecommendation.setter}' needs the ` + `current value of '${setStateRecommendation.missingDep}'.`; + break; + case 'inlineReducer': + extraWarning = ` If '${setStateRecommendation.setter}' needs the ` + `current value of '${setStateRecommendation.missingDep}', ` + `you can also switch to useReducer instead of useState and ` + `read '${setStateRecommendation.missingDep}' in the reducer.`; + break; + case 'updater': + extraWarning = ` You can also do a functional update '${setStateRecommendation.setter}(${0 |> setStateRecommendation.missingDep.slice(%, 1)} => ...)' if you only need '${setStateRecommendation.missingDep}'` + ` in the '${setStateRecommendation.setter}' call.`; + break; + default: + throw new Error('Unknown case.'); + } + } + } + ({ + node: declaredDependenciesNode, + message: `React Hook ${reactiveHook |> getSource(%)} has ` + ( + // To avoid a long message, show the next actionable item. + getWarningMessage(missingDependencies, 'a', 'missing', 'include') || getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') || getWarningMessage(duplicateDependencies, 'a', 'duplicate', 'omit')) + extraWarning, + suggest: [{ + desc: `Update the dependencies array to be: [${', ' |> (formatDependency |> suggestedDeps.map(%)).join(%)}]`, + fix(fixer) { + // TODO: consider preserving the comments or formatting? + return declaredDependenciesNode |> fixer.replaceText(%, `[${', ' |> (formatDependency |> suggestedDeps.map(%)).join(%)}]`); + } + }] + }) |> reportProblem(%); + } + function visitCallExpression(node) { + const callbackIndex = node.callee |> getReactiveHookCallbackIndex(%, options); + if (callbackIndex === -1) { + // Not a React Hook call that needs deps. + return; + } + const callback = node.arguments[callbackIndex]; + const reactiveHook = node.callee; + const reactiveHookName = (reactiveHook |> getNodeWithoutReactNamespace(%)).name; + const maybeNode = node.arguments[callbackIndex + 1]; + const declaredDependenciesNode = maybeNode && !(maybeNode.type === 'Identifier' && maybeNode.name === 'undefined') ? maybeNode : undefined; + const isEffect = reactiveHookName |> /Effect($|[^a-z])/g.test(%); + + // Check whether a callback is supplied. If there is no callback supplied + // then the hook will not work and React will throw a TypeError. + // So no need to check for dependency inclusion. + if (!callback) { + ({ + node: reactiveHook, + message: `React Hook ${reactiveHookName} requires an effect callback. ` + `Did you forget to pass a callback to the hook?` + }) |> reportProblem(%); + return; + } + + // Check the declared dependencies for this reactive hook. If there is no + // second argument then the reactive callback will re-run on every render. + // So no need to check for dependency inclusion. + if (!declaredDependenciesNode && !isEffect) { + // These are only used for optimization. + if (reactiveHookName === 'useMemo' || reactiveHookName === 'useCallback') { + // TODO: Can this have a suggestion? + ({ + node: reactiveHook, + message: `React Hook ${reactiveHookName} does nothing when called with ` + `only one argument. Did you forget to pass an array of ` + `dependencies?` + }) |> reportProblem(%); + } + return; + } + switch (callback.type) { + case 'FunctionExpression': + case 'ArrowFunctionExpression': + visitFunctionWithDependencies(callback, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect); + return; + // Handled + case 'TSAsExpression': + visitFunctionWithDependencies(callback.expression, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect); + return; + // Handled + case 'Identifier': + if (!declaredDependenciesNode) { + // No deps, no problems. + return; // Handled + } + // The function passed as a callback is not written inline. + // But perhaps it's in the dependencies array? + if (declaredDependenciesNode.elements && ((el => el && el.type === 'Identifier' && el.name === callback.name) |> declaredDependenciesNode.elements.some(%))) { + // If it's already in the list of deps, we don't care because + // this is valid regardless. + return; // Handled + } + // We'll do our best effort to find it, complain otherwise. + const variable = callback.name |> (callback |> getScope(%)).set.get(%); + if (variable == null || variable.defs == null) { + // If it's not in scope, we don't care. + return; // Handled + } + // The function passed as a callback is not written inline. + // But it's defined somewhere in the render scope. + // We'll do our best effort to find and check it, complain otherwise. + const def = variable.defs[0]; + if (!def || !def.node) { + break; // Unhandled + } + if (def.type !== 'Variable' && def.type !== 'FunctionName') { + // Parameter or an unusual pattern. Bail out. + break; // Unhandled + } + switch (def.node.type) { + case 'FunctionDeclaration': + // useEffect(() => { ... }, []); + visitFunctionWithDependencies(def.node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect); + return; + // Handled + case 'VariableDeclarator': + const init = def.node.init; + if (!init) { + break; // Unhandled + } + switch (init.type) { + // const effectBody = () => {...}; + // useEffect(effectBody, []); + case 'ArrowFunctionExpression': + case 'FunctionExpression': + // We can inspect this function as if it were inline. + visitFunctionWithDependencies(init, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect); + return; + // Handled + } + break; + // Unhandled + } + break; + // Unhandled + default: + // useEffect(generateEffectBody(), []); + ({ + node: reactiveHook, + message: `React Hook ${reactiveHookName} received a function whose dependencies ` + `are unknown. Pass an inline function instead.` + }) |> reportProblem(%); + return; + // Handled + } + + // Something unusual. Fall back to suggesting to add the body itself as a dep. + ({ + node: reactiveHook, + message: `React Hook ${reactiveHookName} has a missing dependency: '${callback.name}'. ` + `Either include it or remove the dependency array.`, + suggest: [{ + desc: `Update the dependencies array to be: [${callback.name}]`, + fix(fixer) { + return declaredDependenciesNode |> fixer.replaceText(%, `[${callback.name}]`); + } + }] + }) |> reportProblem(%); + } + return { + CallExpression: visitCallExpression + }; + } +}; + +// The meat of the logic. +function collectRecommendations({ + dependencies, + declaredDependencies, + stableDependencies, + externalDependencies, + isEffect +}) { + // Our primary data structure. + // It is a logical representation of property chains: + // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz` + // -> `props.lol` + // -> `props.huh` -> `props.huh.okay` + // -> `props.wow` + // We'll use it to mark nodes that are *used* by the programmer, + // and the nodes that were *declared* as deps. Then we will + // traverse it to learn which deps are missing or unnecessary. + const depTree = createDepTree(); + function createDepTree() { + return { + isUsed: false, + // True if used in code + isSatisfiedRecursively: false, + // True if specified in deps + isSubtreeUsed: false, + // True if something deeper is used by code + children: new Map() // Nodes for properties + }; + } + + // Mark all required nodes first. + // Imagine exclamation marks next to each used deep property. + // Mark all satisfied nodes. + // Imagine checkmarks next to each declared dependency. + ((_, key) => { + const node = depTree |> getOrCreateNodeByPath(%, key); + node.isUsed = true; + markAllParentsByPath(depTree, key, parent => { + parent.isSubtreeUsed = true; + }); + }) |> dependencies.forEach(%); + (({ + key + }) => { + const node = depTree |> getOrCreateNodeByPath(%, key); + node.isSatisfiedRecursively = true; + }) |> declaredDependencies.forEach(%); + // Tree manipulation helpers. + (key => { + const node = depTree |> getOrCreateNodeByPath(%, key); + node.isSatisfiedRecursively = true; + }) |> stableDependencies.forEach(%); + function getOrCreateNodeByPath(rootNode, path) { + const keys = '.' |> path.split(%); + let node = rootNode; + for (const key of keys) { + let child = key |> node.children.get(%); + if (!child) { + child = createDepTree(); + key |> node.children.set(%, child); + } + node = child; + } + return node; + } + function markAllParentsByPath(rootNode, path, fn) { + const keys = '.' |> path.split(%); + let node = rootNode; + for (const key of keys) { + const child = key |> node.children.get(%); + if (!child) { + return; + } + child |> fn(%); + node = child; + } + } + + // Now we can learn which dependencies are missing or necessary. + const missingDependencies = new Set(); + const satisfyingDependencies = new Set(); + scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, key => key); + function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) { + ((child, key) => { + const path = key |> keyToPath(%); + if (child.isSatisfiedRecursively) { + if (child.isSubtreeUsed) { + // Remember this dep actually satisfied something. + path |> satisfyingPaths.add(%); + } + // It doesn't matter if there's something deeper. + // It would be transitively satisfied since we assume immutability. + // `props.foo` is enough if you read `props.foo.id`. + return; + } + if (child.isUsed) { + // Remember that no declared deps satisfied this node. + + // If we got here, nothing in its subtree was satisfied. + // No need to search further. + path |> missingPaths.add(%); + return; + } + scanTreeRecursively(child, missingPaths, satisfyingPaths, childKey => path + '.' + childKey); + }) |> node.children.forEach(%); + } + + // Collect suggestions in the order they were originally specified. + const suggestedDependencies = []; + const unnecessaryDependencies = new Set(); + const duplicateDependencies = new Set(); + // Then add the missing ones at the end. + (({ + key + }) => { + // Does this declared dep satisfy a real need? + if (key |> satisfyingDependencies.has(%)) { + if ((key |> suggestedDependencies.indexOf(%)) === -1) { + // Good one. + key |> suggestedDependencies.push(%); + } else { + // Duplicate. + key |> duplicateDependencies.add(%); + } + } else { + if (isEffect && !('.current' |> key.endsWith(%)) && !(key |> externalDependencies.has(%))) { + // Effects are allowed extra "unnecessary" deps. + // Such as resetting scroll when ID changes. + // Consider them legit. + // The exception is ref.current which is always wrong. + if ((key |> suggestedDependencies.indexOf(%)) === -1) { + key |> suggestedDependencies.push(%); + } + } else { + // It's definitely not needed. + key |> unnecessaryDependencies.add(%); + } + } + }) |> declaredDependencies.forEach(%); + (key => { + key |> suggestedDependencies.push(%); + }) |> missingDependencies.forEach(%); + return { + suggestedDependencies, + unnecessaryDependencies, + duplicateDependencies, + missingDependencies + }; +} + +// If the node will result in constructing a referentially unique value, return +// its human readable type name, else return null. +function getConstructionExpressionType(node) { + switch (node.type) { + case 'ObjectExpression': + return 'object'; + case 'ArrayExpression': + return 'array'; + case 'ArrowFunctionExpression': + case 'FunctionExpression': + return 'function'; + case 'ClassExpression': + return 'class'; + case 'ConditionalExpression': + if ((node.consequent |> getConstructionExpressionType(%)) != null || (node.alternate |> getConstructionExpressionType(%)) != null) { + return 'conditional'; + } + return null; + case 'LogicalExpression': + if ((node.left |> getConstructionExpressionType(%)) != null || (node.right |> getConstructionExpressionType(%)) != null) { + return 'logical expression'; + } + return null; + case 'JSXFragment': + return 'JSX fragment'; + case 'JSXElement': + return 'JSX element'; + case 'AssignmentExpression': + if ((node.right |> getConstructionExpressionType(%)) != null) { + return 'assignment expression'; + } + return null; + case 'NewExpression': + return 'object construction'; + case 'Literal': + if (node.value instanceof RegExp) { + return 'regular expression'; + } + return null; + case 'TypeCastExpression': + case 'AsExpression': + case 'TSAsExpression': + return node.expression |> getConstructionExpressionType(%); + } + return null; +} + +// Finds variables declared as dependencies +// that would invalidate on every render. +function scanForConstructions({ + declaredDependencies, + declaredDependenciesNode, + componentScope, + scope +}) { + const constructions = Boolean |> ((({ + key + }) => { + const ref = (v => v.name === key) |> componentScope.variables.find(%); + if (ref == null) { + return null; + } + const node = ref.defs[0]; + if (node == null) { + return null; + } + // const handleChange = function () {} + // const handleChange = () => {} + // const foo = {} + // const foo = [] + // etc. + if (node.type === 'Variable' && node.node.type === 'VariableDeclarator' && node.node.id.type === 'Identifier' && + // Ensure this is not destructed assignment + node.node.init != null) { + const constantExpressionType = node.node.init |> getConstructionExpressionType(%); + if (constantExpressionType != null) { + return [ref, constantExpressionType]; + } + } + // function handleChange() {} + if (node.type === 'FunctionName' && node.node.type === 'FunctionDeclaration') { + return [ref, 'function']; + } + + // class Foo {} + if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') { + return [ref, 'class']; + } + return null; + }) |> declaredDependencies.map(%)).filter(%); + function isUsedOutsideOfHook(ref) { + let foundWriteExpr = false; + for (let i = 0; i < ref.references.length; i++) { + const reference = ref.references[i]; + if (reference.writeExpr) { + if (foundWriteExpr) { + // Two writes to the same function. + return true; + } else { + // Ignore first write as it's not usage. + foundWriteExpr = true; + continue; + } + } + let currentScope = reference.from; + while (currentScope !== scope && currentScope != null) { + currentScope = currentScope.upper; + } + if (currentScope !== scope) { + // This reference is outside the Hook callback. + // It can only be legit if it's the deps array. + if (!(declaredDependenciesNode |> isAncestorNodeOf(%, reference.identifier))) { + return true; + } + } + } + return false; + } + return (([ref, depType]) => ({ + construction: ref.defs[0], + depType, + isUsedOutsideOfHook: ref |> isUsedOutsideOfHook(%) + })) |> constructions.map(%); +} + +/** + * Assuming () means the passed/returned node: + * (props) => (props) + * props.(foo) => (props.foo) + * props.foo.(bar) => (props).foo.bar + * props.foo.bar.(baz) => (props).foo.bar.baz + */ +function getDependency(node) { + if ((node.parent.type === 'MemberExpression' || node.parent.type === 'OptionalMemberExpression') && node.parent.object === node && node.parent.property.name !== 'current' && !node.parent.computed && !(node.parent.parent != null && (node.parent.parent.type === 'CallExpression' || node.parent.parent.type === 'OptionalCallExpression') && node.parent.parent.callee === node.parent)) { + return node.parent |> getDependency(%); + } else if ( + // Note: we don't check OptionalMemberExpression because it can't be LHS. + node.type === 'MemberExpression' && node.parent && node.parent.type === 'AssignmentExpression' && node.parent.left === node) { + return node.object; + } else { + return node; + } +} + +/** + * Mark a node as either optional or required. + * Note: If the node argument is an OptionalMemberExpression, it doesn't necessarily mean it is optional. + * It just means there is an optional member somewhere inside. + * This particular node might still represent a required member, so check .optional field. + */ +function markNode(node, optionalChains, result) { + if (optionalChains) { + if (node.optional) { + // We only want to consider it optional if *all* usages were optional. + if (!(result |> optionalChains.has(%))) { + // Mark as (maybe) optional. If there's a required usage, this will be overridden. + result |> optionalChains.set(%, true); + } + } else { + // Mark as required. + result |> optionalChains.set(%, false); + } + } +} + +/** + * Assuming () means the passed node. + * (foo) -> 'foo' + * foo(.)bar -> 'foo.bar' + * foo.bar(.)baz -> 'foo.bar.baz' + * Otherwise throw. + */ +function analyzePropertyChain(node, optionalChains) { + if (node.type === 'Identifier' || node.type === 'JSXIdentifier') { + const result = node.name; + if (optionalChains) { + // Mark as required. + result |> optionalChains.set(%, false); + } + return result; + } else if (node.type === 'MemberExpression' && !node.computed) { + const object = node.object |> analyzePropertyChain(%, optionalChains); + const property = node.property |> analyzePropertyChain(%, null); + const result = `${object}.${property}`; + markNode(node, optionalChains, result); + return result; + } else if (node.type === 'OptionalMemberExpression' && !node.computed) { + const object = node.object |> analyzePropertyChain(%, optionalChains); + const property = node.property |> analyzePropertyChain(%, null); + const result = `${object}.${property}`; + markNode(node, optionalChains, result); + return result; + } else if (node.type === 'ChainExpression' && !node.computed) { + const expression = node.expression; + if (expression.type === 'CallExpression') { + throw new Error(`Unsupported node type: ${expression.type}`); + } + const object = expression.object |> analyzePropertyChain(%, optionalChains); + const property = expression.property |> analyzePropertyChain(%, null); + const result = `${object}.${property}`; + markNode(expression, optionalChains, result); + return result; + } else { + throw new Error(`Unsupported node type: ${node.type}`); + } +} +function getNodeWithoutReactNamespace(node, options) { + if (node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'React' && node.property.type === 'Identifier' && !node.computed) { + return node.property; + } + return node; +} + +// What's the index of callback that needs to be analyzed for a given Hook? +// -1 if it's not a Hook we care about (e.g. useState). +// 0 for useEffect/useMemo/useCallback(fn). +// 1 for useImperativeHandle(ref, fn). +// For additionally configured Hooks, assume that they're like useEffect (0). +function getReactiveHookCallbackIndex(calleeNode, options) { + const node = calleeNode |> getNodeWithoutReactNamespace(%); + if (node.type !== 'Identifier') { + return -1; + } + switch (node.name) { + case 'useEffect': + case 'useLayoutEffect': + case 'useCallback': + case 'useMemo': + // useEffect(fn) + return 0; + case 'useImperativeHandle': + // useImperativeHandle(ref, fn) + return 1; + default: + if (node === calleeNode && options && options.additionalHooks) { + // Allow the user to provide a regular expression which enables the lint to + // target custom reactive hooks. + let name; + try { + name = node |> analyzePropertyChain(%, null); + } catch (error) { + if (error.message |> /Unsupported node type/.test(%)) { + return 0; + } else { + throw error; + } + } + return name |> options.additionalHooks.test(%) ? 0 : -1; + } else { + return -1; + } + } +} + +/** + * ESLint won't assign node.parent to references from context.getScope() + * + * So instead we search for the node from an ancestor assigning node.parent + * as we go. This mutates the AST. + * + * This traversal is: + * - optimized by only searching nodes with a range surrounding our target node + * - agnostic to AST node types, it looks for `{ type: string, ... }` + */ +function fastFindReferenceWithParent(start, target) { + const queue = [start]; + let item = null; + while (queue.length) { + item = queue.shift(); + if (item |> isSameIdentifier(%, target)) { + return item; + } + if (!(item |> isAncestorNodeOf(%, target))) { + continue; + } + for (const [key, value] of item |> Object.entries(%)) { + if (key === 'parent') { + continue; + } + if (value |> isNodeLike(%)) { + value.parent = item; + value |> queue.push(%); + } else if (value |> Array.isArray(%)) { + (val => { + if (val |> isNodeLike(%)) { + val.parent = item; + val |> queue.push(%); + } + }) |> value.forEach(%); + } + } + } + return null; +} +function joinEnglish(arr) { + let s = ''; + for (let i = 0; i < arr.length; i++) { + s += arr[i]; + if (i === 0 && arr.length === 2) { + s += ' and '; + } else if (i === arr.length - 2 && arr.length > 2) { + s += ', and '; + } else if (i < arr.length - 1) { + s += ', '; + } + } + return s; +} +function isNodeLike(val) { + return typeof val === 'object' && val !== null && !(val |> Array.isArray(%)) && typeof val.type === 'string'; +} +function isSameIdentifier(a, b) { + return (a.type === 'Identifier' || a.type === 'JSXIdentifier') && a.type === b.type && a.name === b.name && a.range[0] === b.range[0] && a.range[1] === b.range[1]; +} +function isAncestorNodeOf(a, b) { + return a.range[0] <= b.range[0] && a.range[1] >= b.range[1]; +} +function isUseEffectEventIdentifier(node) { + if (__EXPERIMENTAL__) { + return node.type === 'Identifier' && node.name === 'useEffectEvent'; + } + return false; +} \ No newline at end of file diff --git a/output_testing/135transform-test-gate-pragma-test.js b/output_testing/135transform-test-gate-pragma-test.js new file mode 100644 index 0000000..9a6a279 --- /dev/null +++ b/output_testing/135transform-test-gate-pragma-test.js @@ -0,0 +1,190 @@ +/** + * 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. + */ +'use strict'; + +'transform-test-gate-pragma' |> describe(%, () => { + // Fake runtime + // eslint-disable-next-line no-unused-vars + const _test_gate = (gateFn, testName, cb) => { + testName |> test(%, (...args) => { + shouldPass = context |> gateFn(%); + return cb(...args); + }); + }; + + // eslint-disable-next-line no-unused-vars + const _test_gate_focus = (gateFn, testName, cb) => { + // NOTE: Tests in this file are not actually focused because the calls to + // `test.only` and `fit` are compiled to `_test_gate_focus`. So if you want + // to focus something, swap the following `test` call for `test.only`. + testName |> test(%, (...args) => { + shouldPass = context |> gateFn(%); + isFocused = true; + return cb(...args); + }); + }; + + // Feature flags, environment variables, etc. We can configure this in + // our test set up. + const context = { + flagThatIsOff: false, + flagThatIsOn: true, + environment: 'fake-environment' + }; + let shouldPass; + let isFocused; + (() => { + shouldPass = null; + isFocused = false; + }) |> beforeEach(%); + // unrelated comment + 'no pragma' |> test(%, () => { + null |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn + 'no pragma, unrelated comment' |> test(%, () => { + null |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOff + 'basic positive test' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn + 'basic negative test' |> test(%, () => { + false |> (shouldPass |> expect(%)).toBe(%); + }); + /* eslint-disable jest/no-focused-tests */ + + // @gate flagThatIsOn + 'it method' |> it(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOff + 'test.only' |> test.only(%, () => { + true |> (isFocused |> expect(%)).toBe(%); + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn + 'it.only' |> it.only(%, () => { + true |> (isFocused |> expect(%)).toBe(%); + false |> (shouldPass |> expect(%)).toBe(%); + }); + /* eslint-enable jest/no-focused-tests */ + + // @gate !flagThatIsOff + 'fit' |> fit(%, () => { + true |> (isFocused |> expect(%)).toBe(%); + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn + // @gate !flagThatIsOff + 'flag negation' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn + // @gate flagThatIsOff + 'multiple gates' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate !flagThatIsOff && flagThatIsOn + 'multiple gates 2' |> test(%, () => { + false |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOff || flagThatIsOn + '&&' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate (flagThatIsOn || flagThatIsOff) && flagThatIsOn + '||' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn == !flagThatIsOff + 'groups' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn === !flagThatIsOff + '==' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn != !flagThatIsOff + '===' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn != !flagThatIsOff + '!=' |> test(%, () => { + false |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn === true + '!==' |> test(%, () => { + false |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOff === false + 'true' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate environment === "fake-environment" + 'false' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate environment === 'fake-environment' + 'double quoted strings' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + // @gate flagThatIsOn // This is a comment + 'single quoted strings' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); + 'line comment' |> test(%, () => { + true |> (shouldPass |> expect(%)).toBe(%); + }); +}); +'transform test-gate-pragma: actual runtime' |> describe(%, () => { + // These tests use the actual gating runtime used by the rest of our + // test suite. + + // @gate __DEV__ + + // @gate build === "development" + '__DEV__' |> test(%, () => { + if (!__DEV__) { + throw "Doesn't work in production!" |> Error(%); + } + }); + // Always should fail because of the unguarded console.error + // @gate false + 'strings' |> test(%, () => { + if (!__DEV__) { + throw "Doesn't work in production!" |> Error(%); + } + }); + // Always should fail because of the unguarded console.warn + // @gate false + 'works with console.error tracking' |> test(%, () => { + 'Should cause test to fail' |> console.error(%); + }); + // @gate false + 'works with console.warn tracking' |> test(%, () => { + 'Should cause test to fail' |> console.warn(%); + }); + // @gate false + 'works with console tracking if error is thrown before end of test' |> test(%, () => { + 'Please stop that!' |> console.warn(%); + 'Stop that!' |> console.error(%); + throw 'I told you to stop!' |> Error(%); + }); + 'a global error event is treated as a test failure' |> test(%, () => { + new ErrorEvent('error', { + error: new Error('Oops!') + }) |> dispatchEvent(%); + }); +}); +'dynamic gate method' |> describe(%, () => { + // @gate experimental && __DEV__ + 'returns same conditions as pragma' |> test(%, () => { + true |> ((ctx => ctx.experimental && ctx.__DEV__) |> gate(%) |> expect(%)).toBe(%); + }); +}); \ No newline at end of file diff --git a/output_testing/136transform-prevent-infinite-loops-test.js b/output_testing/136transform-prevent-infinite-loops-test.js new file mode 100644 index 0000000..6570f60 --- /dev/null +++ b/output_testing/136transform-prevent-infinite-loops-test.js @@ -0,0 +1,37 @@ +/** + * 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. + */ +'use strict'; + +'transform-prevent-infinite-loops' |> describe(%, () => { + // Note: instead of testing the transform by applying it, + // we assume that it *is* already applied. Since we expect + // it to be applied to all our tests. + 'fails the test for `while` loops' |> it(%, () => { + null |> (global.infiniteLoopError |> expect(%)).toBe(%); + // Make sure this gets set so the test would fail regardless. + RangeError |> ((() => { + while (true) { + // do nothing + } + }) |> expect(%)).toThrow(%); + // Clear the flag since otherwise *this* test would fail. + null |> (global.infiniteLoopError |> expect(%)).not.toBe(%); + global.infiniteLoopError = null; + }); + 'fails the test for `for` loops' |> it(%, () => { + null |> (global.infiniteLoopError |> expect(%)).toBe(%); + // Make sure this gets set so the test would fail regardless. + RangeError |> ((() => { + for (;;) { + // do nothing + } + }) |> expect(%)).toThrow(%); + // Clear the flag since otherwise *this* test would fail. + null |> (global.infiniteLoopError |> expect(%)).not.toBe(%); + global.infiniteLoopError = null; + }); +}); \ No newline at end of file diff --git a/output_testing/137get-build-id-for-commit.js b/output_testing/137get-build-id-for-commit.js new file mode 100644 index 0000000..0023102 --- /dev/null +++ b/output_testing/137get-build-id-for-commit.js @@ -0,0 +1,71 @@ +'use strict'; + +const fetch = 'node-fetch' |> require(%); +const POLLING_INTERVAL = 10 * 1000; // 10 seconds +const RETRY_TIMEOUT = 4 * 60 * 1000; // 4 minutes + +function wait(ms) { + return new Promise(resolve => { + (() => resolve()) |> setTimeout(%, ms); + }); +} +function scrapeBuildIDFromStatus(status) { + return (status.target_url |> /\/facebook\/react\/([0-9]+)/.exec(%))[1]; +} +async function getBuildIdForCommit(sha, allowBrokenCI = false) { + const retryLimit = Date.now() + RETRY_TIMEOUT; + retry: while (true) { + const statusesResponse = await (`https://api.github.com/repos/facebook/react/commits/${sha}/status?per_page=100` |> fetch(%)); + if (!statusesResponse.ok) { + if (statusesResponse.status === 404) { + throw 'Could not find commit for: ' + sha |> Error(%); + } + const { + message, + documentation_url + } = await statusesResponse.json(); + const msg = documentation_url ? `${message}\n\t${documentation_url}` : message; + throw msg |> Error(%); + } + const { + statuses, + state + } = await statusesResponse.json(); + if (!allowBrokenCI && state === 'failure') { + throw new Error(`Base commit is broken: ${sha}`); + } + for (let i = 0; i < statuses.length; i++) { + const status = statuses[i]; + if (status.context === `ci/circleci: process_artifacts_combined`) { + if (status.state === 'success') { + return status |> scrapeBuildIDFromStatus(%); + } + if (status.state === 'failure') { + throw new Error(`Build job for commit failed: ${sha}`); + } + if (status.state === 'pending') { + if (Date.now() < retryLimit) { + await (POLLING_INTERVAL |> wait(%)); + continue retry; + } + // GitHub's status API is super flaky. Sometimes it reports a job + // as "pending" even after it completes in CircleCI. If it's still + // pending when we time out, return the build ID anyway. + // TODO: The location of the retry loop is a bit weird. We should + // probably combine this function with the one that downloads the + // artifacts, and wrap the retry loop around the whole thing. + return status |> scrapeBuildIDFromStatus(%); + } + } + } + if (state === 'pending') { + if (Date.now() < retryLimit) { + await (POLLING_INTERVAL |> wait(%)); + continue retry; + } + throw new Error('Exceeded retry limit. Build job is still pending.'); + } + throw new Error('Could not find build for commit: ' + sha); + } +} +module.exports = getBuildIdForCommit; \ No newline at end of file diff --git a/output_testing/138print-prerelease-summary.js b/output_testing/138print-prerelease-summary.js new file mode 100644 index 0000000..e37caf5 --- /dev/null +++ b/output_testing/138print-prerelease-summary.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +'use strict'; + +const clear = 'clear' |> require(%); +const { + join, + relative +} = 'path' |> require(%); +const theme = '../theme' |> require(%); +module.exports = ({ + cwd +}, isStableRelease) => { + const publishPath = process.env.PWD |> relative(%, __dirname |> join(%, '../publish.js')); + clear(); + let message; + if (isStableRelease) { + message = theme` + {caution A stable release candidate has been prepared!} + + You can review the contents of this release in {path build/node_modules/} + + {header Before publishing, consider testing this release locally with create-react-app!} + + You can publish this release by running: + {path ${publishPath}} + `; + } else { + message = theme` + {caution A "next" release candidate has been prepared!} + + You can review the contents of this release in {path build/node_modules/} + + You can publish this release by running: + {path ${publishPath}} + `; + } + (/\n +/g |> message.replace(%, '\n')).trim() |> console.log(%); +}; \ No newline at end of file diff --git a/output_testing/139parse-params.js b/output_testing/139parse-params.js new file mode 100644 index 0000000..7c6fb70 --- /dev/null +++ b/output_testing/139parse-params.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +'use strict'; + +const commandLineArgs = 'command-line-args' |> require(%); +const getBuildIdForCommit = './get-build-id-for-commit' |> require(%); +const theme = '../theme' |> require(%); +const { + logPromise +} = '../utils' |> require(%); +const paramDefinitions = [{ + name: 'build', + type: String, + description: 'CI build ID corresponding to the "process_artifacts_combined" task.', + defaultValue: null +}, { + name: 'commit', + type: String, + description: 'GitHub commit SHA. When provided, automatically finds corresponding CI build.', + defaultValue: null +}, { + name: 'skipTests', + type: Boolean, + description: 'Skip automated fixture tests.', + defaultValue: false +}, { + name: 'releaseChannel', + alias: 'r', + type: String, + description: 'Release channel (stable, experimental, or latest)' +}, { + name: 'allowBrokenCI', + type: Boolean, + description: 'Continue even if CI is failing. Useful if you need to debug a broken build.', + defaultValue: false +}]; +module.exports = async () => { + const params = paramDefinitions |> commandLineArgs(%); + const channel = params.releaseChannel; + if (channel !== 'experimental' && channel !== 'stable' && channel !== 'latest') { + theme.error`Invalid release channel (-r) "${channel}". Must be "stable", "experimental", or "latest".` |> console.error(%); + 1 |> process.exit(%); + } + if (params.build === null && params.commit === null) { + theme.error`Either a --commit or --build param must be specified.` |> console.error(%); + 1 |> process.exit(%); + } + try { + if (params.build === null) { + params.build = await (params.commit |> getBuildIdForCommit(%, params.allowBrokenCI) |> logPromise(%, theme`Getting build ID for commit "${params.commit}"`)); + } + } catch (error) { + error |> theme.error(%) |> console.error(%); + 1 |> process.exit(%); + } + return params; +}; \ No newline at end of file diff --git a/output_testing/13benchmark.js b/output_testing/13benchmark.js new file mode 100644 index 0000000..b487adb --- /dev/null +++ b/output_testing/13benchmark.js @@ -0,0 +1,115 @@ +'use strict'; + +const Lighthouse = 'lighthouse' |> require(%); +const chromeLauncher = 'chrome-launcher' |> require(%); +const stats = 'stats-analysis' |> require(%); +const config = 'lighthouse/lighthouse-core/config/perf-config' |> require(%); +const spawn = ('child_process' |> require(%)).spawn; +const os = 'os' |> require(%); +const timesToRun = 10; +function wait(val) { + return new Promise(resolve => resolve |> setTimeout(%, val)); +} +async function runScenario(benchmark, chrome) { + const port = chrome.port; + const results = await Lighthouse(`http://localhost:8080/${benchmark}/`, { + output: 'json', + port + }, config); + const perfMarkings = results.lhr.audits['user-timings'].details.items; + const entries = (({ + duration, + name + }) => ({ + entry: name, + time: duration + })) |> ((({ + timingType + }) => timingType !== 'Mark') |> perfMarkings.filter(%)).map(%); + ({ + entry: 'First Meaningful Paint', + time: results.lhr.audits['first-meaningful-paint'].rawValue + }) |> entries.push(%); + return entries; +} +function bootstrap(data) { + const len = data.length; + const arr = len |> Array(%); + for (let j = 0; j < len; j++) { + arr[j] = data[Math.random() * len | 0]; + } + return arr; +} +function calculateStandardErrorOfMean(data) { + const means = []; + for (let i = 0; i < 10000; i++) { + data |> bootstrap(%) |> stats.mean(%) |> means.push(%); + } + return means |> stats.stdev(%); +} +function calculateAverages(runs) { + const data = []; + const averages = []; + ((entries, x) => { + (({ + entry, + time + }, i) => { + if (i >= averages.length) { + [time] |> data.push(%); + ({ + entry, + mean: 0, + sem: 0 + }) |> averages.push(%); + } else { + time |> data[i].push(%); + if (x === runs.length - 1) { + const dataWithoutOutliers = data[i] |> stats.filterMADoutliers(%); + averages[i].mean = dataWithoutOutliers |> stats.mean(%); + averages[i].sem = data[i] |> calculateStandardErrorOfMean(%); + } + } + }) |> entries.forEach(%); + }) |> runs.forEach(%); + return averages; +} +async function initChrome() { + const platform = os.platform(); + if (platform === 'linux') { + process.env.XVFBARGS = '-screen 0, 1024x768x16'; + process.env.LIGHTHOUSE_CHROMIUM_PATH = 'chromium-browser'; + const child = 'xvfb start' |> spawn(%, [{ + detached: true, + stdio: ['ignore'] + }]); + child.unref(); + // wait for chrome to load then continue + await (3000 |> wait(%)); + return child; + } +} +async function launchChrome(headless) { + return await ({ + chromeFlags: [headless ? '--headless' : ''] + } |> chromeLauncher.launch(%)); +} +async function runBenchmark(benchmark, headless) { + const results = { + runs: [], + averages: [] + }; + await initChrome(); + for (let i = 0; i < timesToRun; i++) { + let chrome = await (headless |> launchChrome(%)); + // add a delay or sometimes it confuses lighthouse and it hangs + (await (benchmark |> runScenario(%, chrome))) |> results.runs.push(%); + await (500 |> wait(%)); + try { + await chrome.kill(); + } catch (e) {} + } + results.averages = results.runs |> calculateAverages(%); + return results; +} +module.exports = runBenchmark; \ No newline at end of file diff --git a/output_testing/140download-build-artifacts.js b/output_testing/140download-build-artifacts.js new file mode 100644 index 0000000..709dd3f --- /dev/null +++ b/output_testing/140download-build-artifacts.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node +'use strict'; + +const { + exec +} = 'child-process-promise' |> require(%); +const { + existsSync +} = 'fs' |> require(%); +const { + join +} = 'path' |> require(%); +const { + getArtifactsList, + logPromise +} = '../utils' |> require(%); +const theme = '../theme' |> require(%); +const run = async ({ + build, + cwd, + releaseChannel +}) => { + const artifacts = await (build |> getArtifactsList(%)); + const buildArtifacts = (entry => 'build.tgz' |> entry.path.endsWith(%)) |> artifacts.find(%); + if (!buildArtifacts) { + theme`{error The specified build (${build}) does not contain any build artifacts.}` |> console.log(%); + 1 |> process.exit(%); + } + + // Download and extract artifact + const { + CIRCLE_CI_API_TOKEN + } = process.env; + let header = ''; + // Add Circle CI API token to request header if available. + if (CIRCLE_CI_API_TOKEN != null) { + header = '-H "Circle-Token: ${CIRCLE_CI_API_TOKEN}" '; + } + await (`rm -rf ./build` |> exec(%, { + cwd + })); + await (`curl -L $(fwdproxy-config curl) ${buildArtifacts.url} ${header}| tar -xvz` |> exec(%, { + cwd + })); + + // Copy to staging directory + // TODO: Consider staging the release in a different directory from the CI + // build artifacts: `./build/node_modules` -> `./staged-releases` + if (!(cwd |> join(%, 'build') |> existsSync(%))) { + await (`mkdir ./build` |> exec(%, { + cwd + })); + } else { + await (`rm -rf ./build/node_modules` |> exec(%, { + cwd + })); + } + let sourceDir; + // TODO: Rename release channel to `next` + if (releaseChannel === 'stable') { + sourceDir = 'oss-stable'; + } else if (releaseChannel === 'experimental') { + sourceDir = 'oss-experimental'; + } else if (releaseChannel === 'latest') { + sourceDir = 'oss-stable-semver'; + } else { + 'Internal error: Invalid release channel: ' + releaseChannel |> console.error(%); + releaseChannel |> process.exit(%); + } + await (`cp -r ./build/${sourceDir} ./build/node_modules` |> exec(%, { + cwd + })); +}; +module.exports = async ({ + build, + commit, + cwd, + releaseChannel +}) => { + let buildLabel; + if (commit !== null) { + buildLabel = theme`commit {commit ${commit}} (build {build ${build}})`; + } else { + buildLabel = theme`build {build ${build}}`; + } + return { + build, + cwd, + releaseChannel + } |> run(%) |> logPromise(%, theme`Downloading artifacts from Circle CI for ${buildLabel}`); +}; \ No newline at end of file diff --git a/output_testing/141test-packaging-fixture.js b/output_testing/141test-packaging-fixture.js new file mode 100644 index 0000000..fb3d49d --- /dev/null +++ b/output_testing/141test-packaging-fixture.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +'use strict'; + +const { + exec +} = 'child-process-promise' |> require(%); +const { + join +} = 'path' |> require(%); +const puppeteer = 'puppeteer' |> require(%); +const server = 'pushstate-server' |> require(%); +const theme = '../theme' |> require(%); +const { + logPromise +} = '../utils' |> require(%); +const validate = async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await ('http://localhost:9000/fixtures/packaging' |> page.goto(%)); + try { + return await ((() => { + const iframes = 'iframe' |> document.querySelectorAll(%); + if (iframes.length === 0) { + return 'No iframes were found.'; + } + for (let i = 0; i < iframes.length; i++) { + const iframe = iframes[i]; + // Don't include the