Skip to content

Commit 87809d9

Browse files
coadofacebook-github-bot
authored andcommitted
Add no-deep-imports rule to eslint-plugin-react-native (#50542)
Summary: Pull Request resolved: #50542 After TS types generation is completed, react native deep imports will be deprecated. This rule produces warnings to let users know to use root imports instead. For more information about why this rule was added, please check [RFC](react-native-community/discussions-and-proposals#894). Changelog: [General][Added] - Added no-deep-imports rule to eslint-plugin-react-native. Reviewed By: robhogan Differential Revision: D71398004 fbshipit-source-id: 69104f69b1b1c59b5b0f115dcdd708a46d8d614d
1 parent 70cdf12 commit 87809d9

File tree

6 files changed

+357
-0
lines changed

6 files changed

+357
-0
lines changed

.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ module.exports = {
3535
'no-undef': 0,
3636
},
3737
},
38+
{
39+
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
40+
rules: {
41+
'@react-native/no-deep-imports': 0,
42+
},
43+
},
3844
{
3945
files: [
4046
'./packages/react-native/**/*.{js,flow}',

packages/eslint-config-react-native/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ module.exports = {
5353
files: ['*.jsx'],
5454
parser: '@babel/eslint-parser',
5555
},
56+
{
57+
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
58+
rules: {
59+
'@react-native/no-deep-imports': 1,
60+
},
61+
},
5662
{
5763
files: ['*.ts', '*.tsx'],
5864
parser: '@typescript-eslint/parser',
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @oncall react_native
9+
*/
10+
11+
'use strict';
12+
13+
const rule = require('../no-deep-imports.js');
14+
const {publicAPIMapping} = require('../utils.js');
15+
const ESLintTester = require('./eslint-tester.js');
16+
const path = require('path');
17+
18+
const eslintTester = new ESLintTester();
19+
20+
test('resolve all public API paths', () => {
21+
for (const subpath of Object.keys(publicAPIMapping)) {
22+
require.resolve(path.join('react-native', subpath));
23+
}
24+
});
25+
26+
eslintTester.run('../no-deep-imports', rule, {
27+
valid: [
28+
"import {View} from 'react-native';",
29+
"const {View} = require('react-native');",
30+
"import Foo from 'react-native-foo';",
31+
"import Foo from 'react-native-foo/Foo';",
32+
"import Foo from 'react/native/Foo';",
33+
],
34+
invalid: [
35+
{
36+
code: "import View from 'react-native/Libraries/Components/View/View';",
37+
errors: [
38+
{
39+
messageId: 'deepImport',
40+
data: {importPath: 'react-native/Libraries/Components/View/View'},
41+
},
42+
],
43+
output: "import {View} from 'react-native';",
44+
},
45+
{
46+
code: "const View = require('react-native/Libraries/Components/View/View');",
47+
errors: [
48+
{
49+
messageId: 'deepImport',
50+
data: {importPath: 'react-native/Libraries/Components/View/View'},
51+
},
52+
],
53+
output: "const {View} = require('react-native');",
54+
},
55+
{
56+
code: "var View = require('react-native/Libraries/Components/View/View');",
57+
errors: [
58+
{
59+
messageId: 'deepImport',
60+
data: {importPath: 'react-native/Libraries/Components/View/View'},
61+
},
62+
],
63+
output: "var {View} = require('react-native');",
64+
},
65+
{
66+
code: "import Foo from 'react-native/Libraries/Components/Foo';",
67+
errors: [
68+
{
69+
messageId: 'deepImport',
70+
data: {importPath: 'react-native/Libraries/Components/Foo'},
71+
},
72+
],
73+
output: null,
74+
},
75+
{
76+
code: "import {Foo} from 'react-native/Libraries/Components/Foo';",
77+
errors: [
78+
{
79+
messageId: 'deepImport',
80+
data: {importPath: 'react-native/Libraries/Components/Foo'},
81+
},
82+
],
83+
output: null,
84+
},
85+
{
86+
code: "const {Foo} = require('react-native/Libraries/Foo');",
87+
errors: [
88+
{
89+
messageId: 'deepImport',
90+
data: {importPath: 'react-native/Libraries/Foo'},
91+
},
92+
],
93+
output: null,
94+
},
95+
{
96+
code: "if(require('react-native/Libraries/Foo')) {};",
97+
errors: [
98+
{
99+
messageId: 'deepImport',
100+
data: {importPath: 'react-native/Libraries/Foo'},
101+
},
102+
],
103+
output: null,
104+
},
105+
],
106+
});

packages/eslint-plugin-react-native/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99

1010
exports.rules = {
1111
'platform-colors': require('./platform-colors'),
12+
'no-deep-imports': require('./no-deep-imports'),
1213
};
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
const {publicAPIMapping} = require('./utils.js');
13+
14+
module.exports = {
15+
meta: {
16+
type: 'problem',
17+
docs: {
18+
description: 'Disallow deep imports from react native',
19+
},
20+
messages: {
21+
deepImport:
22+
"'{{importPath}}' React Native deep imports are deprecated. Please use the top level import instead.",
23+
},
24+
schema: [],
25+
fixable: 'code',
26+
},
27+
28+
create: function (context) {
29+
return {
30+
ImportDeclaration(node) {
31+
if (!isDeepReactNativeImport(node.source)) {
32+
return;
33+
}
34+
if (isDefaultImport(node)) {
35+
const reactNativeSource = node.source.value.slice(
36+
'react-native/'.length,
37+
);
38+
const publicAPIDefaultComponent = publicAPIMapping[reactNativeSource];
39+
if (publicAPIDefaultComponent) {
40+
context.report({
41+
...getStandardReport(node.source),
42+
fix(fixer) {
43+
return fixer.replaceText(
44+
node,
45+
`import {${publicAPIDefaultComponent}} from 'react-native';`,
46+
);
47+
},
48+
});
49+
} else {
50+
context.report(getStandardReport(node.source));
51+
}
52+
} else {
53+
context.report(getStandardReport(node.source));
54+
}
55+
},
56+
CallExpression(node) {
57+
if (!isDeepRequire(node)) {
58+
return;
59+
}
60+
61+
const parent = node.parent;
62+
const importPath = node.arguments[0].value;
63+
64+
if (
65+
parent.type === 'VariableDeclarator' &&
66+
parent.id.type === 'Identifier'
67+
) {
68+
const reactNativeSource = importPath.slice('react-native/'.length);
69+
const publicAPIDefaultComponent = publicAPIMapping[reactNativeSource];
70+
if (publicAPIDefaultComponent) {
71+
context.report({
72+
...getStandardReport(node.arguments[0]),
73+
fix(fixer) {
74+
return fixer.replaceText(
75+
parent,
76+
`{${publicAPIDefaultComponent}} = require('react-native')`,
77+
);
78+
},
79+
});
80+
} else {
81+
context.report(getStandardReport(node.arguments[0]));
82+
}
83+
} else {
84+
context.report(getStandardReport(node.arguments[0]));
85+
}
86+
},
87+
};
88+
89+
function getStandardReport(source) {
90+
return {
91+
node: source,
92+
messageId: 'deepImport',
93+
data: {
94+
importPath: source.value,
95+
},
96+
};
97+
}
98+
99+
function isDefaultImport(node) {
100+
return (
101+
node.specifiers.length === 1 &&
102+
node.specifiers.some(
103+
specifier => specifier.type === 'ImportDefaultSpecifier',
104+
)
105+
);
106+
}
107+
108+
function isDeepRequire(node) {
109+
return (
110+
node.callee.type === 'Identifier' &&
111+
node.callee.name === 'require' &&
112+
node.arguments.length === 1 &&
113+
node.arguments[0].type === 'Literal' &&
114+
typeof node.arguments[0].value === 'string' &&
115+
isDeepReactNativeImport(node.arguments[0])
116+
);
117+
}
118+
119+
function isDeepReactNativeImport(source) {
120+
if (source.type !== 'Literal' || typeof source.value !== 'string') {
121+
return false;
122+
}
123+
124+
const importPath = source.value;
125+
const parts = importPath.split('/');
126+
return parts.length > 1 && parts[0] === 'react-native';
127+
}
128+
},
129+
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
/**
13+
* The correctness of paths is checked in the test file.
14+
* The assumption is that renaming/removing components shouldn't happen too often.
15+
* If a new component is added, it should be imported from the root.
16+
* If the path is not matched, the auto-fix won't be suggested.
17+
*/
18+
const publicAPIMapping = {
19+
'Libraries/Components/AccessibilityInfo/AccessibilityInfo':
20+
'AccessibilityInfo',
21+
'Libraries/Components/ActivityIndicator/ActivityIndicator':
22+
'ActivityIndicator',
23+
'Libraries/Components/Button': 'Button',
24+
'Libraries/Components/DrawerAndroid/DrawerLayoutAndroid':
25+
'DrawerLayoutAndroid',
26+
'Libraries/Components/LayoutConformance/LayoutConformance':
27+
'experimental_LayoutConformance',
28+
'Libraries/Lists/FlatList': 'FlatList',
29+
'Libraries/Image/Image': 'Image',
30+
'Libraries/Image/ImageBackground': 'ImageBackground',
31+
'Libraries/Components/TextInput/InputAccessoryView': 'InputAccessoryView',
32+
'Libraries/Components/Keyboard/KeyboardAvoidingView': 'KeyboardAvoidingView',
33+
'Libraries/Modal/Modal': 'Modal',
34+
'Libraries/Components/Pressable/Pressable': 'Pressable',
35+
'Libraries/Components/ProgressBarAndroid/ProgressBarAndroid':
36+
'ProgressBarAndroid',
37+
'Libraries/Components/RefreshControl/RefreshControl': 'RefreshControl',
38+
'Libraries/Components/SafeAreaView/SafeAreaView': 'SafeAreaView',
39+
'Libraries/Components/ScrollView/ScrollView': 'ScrollView',
40+
'Libraries/Lists/SectionList': 'SectionList',
41+
'Libraries/Components/StatusBar/StatusBar': 'StatusBar',
42+
'Libraries/Components/Switch/Switch': 'Switch',
43+
'Libraries/Text/Text': 'Text',
44+
'Libraries/Components/TextInput/TextInput': 'TextInput',
45+
'Libraries/Components/Touchable/Touchable': 'Touchable',
46+
'Libraries/Components/Touchable/TouchableHighlight': 'TouchableHighlight',
47+
'Libraries/Components/Touchable/TouchableNativeFeedback':
48+
'TouchableNativeFeedback',
49+
'Libraries/Components/Touchable/TouchableOpacity': 'TouchableOpacity',
50+
'Libraries/Components/Touchable/TouchableWithoutFeedback':
51+
'TouchableWithoutFeedback',
52+
'Libraries/Components/View/View': 'View',
53+
'Libraries/Lists/VirtualizedList': 'VirtualizedList',
54+
'Libraries/Lists/VirtualizedSectionList': 'VirtualizedSectionList',
55+
'Libraries/ActionSheetIOS/ActionSheetIOS': 'ActionSheetIOS',
56+
'Libraries/Alert/Alert': 'Alert',
57+
'Libraries/Animated/Animated': 'Animated',
58+
'Libraries/Utilities/Appearance': 'Appearance',
59+
'Libraries/ReactNative/AppRegistry': 'AppRegistry',
60+
'Libraries/AppState/AppState': 'AppState',
61+
'Libraries/Utilities/BackHandler': 'BackHandler',
62+
'Libraries/Components/Clipboard/Clipboard': 'Clipboard',
63+
'Libraries/Utilities/DeviceInfo': 'DeviceInfo',
64+
'src/private/devmenu/DevMenu': 'DevMenu',
65+
'Libraries/Utilities/DevSettings': 'DevSettings',
66+
'Libraries/Utilities/Dimensions': 'Dimensions',
67+
'Libraries/Animated/Easing': 'Easing',
68+
'Libraries/ReactNative/I18nManager': 'I18nManager',
69+
'Libraries/Interaction/InteractionManager': 'InteractionManager',
70+
'Libraries/Components/Keyboard/Keyboard': 'Keyboard',
71+
'Libraries/LayoutAnimation/LayoutAnimation': 'LayoutAnimation',
72+
'Libraries/Linking/Linking': 'Linking',
73+
'Libraries/LogBox/LogBox': 'LogBox',
74+
'Libraries/NativeModules/specs/NativeDialogManagerAndroid':
75+
'NativeDialogManagerAndroid',
76+
'Libraries/EventEmitter/NativeEventEmitter': 'NativeEventEmitter',
77+
'Libraries/Network/RCTNetworking': 'Networking',
78+
'Libraries/Interaction/PanResponder': 'PanResponder',
79+
'Libraries/PermissionsAndroid/PermissionsAndroid': 'PermissionsAndroid',
80+
'Libraries/Utilities/PixelRatio': 'PixelRatio',
81+
'Libraries/PushNotificationIOS/PushNotificationIOS': 'PushNotificationIOS',
82+
'Libraries/Settings/Settings': 'Settings',
83+
'Libraries/Share/Share': 'Share',
84+
'Libraries/StyleSheet/StyleSheet': 'StyleSheet',
85+
'Libraries/Performance/Systrace': 'Systrace',
86+
'Libraries/Components/ToastAndroid/ToastAndroid': 'ToastAndroid',
87+
'Libraries/TurboModule/TurboModuleRegistry': 'TurboModuleRegistry',
88+
'Libraries/ReactNative/UIManager': 'UIManager',
89+
'Libraries/Animated/useAnimatedValue': 'useAnimatedValue',
90+
'Libraries/Utilities/useColorScheme': 'useColorScheme',
91+
'Libraries/Utilities/useWindowDimensions': 'useWindowDimensions',
92+
'Libraries/UTFSequence': 'UTFSequence',
93+
'Libraries/Vibration/Vibration': 'Vibration',
94+
'Libraries/Utilities/codegenNativeComponent': 'codegenNativeComponent',
95+
'Libraries/Utilities/codegenNativeCommands': 'codegenNativeCommands',
96+
'Libraries/EventEmitter/RCTDeviceEventEmitter': 'DeviceEventEmitter',
97+
'Libraries/StyleSheet/PlatformColorValueTypesIOS': 'DynamicColorIOS',
98+
'Libraries/EventEmitter/RCTNativeAppEventEmitter': 'NativeAppEventEmitter',
99+
'Libraries/BatchedBridge/NativeModules': 'NativeModules',
100+
'Libraries/Utilities/Platform': 'Platform',
101+
'Libraries/StyleSheet/PlatformColorValueTypes': 'PlatformColor',
102+
'Libraries/StyleSheet/processColor': 'processColor',
103+
'Libraries/ReactNative/requireNativeComponent': 'requireNativeComponent',
104+
'Libraries/ReactNative/RootTag': 'RootTagContext',
105+
};
106+
107+
module.exports = {
108+
publicAPIMapping,
109+
};

0 commit comments

Comments
 (0)