Skip to content

Commit

Permalink
Add a lint rule to disallow Haste imports (facebook#25058)
Browse files Browse the repository at this point in the history
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: facebook#25058

Differential Revision: D15515826

Pulled By: cpojer

fbshipit-source-id: d58a3c30dfe0887f8a530e3393af4af5a1ec1cac
  • Loading branch information
ide authored and facebook-github-bot committed May 30, 2019
1 parent d31e906 commit 33ee6f8
Show file tree
Hide file tree
Showing 25 changed files with 167 additions and 33 deletions.
8 changes: 8 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
],

"overrides": [
{
"files": [
"Libraries/**/*.js",
],
rules: {
'@react-native-community/no-haste-imports': 2
}
},
{
"files": [
"**/__fixtures__/**/*.js",
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Alert/NativeAlertManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Animated/src/NativeAnimatedModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Libraries/BatchedBridge/__tests__/MessageQueue-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion Libraries/BatchedBridge/__tests__/NativeModules-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Components/ToastAndroid/NativeToastAndroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {|
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Image/NativeImageEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Modal/NativeModalManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Libraries/NativeModules/specs/NativeDialogManagerAndroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Network/NativeNetworkingAndroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
4 changes: 2 additions & 2 deletions Libraries/Network/NativeNetworkingIOS.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand Down
4 changes: 2 additions & 2 deletions Libraries/PermissionsAndroid/NativePermissionsAndroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Settings/NativeSettingsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {|
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Utilities/NativeDeviceInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion Libraries/react-native/react-native-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion ReactAndroid/src/androidTest/js/ScrollViewTestModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-config-react-native-community/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
'react',
'react-hooks',
'react-native',
'@react-native-community',
'jest',
],

Expand Down
21 changes: 21 additions & 0 deletions packages/eslint-plugin-react-native-community/README.md
Original file line number Diff line number Diff line change
@@ -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"]
}
```
12 changes: 12 additions & 0 deletions packages/eslint-plugin-react-native-community/index.js
Original file line number Diff line number Diff line change
@@ -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'),
};
73 changes: 73 additions & 0 deletions packages/eslint-plugin-react-native-community/no-haste-imports.js
Original file line number Diff line number Diff line change
@@ -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'
);
}
11 changes: 11 additions & 0 deletions packages/eslint-plugin-react-native-community/package.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]:facebook/react-native.git"
},
"license": "MIT"
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,11 @@
shell-quote "1.6.1"
ws "^1.1.0"

"@react-native-community/[email protected]":
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"
Expand Down

0 comments on commit 33ee6f8

Please sign in to comment.