Skip to content

Commit 57089cb

Browse files
authoredJan 24, 2024
feat!: no-restricted-imports allow multiple config entries for same path (#18021)
* feat!: no-restricted-imports allow multiple config entries for same path Fixes #15261 * add test with with more elements * update migration guide
1 parent 33d1ab0 commit 57089cb

File tree

3 files changed

+367
-43
lines changed

3 files changed

+367
-43
lines changed
 

‎docs/src/use/migrate-to-9.0.0.md

+36
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The lists below are ordered roughly by the number of users each change is expect
3030
* [New checks in `no-implicit-coercion` by default](#no-implicit-coercion)
3131
* [Case-sensitive flags in `no-invalid-regexp`](#no-invalid-regexp)
3232
* [`varsIgnorePattern` option of `no-unused-vars` no longer applies to catch arguments](#vars-ignore-pattern)
33+
* [`no-restricted-imports` now accepts multiple config entries with the same `name`](#no-restricted-imports)
3334
* [`"eslint:recommended"` and `"eslint:all"` strings no longer accepted in flat config](#string-config)
3435
* [`no-inner-declarations` has a new default behavior with a new option](#no-inner-declarations)
3536

@@ -281,6 +282,41 @@ try {
281282

282283
**Related issue(s):** [#17540](https://github.com/eslint/eslint/issues/17540)
283284

285+
## <a name="no-restricted-imports"></a> `no-restricted-imports` now accepts multiple config entries with the same `name`
286+
287+
In previous versions of ESLint, if multiple entries in the `paths` array of your configuration for the `no-restricted-imports` rule had the same `name` property, only the last one would apply, while the previous ones would be ignored.
288+
289+
As of ESLint v9.0.0, all entries apply, allowing for specifying different messages for different imported names. For example, you can now configure the rule like this:
290+
291+
```js
292+
{
293+
rules: {
294+
"no-restricted-imports": ["error", {
295+
paths: [
296+
{
297+
name: "react-native",
298+
importNames: ["Text"],
299+
message: "import 'Text' from 'ui/_components' instead"
300+
},
301+
{
302+
name: "react-native",
303+
importNames: ["View"],
304+
message: "import 'View' from 'ui/_components' instead"
305+
}
306+
]
307+
}]
308+
}
309+
}
310+
```
311+
312+
and both `import { Text } from "react-native"` and `import { View } from "react-native"` will be reported, with different messages.
313+
314+
In previous versions of ESLint, with this configuration only `import { View } from "react-native"` would be reported.
315+
316+
**To address:** If your configuration for this rule has multiple entries with the same `name`, you may need to remove unintentional ones.
317+
318+
**Related issue(s):** [#15261](https://github.com/eslint/eslint/issues/15261)
319+
284320
## <a name="string-config"></a> `"eslint:recommended"` and `"eslint:all"` no longer accepted in flat config
285321

286322
In ESLint v8.x, `eslint.config.js` could refer to `"eslint:recommended"` and `"eslint:all"` configurations by inserting a string into the config array, as in this example:

‎lib/rules/no-restricted-imports.js

+53-43
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,25 @@ module.exports = {
161161
(Object.hasOwn(options[0], "paths") || Object.hasOwn(options[0], "patterns"));
162162

163163
const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
164-
const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
164+
const groupedRestrictedPaths = restrictedPaths.reduce((memo, importSource) => {
165+
const path = typeof importSource === "string"
166+
? importSource
167+
: importSource.name;
168+
169+
if (!memo[path]) {
170+
memo[path] = [];
171+
}
172+
165173
if (typeof importSource === "string") {
166-
memo[importSource] = { message: null };
174+
memo[path].push({});
167175
} else {
168-
memo[importSource.name] = {
176+
memo[path].push({
169177
message: importSource.message,
170178
importNames: importSource.importNames
171-
};
179+
});
172180
}
173181
return memo;
174-
}, {});
182+
}, Object.create(null));
175183

176184
// Handle patterns too, either as strings or groups
177185
let restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
@@ -203,57 +211,59 @@ module.exports = {
203211
* @private
204212
*/
205213
function checkRestrictedPathAndReport(importSource, importNames, node) {
206-
if (!Object.hasOwn(restrictedPathMessages, importSource)) {
214+
if (!Object.hasOwn(groupedRestrictedPaths, importSource)) {
207215
return;
208216
}
209217

210-
const customMessage = restrictedPathMessages[importSource].message;
211-
const restrictedImportNames = restrictedPathMessages[importSource].importNames;
218+
groupedRestrictedPaths[importSource].forEach(restrictedPathEntry => {
219+
const customMessage = restrictedPathEntry.message;
220+
const restrictedImportNames = restrictedPathEntry.importNames;
212221

213-
if (restrictedImportNames) {
214-
if (importNames.has("*")) {
215-
const specifierData = importNames.get("*")[0];
222+
if (restrictedImportNames) {
223+
if (importNames.has("*")) {
224+
const specifierData = importNames.get("*")[0];
225+
226+
context.report({
227+
node,
228+
messageId: customMessage ? "everythingWithCustomMessage" : "everything",
229+
loc: specifierData.loc,
230+
data: {
231+
importSource,
232+
importNames: restrictedImportNames,
233+
customMessage
234+
}
235+
});
236+
}
216237

238+
restrictedImportNames.forEach(importName => {
239+
if (importNames.has(importName)) {
240+
const specifiers = importNames.get(importName);
241+
242+
specifiers.forEach(specifier => {
243+
context.report({
244+
node,
245+
messageId: customMessage ? "importNameWithCustomMessage" : "importName",
246+
loc: specifier.loc,
247+
data: {
248+
importSource,
249+
customMessage,
250+
importName
251+
}
252+
});
253+
});
254+
}
255+
});
256+
} else {
217257
context.report({
218258
node,
219-
messageId: customMessage ? "everythingWithCustomMessage" : "everything",
220-
loc: specifierData.loc,
259+
messageId: customMessage ? "pathWithCustomMessage" : "path",
221260
data: {
222261
importSource,
223-
importNames: restrictedImportNames,
224262
customMessage
225263
}
226264
});
227265
}
228-
229-
restrictedImportNames.forEach(importName => {
230-
if (importNames.has(importName)) {
231-
const specifiers = importNames.get(importName);
232-
233-
specifiers.forEach(specifier => {
234-
context.report({
235-
node,
236-
messageId: customMessage ? "importNameWithCustomMessage" : "importName",
237-
loc: specifier.loc,
238-
data: {
239-
importSource,
240-
customMessage,
241-
importName
242-
}
243-
});
244-
});
245-
}
246-
});
247-
} else {
248-
context.report({
249-
node,
250-
messageId: customMessage ? "pathWithCustomMessage" : "path",
251-
data: {
252-
importSource,
253-
customMessage
254-
}
255-
});
256-
}
266+
});
257267
}
258268

259269
/**

‎tests/lib/rules/no-restricted-imports.js

+278
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,284 @@ ruleTester.run("no-restricted-imports", rule, {
10201020
endColumn: 18
10211021
}]
10221022
},
1023+
1024+
// https://github.com/eslint/eslint/issues/15261
1025+
{
1026+
code: "import { Image, Text, ScrollView } from 'react-native'",
1027+
options: [{
1028+
paths: [
1029+
{
1030+
name: "react-native",
1031+
importNames: ["Text"],
1032+
message: "import Text from ui/_components instead"
1033+
},
1034+
{
1035+
name: "react-native",
1036+
importNames: ["TextInput"],
1037+
message: "import TextInput from ui/_components instead"
1038+
},
1039+
{ name: "react-native", importNames: ["View"], message: "import View from ui/_components instead " },
1040+
{
1041+
name: "react-native",
1042+
importNames: ["ScrollView"],
1043+
message: "import ScrollView from ui/_components instead"
1044+
},
1045+
{
1046+
name: "react-native",
1047+
importNames: ["KeyboardAvoidingView"],
1048+
message: "import KeyboardAvoidingView from ui/_components instead"
1049+
},
1050+
{
1051+
name: "react-native",
1052+
importNames: ["ImageBackground"],
1053+
message: "import ImageBackground from ui/_components instead"
1054+
},
1055+
{
1056+
name: "react-native",
1057+
importNames: ["Image"],
1058+
message: "import Image from ui/_components instead"
1059+
}
1060+
]
1061+
}],
1062+
errors: [
1063+
{
1064+
message: "'Image' import from 'react-native' is restricted. import Image from ui/_components instead",
1065+
type: "ImportDeclaration",
1066+
line: 1,
1067+
column: 10,
1068+
endColumn: 15
1069+
},
1070+
{
1071+
message: "'Text' import from 'react-native' is restricted. import Text from ui/_components instead",
1072+
type: "ImportDeclaration",
1073+
line: 1,
1074+
column: 17,
1075+
endColumn: 21
1076+
},
1077+
{
1078+
message: "'ScrollView' import from 'react-native' is restricted. import ScrollView from ui/_components instead",
1079+
type: "ImportDeclaration",
1080+
line: 1,
1081+
column: 23,
1082+
endColumn: 33
1083+
}
1084+
]
1085+
},
1086+
{
1087+
code: "import { foo, bar, baz } from 'mod'",
1088+
options: [{
1089+
paths: [
1090+
{
1091+
name: "mod",
1092+
importNames: ["foo"],
1093+
message: "Import foo from qux instead."
1094+
},
1095+
{
1096+
name: "mod",
1097+
importNames: ["baz"],
1098+
message: "Import baz from qux instead."
1099+
}
1100+
]
1101+
}],
1102+
errors: [
1103+
{
1104+
message: "'foo' import from 'mod' is restricted. Import foo from qux instead.",
1105+
type: "ImportDeclaration",
1106+
line: 1,
1107+
column: 10,
1108+
endColumn: 13
1109+
},
1110+
{
1111+
message: "'baz' import from 'mod' is restricted. Import baz from qux instead.",
1112+
type: "ImportDeclaration",
1113+
line: 1,
1114+
column: 20,
1115+
endColumn: 23
1116+
}
1117+
]
1118+
},
1119+
{
1120+
code: "import { foo, bar, baz, qux } from 'mod'",
1121+
options: [{
1122+
paths: [
1123+
{
1124+
name: "mod",
1125+
importNames: ["bar"],
1126+
message: "Use `barbaz` instead of `bar`."
1127+
},
1128+
{
1129+
name: "mod",
1130+
importNames: ["foo", "qux"],
1131+
message: "Don't use 'foo' and `qux` from 'mod'."
1132+
}
1133+
]
1134+
}],
1135+
errors: [
1136+
{
1137+
message: "'foo' import from 'mod' is restricted. Don't use 'foo' and `qux` from 'mod'.",
1138+
type: "ImportDeclaration",
1139+
line: 1,
1140+
column: 10,
1141+
endColumn: 13
1142+
},
1143+
{
1144+
message: "'bar' import from 'mod' is restricted. Use `barbaz` instead of `bar`.",
1145+
type: "ImportDeclaration",
1146+
line: 1,
1147+
column: 15,
1148+
endColumn: 18
1149+
},
1150+
{
1151+
message: "'qux' import from 'mod' is restricted. Don't use 'foo' and `qux` from 'mod'.",
1152+
type: "ImportDeclaration",
1153+
line: 1,
1154+
column: 25,
1155+
endColumn: 28
1156+
}
1157+
]
1158+
},
1159+
{
1160+
code: "import { foo, bar, baz, qux } from 'mod'",
1161+
options: [{
1162+
paths: [
1163+
{
1164+
name: "mod",
1165+
importNames: ["foo", "baz"],
1166+
message: "Don't use 'foo' or 'baz' from 'mod'."
1167+
},
1168+
{
1169+
name: "mod",
1170+
importNames: ["a", "c"],
1171+
message: "Don't use 'a' or 'c' from 'mod'."
1172+
},
1173+
{
1174+
name: "mod",
1175+
importNames: ["b", "bar"],
1176+
message: "Use 'b' or `bar` from 'quux/mod' instead."
1177+
}
1178+
]
1179+
}],
1180+
errors: [
1181+
{
1182+
message: "'foo' import from 'mod' is restricted. Don't use 'foo' or 'baz' from 'mod'.",
1183+
type: "ImportDeclaration",
1184+
line: 1,
1185+
column: 10,
1186+
endColumn: 13
1187+
},
1188+
{
1189+
message: "'bar' import from 'mod' is restricted. Use 'b' or `bar` from 'quux/mod' instead.",
1190+
type: "ImportDeclaration",
1191+
line: 1,
1192+
column: 15,
1193+
endColumn: 18
1194+
},
1195+
{
1196+
message: "'baz' import from 'mod' is restricted. Don't use 'foo' or 'baz' from 'mod'.",
1197+
type: "ImportDeclaration",
1198+
line: 1,
1199+
column: 20,
1200+
endColumn: 23
1201+
}
1202+
]
1203+
},
1204+
{
1205+
code: "import * as mod from 'mod'",
1206+
options: [{
1207+
paths: [
1208+
{
1209+
name: "mod",
1210+
importNames: ["foo"],
1211+
message: "Import foo from qux instead."
1212+
},
1213+
{
1214+
name: "mod",
1215+
importNames: ["bar"],
1216+
message: "Import bar from qux instead."
1217+
}
1218+
]
1219+
}],
1220+
errors: [
1221+
{
1222+
message: "* import is invalid because 'foo' from 'mod' is restricted. Import foo from qux instead.",
1223+
type: "ImportDeclaration",
1224+
line: 1,
1225+
column: 8,
1226+
endColumn: 16
1227+
},
1228+
{
1229+
message: "* import is invalid because 'bar' from 'mod' is restricted. Import bar from qux instead.",
1230+
type: "ImportDeclaration",
1231+
line: 1,
1232+
column: 8,
1233+
endColumn: 16
1234+
}
1235+
]
1236+
},
1237+
{
1238+
code: "import { foo } from 'mod'",
1239+
options: [{
1240+
paths: [
1241+
1242+
// restricts importing anything from the module
1243+
{
1244+
name: "mod"
1245+
},
1246+
1247+
// message for a specific import name
1248+
{
1249+
name: "mod",
1250+
importNames: ["bar"],
1251+
message: "Import bar from qux instead."
1252+
}
1253+
]
1254+
}],
1255+
errors: [
1256+
{
1257+
message: "'mod' import is restricted from being used.",
1258+
type: "ImportDeclaration",
1259+
line: 1,
1260+
column: 1,
1261+
endColumn: 26
1262+
}
1263+
]
1264+
},
1265+
{
1266+
code: "import { bar } from 'mod'",
1267+
options: [{
1268+
paths: [
1269+
1270+
// restricts importing anything from the module
1271+
{
1272+
name: "mod"
1273+
},
1274+
1275+
// message for a specific import name
1276+
{
1277+
name: "mod",
1278+
importNames: ["bar"],
1279+
message: "Import bar from qux instead."
1280+
}
1281+
]
1282+
}],
1283+
errors: [
1284+
{
1285+
message: "'mod' import is restricted from being used.",
1286+
type: "ImportDeclaration",
1287+
line: 1,
1288+
column: 1,
1289+
endColumn: 26
1290+
},
1291+
{
1292+
message: "'bar' import from 'mod' is restricted. Import bar from qux instead.",
1293+
type: "ImportDeclaration",
1294+
line: 1,
1295+
column: 10,
1296+
endColumn: 13
1297+
}
1298+
]
1299+
},
1300+
10231301
{
10241302
code: "import foo, { bar } from 'mod';",
10251303
options: [{

1 commit comments

Comments
 (1)

Danyalkasiri commented on Apr 24, 2024

@Danyalkasiri
Please sign in to comment.