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
This commit is contained in:
Родитель
d31e90648e
Коммит
33ee6f8b99
|
@ -6,6 +6,14 @@
|
||||||
],
|
],
|
||||||
|
|
||||||
"overrides": [
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"Libraries/**/*.js",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@react-native-community/no-haste-imports': 2
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
"**/__fixtures__/**/*.js",
|
"**/__fixtures__/**/*.js",
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
export type Buttons = Array<{
|
export type Buttons = Array<{
|
||||||
text?: string,
|
text?: string,
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
type EndResult = {finished: boolean};
|
type EndResult = {finished: boolean};
|
||||||
type EndCallback = (result: EndResult) => void;
|
type EndCallback = (result: EndResult) => void;
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('MessageQueue', function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
MessageQueue = require('../MessageQueue');
|
MessageQueue = require('../MessageQueue');
|
||||||
MessageQueueTestModule = require('MessageQueueTestModule');
|
MessageQueueTestModule = require('../__mocks__/MessageQueueTestModule');
|
||||||
queue = new MessageQueue();
|
queue = new MessageQueue();
|
||||||
queue.registerCallableModule(
|
queue.registerCallableModule(
|
||||||
'MessageQueueTestModule',
|
'MessageQueueTestModule',
|
||||||
|
|
|
@ -40,7 +40,7 @@ describe('MessageQueue', function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
||||||
global.__fbBatchedBridgeConfig = require('MessageQueueTestConfig');
|
global.__fbBatchedBridgeConfig = require('../__mocks__/MessageQueueTestConfig');
|
||||||
BatchedBridge = require('../BatchedBridge');
|
BatchedBridge = require('../BatchedBridge');
|
||||||
NativeModules = require('../NativeModules');
|
NativeModules = require('../NativeModules');
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
export interface Spec extends TurboModule {
|
export interface Spec extends TurboModule {
|
||||||
+getCurrentBoldTextState: (
|
+getCurrentBoldTextState: (
|
||||||
|
|
|
@ -10,9 +10,9 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry');
|
const ReactNativeViewConfigRegistry = require('../../Renderer/shims/ReactNativeViewConfigRegistry');
|
||||||
const ReactNativeViewViewConfig = require('ReactNativeViewViewConfig');
|
const ReactNativeViewViewConfig = require('../View/ReactNativeViewViewConfig');
|
||||||
const verifyComponentAttributeEquivalence = require('verifyComponentAttributeEquivalence');
|
const verifyComponentAttributeEquivalence = require('../../Utilities/verifyComponentAttributeEquivalence');
|
||||||
|
|
||||||
const PullToRefreshViewViewConfig = {
|
const PullToRefreshViewViewConfig = {
|
||||||
uiViewClassName: 'PullToRefreshView',
|
uiViewClassName: 'PullToRefreshView',
|
||||||
|
@ -35,8 +35,8 @@ const PullToRefreshViewViewConfig = {
|
||||||
|
|
||||||
validAttributes: {
|
validAttributes: {
|
||||||
...ReactNativeViewViewConfig.validAttributes,
|
...ReactNativeViewViewConfig.validAttributes,
|
||||||
tintColor: { process: require('processColor') },
|
tintColor: { process: require('../../StyleSheet/processColor') },
|
||||||
titleColor: { process: require('processColor') },
|
titleColor: { process: require('../../StyleSheet/processColor') },
|
||||||
title: true,
|
title: true,
|
||||||
refreshing: true,
|
refreshing: true,
|
||||||
onRefresh: true,
|
onRefresh: true,
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
export interface Spec extends TurboModule {
|
export interface Spec extends TurboModule {
|
||||||
+getConstants: () => {|
|
+getConstants: () => {|
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
export interface Spec extends TurboModule {
|
export interface Spec extends TurboModule {
|
||||||
+cropImage: (
|
+cropImage: (
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
export interface Spec extends TurboModule {
|
export interface Spec extends TurboModule {
|
||||||
// RCTEventEmitter
|
// RCTEventEmitter
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
/* 'buttonClicked' | 'dismissed' */
|
/* 'buttonClicked' | 'dismissed' */
|
||||||
type DialogAction = string;
|
type DialogAction = string;
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
type Header = [string, string];
|
type Header = [string, string];
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
export interface Spec extends TurboModule {
|
export interface Spec extends TurboModule {
|
||||||
+sendRequest: (
|
+sendRequest: (
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
// TODO: Use proper enum types.
|
// TODO: Use proper enum types.
|
||||||
export type PermissionStatus = string;
|
export type PermissionStatus = string;
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
export interface Spec extends TurboModule {
|
export interface Spec extends TurboModule {
|
||||||
+getConstants: () => {|
|
+getConstants: () => {|
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {TurboModule} from 'RCTExport';
|
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||||
import * as TurboModuleRegistry from 'TurboModuleRegistry';
|
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||||
|
|
||||||
type DisplayMetricsAndroid = {|
|
type DisplayMetricsAndroid = {|
|
||||||
width: number,
|
width: number,
|
||||||
|
|
|
@ -10,8 +10,10 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
/* eslint-disable @react-native-community/no-haste-imports */
|
||||||
|
|
||||||
const invariant = require('invariant');
|
const invariant = require('invariant');
|
||||||
const warnOnce = require('warnOnce');
|
const warnOnce = require('../Utilities/warnOnce');
|
||||||
|
|
||||||
// Export React, plus some native additions.
|
// Export React, plus some native additions.
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -25,7 +25,7 @@ const {ScrollListener} = NativeModules;
|
||||||
|
|
||||||
const NUM_ITEMS = 100;
|
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
|
// Shared by integration tests for ScrollView and HorizontalScrollView
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
"@babel/generator": "^7.0.0",
|
"@babel/generator": "^7.0.0",
|
||||||
|
"@react-native-community/eslint-plugin": "1.0.0",
|
||||||
"@reactions/component": "^2.0.2",
|
"@reactions/component": "^2.0.2",
|
||||||
"async": "^2.4.0",
|
"async": "^2.4.0",
|
||||||
"babel-eslint": "10.0.1",
|
"babel-eslint": "10.0.1",
|
||||||
|
|
|
@ -22,6 +22,7 @@ module.exports = {
|
||||||
'react',
|
'react',
|
||||||
'react-hooks',
|
'react-hooks',
|
||||||
'react-native',
|
'react-native',
|
||||||
|
'@react-native-community',
|
||||||
'jest',
|
'jest',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
```
|
|
@ -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'),
|
||||||
|
};
|
|
@ -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'
|
||||||
|
);
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -1160,6 +1160,11 @@
|
||||||
shell-quote "1.6.1"
|
shell-quote "1.6.1"
|
||||||
ws "^1.1.0"
|
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":
|
"@reactions/component@^2.0.2":
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@reactions/component/-/component-2.0.2.tgz#40f8c1c2c37baabe57a0c944edb9310dc1ec6642"
|
resolved "https://registry.yarnpkg.com/@reactions/component/-/component-2.0.2.tgz#40f8c1c2c37baabe57a0c944edb9310dc1ec6642"
|
||||||
|
|
Загрузка…
Ссылка в новой задаче