From 33ee6f8b99d423dbe8e604852232a63d5abe94d4 Mon Sep 17 00:00:00 2001 From: James Ide Date: Thu, 30 May 2019 07:41:37 -0700 Subject: [PATCH] Add a lint rule to disallow Haste imports (#25058) Summary: This is an ESLint plugin that infers whether an import looks like a Haste module name. To keep the linter fast and simple, it does not look in the Haste map. Instead, it looks for uppercase characters in single-name import paths, since npm has disallowed uppercase letters in package names for a long time. There are some false negatives (e.g. "merge" is a Haste module and this linter rule would not pick it up) but those are about 1.1% of the module names in the RN repo, and unit tests and integration tests will fail anyway once Haste is turned off. You can disable the lint rule on varying granular levels with ESLint's normal disabling/enabling mechanisms. Also rewrote more Haste imports so that the linter passes (i.e. fixed lint errors as part of this PR). ## Changelog [General] [Changed] - Add a lint rule to disallow Haste imports Pull Request resolved: https://github.com/facebook/react-native/pull/25058 Differential Revision: D15515826 Pulled By: cpojer fbshipit-source-id: d58a3c30dfe0887f8a530e3393af4af5a1ec1cac --- .eslintrc | 8 ++ Libraries/Alert/NativeAlertManager.js | 4 +- .../Animated/src/NativeAnimatedModule.js | 4 +- .../__tests__/MessageQueue-test.js | 2 +- .../__tests__/NativeModules-test.js | 2 +- .../NativeAccessibilityManager.js | 4 +- .../PullToRefreshViewNativeViewConfig.js | 10 +-- .../ToastAndroid/NativeToastAndroid.js | 4 +- Libraries/Image/NativeImageEditor.js | 4 +- Libraries/Modal/NativeModalManager.js | 4 +- .../specs/NativeDialogManagerAndroid.js | 4 +- Libraries/Network/NativeNetworkingAndroid.js | 4 +- Libraries/Network/NativeNetworkingIOS.js | 4 +- .../NativePermissionsAndroid.js | 4 +- Libraries/Settings/NativeSettingsManager.js | 4 +- Libraries/Utilities/NativeDeviceInfo.js | 4 +- .../react-native-implementation.js | 4 +- .../androidTest/js/ScrollViewTestModule.js | 2 +- package.json | 1 + .../index.js | 1 + .../README.md | 21 ++++++ .../index.js | 12 +++ .../no-haste-imports.js | 73 +++++++++++++++++++ .../package.json | 11 +++ yarn.lock | 5 ++ 25 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 packages/eslint-plugin-react-native-community/README.md create mode 100644 packages/eslint-plugin-react-native-community/index.js create mode 100644 packages/eslint-plugin-react-native-community/no-haste-imports.js create mode 100644 packages/eslint-plugin-react-native-community/package.json diff --git a/.eslintrc b/.eslintrc index d18f7eb828..9c36dc1804 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,6 +6,14 @@ ], "overrides": [ + { + "files": [ + "Libraries/**/*.js", + ], + rules: { + '@react-native-community/no-haste-imports': 2 + } + }, { "files": [ "**/__fixtures__/**/*.js", diff --git a/Libraries/Alert/NativeAlertManager.js b/Libraries/Alert/NativeAlertManager.js index 0a540cac72..16191e7e47 100644 --- a/Libraries/Alert/NativeAlertManager.js +++ b/Libraries/Alert/NativeAlertManager.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; export type Buttons = Array<{ text?: string, diff --git a/Libraries/Animated/src/NativeAnimatedModule.js b/Libraries/Animated/src/NativeAnimatedModule.js index b13d6f3319..80ece9d69d 100644 --- a/Libraries/Animated/src/NativeAnimatedModule.js +++ b/Libraries/Animated/src/NativeAnimatedModule.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; type EndResult = {finished: boolean}; type EndCallback = (result: EndResult) => void; diff --git a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js index f8568cae72..0c2db74b52 100644 --- a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js +++ b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js @@ -33,7 +33,7 @@ describe('MessageQueue', function() { beforeEach(function() { jest.resetModules(); MessageQueue = require('../MessageQueue'); - MessageQueueTestModule = require('MessageQueueTestModule'); + MessageQueueTestModule = require('../__mocks__/MessageQueueTestModule'); queue = new MessageQueue(); queue.registerCallableModule( 'MessageQueueTestModule', diff --git a/Libraries/BatchedBridge/__tests__/NativeModules-test.js b/Libraries/BatchedBridge/__tests__/NativeModules-test.js index 6457385f23..3bc364694e 100644 --- a/Libraries/BatchedBridge/__tests__/NativeModules-test.js +++ b/Libraries/BatchedBridge/__tests__/NativeModules-test.js @@ -40,7 +40,7 @@ describe('MessageQueue', function() { beforeEach(function() { jest.resetModules(); - global.__fbBatchedBridgeConfig = require('MessageQueueTestConfig'); + global.__fbBatchedBridgeConfig = require('../__mocks__/MessageQueueTestConfig'); BatchedBridge = require('../BatchedBridge'); NativeModules = require('../NativeModules'); }); diff --git a/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js b/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js index 2e90cdf4f8..b0fe8dd1d7 100644 --- a/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js +++ b/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; export interface Spec extends TurboModule { +getCurrentBoldTextState: ( diff --git a/Libraries/Components/RefreshControl/PullToRefreshViewNativeViewConfig.js b/Libraries/Components/RefreshControl/PullToRefreshViewNativeViewConfig.js index 27a1071076..f76c8e02dd 100644 --- a/Libraries/Components/RefreshControl/PullToRefreshViewNativeViewConfig.js +++ b/Libraries/Components/RefreshControl/PullToRefreshViewNativeViewConfig.js @@ -10,9 +10,9 @@ 'use strict'; -const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry'); -const ReactNativeViewViewConfig = require('ReactNativeViewViewConfig'); -const verifyComponentAttributeEquivalence = require('verifyComponentAttributeEquivalence'); +const ReactNativeViewConfigRegistry = require('../../Renderer/shims/ReactNativeViewConfigRegistry'); +const ReactNativeViewViewConfig = require('../View/ReactNativeViewViewConfig'); +const verifyComponentAttributeEquivalence = require('../../Utilities/verifyComponentAttributeEquivalence'); const PullToRefreshViewViewConfig = { uiViewClassName: 'PullToRefreshView', @@ -35,8 +35,8 @@ const PullToRefreshViewViewConfig = { validAttributes: { ...ReactNativeViewViewConfig.validAttributes, - tintColor: { process: require('processColor') }, - titleColor: { process: require('processColor') }, + tintColor: { process: require('../../StyleSheet/processColor') }, + titleColor: { process: require('../../StyleSheet/processColor') }, title: true, refreshing: true, onRefresh: true, diff --git a/Libraries/Components/ToastAndroid/NativeToastAndroid.js b/Libraries/Components/ToastAndroid/NativeToastAndroid.js index 89e7443874..079a7d0112 100644 --- a/Libraries/Components/ToastAndroid/NativeToastAndroid.js +++ b/Libraries/Components/ToastAndroid/NativeToastAndroid.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; export interface Spec extends TurboModule { +getConstants: () => {| diff --git a/Libraries/Image/NativeImageEditor.js b/Libraries/Image/NativeImageEditor.js index 4f4da0b538..e208cb47b6 100644 --- a/Libraries/Image/NativeImageEditor.js +++ b/Libraries/Image/NativeImageEditor.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; export interface Spec extends TurboModule { +cropImage: ( diff --git a/Libraries/Modal/NativeModalManager.js b/Libraries/Modal/NativeModalManager.js index 28bf3d9a54..d293caf68f 100644 --- a/Libraries/Modal/NativeModalManager.js +++ b/Libraries/Modal/NativeModalManager.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; export interface Spec extends TurboModule { // RCTEventEmitter diff --git a/Libraries/NativeModules/specs/NativeDialogManagerAndroid.js b/Libraries/NativeModules/specs/NativeDialogManagerAndroid.js index 1ffb71c8ce..d1e1f9a7c8 100644 --- a/Libraries/NativeModules/specs/NativeDialogManagerAndroid.js +++ b/Libraries/NativeModules/specs/NativeDialogManagerAndroid.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; /* 'buttonClicked' | 'dismissed' */ type DialogAction = string; diff --git a/Libraries/Network/NativeNetworkingAndroid.js b/Libraries/Network/NativeNetworkingAndroid.js index 6db46cef70..06ccdf29fa 100644 --- a/Libraries/Network/NativeNetworkingAndroid.js +++ b/Libraries/Network/NativeNetworkingAndroid.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; type Header = [string, string]; diff --git a/Libraries/Network/NativeNetworkingIOS.js b/Libraries/Network/NativeNetworkingIOS.js index f61b3520a0..5c685de784 100644 --- a/Libraries/Network/NativeNetworkingIOS.js +++ b/Libraries/Network/NativeNetworkingIOS.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; export interface Spec extends TurboModule { +sendRequest: ( diff --git a/Libraries/PermissionsAndroid/NativePermissionsAndroid.js b/Libraries/PermissionsAndroid/NativePermissionsAndroid.js index fbac01fc8d..025f9af184 100644 --- a/Libraries/PermissionsAndroid/NativePermissionsAndroid.js +++ b/Libraries/PermissionsAndroid/NativePermissionsAndroid.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; // TODO: Use proper enum types. export type PermissionStatus = string; diff --git a/Libraries/Settings/NativeSettingsManager.js b/Libraries/Settings/NativeSettingsManager.js index fd050c996b..19de825248 100644 --- a/Libraries/Settings/NativeSettingsManager.js +++ b/Libraries/Settings/NativeSettingsManager.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; export interface Spec extends TurboModule { +getConstants: () => {| diff --git a/Libraries/Utilities/NativeDeviceInfo.js b/Libraries/Utilities/NativeDeviceInfo.js index dd90580654..8e0b785a6f 100644 --- a/Libraries/Utilities/NativeDeviceInfo.js +++ b/Libraries/Utilities/NativeDeviceInfo.js @@ -10,8 +10,8 @@ 'use strict'; -import type {TurboModule} from 'RCTExport'; -import * as TurboModuleRegistry from 'TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; type DisplayMetricsAndroid = {| width: number, diff --git a/Libraries/react-native/react-native-implementation.js b/Libraries/react-native/react-native-implementation.js index 94aeecb539..8e0a72c5ce 100644 --- a/Libraries/react-native/react-native-implementation.js +++ b/Libraries/react-native/react-native-implementation.js @@ -10,8 +10,10 @@ 'use strict'; +/* eslint-disable @react-native-community/no-haste-imports */ + const invariant = require('invariant'); -const warnOnce = require('warnOnce'); +const warnOnce = require('../Utilities/warnOnce'); // Export React, plus some native additions. module.exports = { diff --git a/ReactAndroid/src/androidTest/js/ScrollViewTestModule.js b/ReactAndroid/src/androidTest/js/ScrollViewTestModule.js index 5da2b7686c..3e09724580 100644 --- a/ReactAndroid/src/androidTest/js/ScrollViewTestModule.js +++ b/ReactAndroid/src/androidTest/js/ScrollViewTestModule.js @@ -25,7 +25,7 @@ const {ScrollListener} = NativeModules; const NUM_ITEMS = 100; -import type {PressEvent} from 'CoreEventTypes'; +import type {PressEvent} from 'react-native/Libraries/Types/CoreEventTypes'; // Shared by integration tests for ScrollView and HorizontalScrollView diff --git a/package.json b/package.json index ec40027e2d..d9bbd3257e 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "devDependencies": { "@babel/core": "^7.0.0", "@babel/generator": "^7.0.0", + "@react-native-community/eslint-plugin": "1.0.0", "@reactions/component": "^2.0.2", "async": "^2.4.0", "babel-eslint": "10.0.1", diff --git a/packages/eslint-config-react-native-community/index.js b/packages/eslint-config-react-native-community/index.js index d1cfc69487..8691452b8f 100644 --- a/packages/eslint-config-react-native-community/index.js +++ b/packages/eslint-config-react-native-community/index.js @@ -22,6 +22,7 @@ module.exports = { 'react', 'react-hooks', 'react-native', + '@react-native-community', 'jest', ], diff --git a/packages/eslint-plugin-react-native-community/README.md b/packages/eslint-plugin-react-native-community/README.md new file mode 100644 index 0000000000..262c7e9217 --- /dev/null +++ b/packages/eslint-plugin-react-native-community/README.md @@ -0,0 +1,21 @@ +# eslint-plugin-react-native-community + +This plugin is intended to be used in `@react-native-community/eslint-plugin`. You probably want to install that package instead. + +## Installation + +``` +yarn add --dev eslint @react-native-community/eslint-plugin +``` + +*Note: We're using `yarn` to install deps. Feel free to change commands to use `npm` 3+ and `npx` if you like* + +## Usage + +Add to your eslint config (`.eslintrc`, or `eslintConfig` field in `package.json`): + +```json +{ + "plugins": ["@react-native-community"] +} +``` diff --git a/packages/eslint-plugin-react-native-community/index.js b/packages/eslint-plugin-react-native-community/index.js new file mode 100644 index 0000000000..f016505750 --- /dev/null +++ b/packages/eslint-plugin-react-native-community/index.js @@ -0,0 +1,12 @@ +/** + * 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. + * + * @format + */ + +exports.rules = { + 'no-haste-imports': require('./no-haste-imports'), +}; diff --git a/packages/eslint-plugin-react-native-community/no-haste-imports.js b/packages/eslint-plugin-react-native-community/no-haste-imports.js new file mode 100644 index 0000000000..5282c6950e --- /dev/null +++ b/packages/eslint-plugin-react-native-community/no-haste-imports.js @@ -0,0 +1,73 @@ +/** + * 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. + * + * @format + */ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: + 'disallow Haste module names in import statements and require calls', + }, + schema: [], + }, + + create(context) { + return { + ImportDeclaration(node) { + checkImportForHaste(context, node.source.value, node.source); + }, + CallExpression(node) { + if (isStaticRequireCall(node)) { + const [firstArgument] = node.arguments; + checkImportForHaste(context, firstArgument.value, firstArgument); + } + }, + }; + }, +}; + +function checkImportForHaste(context, importPath, node) { + if (isLikelyHasteModuleName(importPath)) { + context.report({ + node, + message: `"${importPath}" appears to be a Haste module name. Use path-based imports instead.`, + }); + } +} + +function isLikelyHasteModuleName(importPath) { + // Our heuristic assumes an import path is a Haste module name if it is not a + // path and doesn't appear to be an npm package. For several years, npm has + // disallowed uppercase characters in package names. + // + // This heuristic has a ~1% false negative rate for the filenames in React + // Native, which is acceptable since the linter will not complain wrongly and + // the rate is so low. False negatives that slip through will be caught by + // tests with Haste disabled. + return ( + // Exclude relative paths + !importPath.startsWith('.') && + // Exclude package-internal paths and scoped packages + !importPath.includes('/') && + // Include camelCase and UpperCamelCase + /[A-Z]/.test(importPath) + ); +} + +function isStaticRequireCall(node) { + return ( + node && + node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'require' && + node.arguments.length === 1 && + node.arguments[0].type === 'Literal' && + typeof node.arguments[0].value === 'string' + ); +} diff --git a/packages/eslint-plugin-react-native-community/package.json b/packages/eslint-plugin-react-native-community/package.json new file mode 100644 index 0000000000..5a6c28f26d --- /dev/null +++ b/packages/eslint-plugin-react-native-community/package.json @@ -0,0 +1,11 @@ +{ + "name": "@react-native-community/eslint-plugin", + "version": "1.0.0", + "description": "ESLint rules for @react-native-community/eslint-config", + "main": "index.js", + "repository": { + "type": "git", + "url": "git@github.com:facebook/react-native.git" + }, + "license": "MIT" +} diff --git a/yarn.lock b/yarn.lock index 1db6f52c69..37f37f1218 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1160,6 +1160,11 @@ shell-quote "1.6.1" ws "^1.1.0" +"@react-native-community/eslint-plugin@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@react-native-community/eslint-plugin/-/eslint-plugin-1.0.0.tgz#ae9a430f2c5795debca491f15a989fce86ea75a0" + integrity sha512-GLhSN8dRt4lpixPQh+8prSCy6PYk/MT/mvji/ojAd5yshowDo6HFsimCSTD/uWAdjpUq91XK9tVdTNWfGRlKQA== + "@reactions/component@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@reactions/component/-/component-2.0.2.tgz#40f8c1c2c37baabe57a0c944edb9310dc1ec6642"