diff --git a/.eslintrc.js b/.eslintrc.js index 6998bca453f18..b6d07a79e6601 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -99,6 +99,18 @@ module.exports = { 'react-internal/warning-args': ERROR, 'react-internal/no-production-logging': ERROR, 'react-internal/no-cross-fork-imports': ERROR, + 'react-internal/no-cross-fork-types': [ + ERROR, + { + old: [ + 'firstEffect', + 'nextEffect', + // Disabled because it's also used by the Hook type. + // 'lastEffect', + ], + new: ['subtreeTag'], + }, + ], }, overrides: [ diff --git a/scripts/eslint-rules/__tests__/no-cross-fork-types-test.internal.js b/scripts/eslint-rules/__tests__/no-cross-fork-types-test.internal.js new file mode 100644 index 0000000000000..57c50a55b315f --- /dev/null +++ b/scripts/eslint-rules/__tests__/no-cross-fork-types-test.internal.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +const rule = require('../no-cross-fork-types'); +const RuleTester = require('eslint').RuleTester; +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 8, + sourceType: 'module', + }, +}); + +const newAccessWarning = + 'Field cannot be accessed inside the old reconciler fork, only the ' + + 'new fork.'; + +const oldAccessWarning = + 'Field cannot be accessed inside the new reconciler fork, only the ' + + 'old fork.'; + +ruleTester.run('eslint-rules/no-cross-fork-types', rule, { + valid: [ + { + code: ` +const a = obj.key_old; +const b = obj.key_new; +const {key_old, key_new} = obj; +`, + filename: 'ReactFiberWorkLoop.js', + }, + { + code: ` +const a = obj.key_old; +const {key_old} = obj; +`, + filename: 'ReactFiberWorkLoop.old.js', + }, + { + code: ` +const a = obj.key_new; +const {key_new} = obj; +`, + filename: 'ReactFiberWorkLoop.new.js', + }, + ], + invalid: [ + { + code: 'const a = obj.key_new;', + filename: 'ReactFiberWorkLoop.old.js', + errors: [{message: newAccessWarning}], + }, + { + code: 'const a = obj.key_old;', + filename: 'ReactFiberWorkLoop.new.js', + errors: [{message: oldAccessWarning}], + }, + + { + code: 'const {key_new} = obj;', + filename: 'ReactFiberWorkLoop.old.js', + errors: [{message: newAccessWarning}], + }, + { + code: 'const {key_old} = obj;', + filename: 'ReactFiberWorkLoop.new.js', + errors: [{message: oldAccessWarning}], + }, + { + code: 'const subtreeTag = obj.subtreeTag;', + filename: 'ReactFiberWorkLoop.old.js', + options: [{new: ['subtreeTag']}], + errors: [{message: newAccessWarning}], + }, + { + code: 'const firstEffect = obj.firstEffect;', + filename: 'ReactFiberWorkLoop.new.js', + options: [{old: ['firstEffect']}], + errors: [{message: oldAccessWarning}], + }, + ], +}); diff --git a/scripts/eslint-rules/index.js b/scripts/eslint-rules/index.js index 1451eb906d28f..0a08b0d4cfcdf 100644 --- a/scripts/eslint-rules/index.js +++ b/scripts/eslint-rules/index.js @@ -8,5 +8,6 @@ module.exports = { 'invariant-args': require('./invariant-args'), 'no-production-logging': require('./no-production-logging'), 'no-cross-fork-imports': require('./no-cross-fork-imports'), + 'no-cross-fork-types': require('./no-cross-fork-types'), }, }; diff --git a/scripts/eslint-rules/no-cross-fork-types.js b/scripts/eslint-rules/no-cross-fork-types.js new file mode 100644 index 0000000000000..eaa9f07d74e06 --- /dev/null +++ b/scripts/eslint-rules/no-cross-fork-types.js @@ -0,0 +1,126 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +/* eslint-disable no-for-of-loops/no-for-of-loops */ + +'use strict'; + +function isOldFork(filename) { + return filename.endsWith('.old.js') || filename.endsWith('.old'); +} + +function isNewFork(filename) { + return filename.endsWith('.new.js') || filename.endsWith('.new'); +} + +function warnIfNewField(context, newFields, identifier) { + const name = identifier.name; + if (name.endsWith('_new') || (newFields !== null && newFields.has(name))) { + context.report({ + node: identifier, + message: + 'Field cannot be accessed inside the old reconciler fork, only the ' + + 'new fork.', + }); + } +} + +function warnIfOldField(context, oldFields, identifier) { + const name = identifier.name; + if (name.endsWith('_old') || (oldFields !== null && oldFields.has(name))) { + context.report({ + node: identifier, + message: + 'Field cannot be accessed inside the new reconciler fork, only the ' + + 'old fork.', + }); + } +} + +module.exports = { + meta: { + type: 'problem', + fixable: 'code', + }, + create(context) { + const sourceFilename = context.getFilename(); + + if (isOldFork(sourceFilename)) { + const options = context.options; + let newFields = null; + if (options !== null) { + for (const option of options) { + if (option.new !== undefined) { + if (newFields === null) { + newFields = new Set(option.new); + } else { + for (const field of option.new) { + newFields.add(field); + } + } + } + } + } + return { + MemberExpression(node) { + const property = node.property; + if (property.type === 'Identifier') { + warnIfNewField(context, newFields, property); + } + }, + + ObjectPattern(node) { + for (const property of node.properties) { + const key = property.key; + if (key.type === 'Identifier') { + warnIfNewField(context, newFields, key); + } + } + }, + }; + } + + if (isNewFork(sourceFilename)) { + const options = context.options; + let oldFields = null; + if (options !== null) { + for (const option of options) { + if (option.old !== undefined) { + if (oldFields === null) { + oldFields = new Set(option.old); + } else { + for (const field of option.new) { + oldFields.add(field); + } + } + } + } + } + return { + MemberExpression(node) { + const property = node.property; + if (property.type === 'Identifier') { + warnIfOldField(context, oldFields, property); + } + }, + + ObjectPattern(node) { + for (const property of node.properties) { + const key = property.key; + if (key.type === 'Identifier') { + warnIfOldField(context, oldFields, key); + } + } + }, + }; + } + + return {}; + }, +};