diff --git a/docs/src/rules/no-restricted-imports.md b/docs/src/rules/no-restricted-imports.md index dfead74da6b..3925ef18426 100644 --- a/docs/src/rules/no-restricted-imports.md +++ b/docs/src/rules/no-restricted-imports.md @@ -108,6 +108,18 @@ Pattern matches can also be configured to be case-sensitive: }] ``` +Pattern matches can restrict specific import names only, similar to the `paths` option: + +```json +"no-restricted-imports": ["error", { + "patterns": [{ + "group": ["utils/*"], + "importNames": ["isEmpty"], + "message": "Use 'isEmpty' from lodash instead." + }] +}] +``` + To restrict the use of all Node.js core imports (via ): ```json @@ -206,6 +218,16 @@ import pick from 'lodash/pick'; import pick from 'fooBar'; ``` +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + importNames: ['isEmpty'], + message: "Use 'isEmpty' from lodash instead." +}]}]*/ + +import { isEmpty } from 'utils/collection-utils'; +``` + Examples of **correct** code for this rule: ::: correct @@ -261,6 +283,16 @@ import lodash from 'lodash'; import pick from 'food'; ``` +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + importNames: ['isEmpty'], + message: "Use 'isEmpty' from lodash instead." +}]}]*/ + +import { hasValues } from 'utils/collection-utils'; +``` + ## When Not To Use It Don't use this rule or don't include a module in the list for this rule if you want to be able to import a module in your project without an ESLint error or warning. diff --git a/lib/rules/no-restricted-imports.js b/lib/rules/no-restricted-imports.js index 60a25e5d1e1..f4838679efc 100644 --- a/lib/rules/no-restricted-imports.js +++ b/lib/rules/no-restricted-imports.js @@ -58,6 +58,14 @@ const arrayOfStringsOrObjectPatterns = { items: { type: "object", properties: { + importNames: { + type: "array", + items: { + type: "string" + }, + minItems: 1, + uniqueItems: true + }, group: { type: "array", items: { @@ -102,6 +110,14 @@ module.exports = { // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}", + patternAndImportName: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + patternAndImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}", + + patternAndEverything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + patternAndEverythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}", + everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.", // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}", @@ -159,9 +175,10 @@ module.exports = { } // relative paths are supported for this rule - const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive }) => ({ + const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive, importNames }) => ({ matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group), - customMessage: message + customMessage: message, + importNames })); // if no imports are restricted we don't need to check @@ -234,20 +251,68 @@ module.exports = { /** * Report a restricted path specifically for patterns. * @param {node} node representing the restricted path reference - * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails + * @param {Object} group contains an Ignore instance for paths, the customMessage to show on failure, + * and any restricted import names that have been specified in the config + * @param {Map} importNames Map of import names that are being imported * @returns {void} * @private */ - function reportPathForPatterns(node, group) { + function reportPathForPatterns(node, group, importNames) { const importSource = node.source.value.trim(); - context.report({ - node, - messageId: group.customMessage ? "patternWithCustomMessage" : "patterns", - data: { - importSource, - customMessage: group.customMessage + const customMessage = group.customMessage; + const restrictedImportNames = group.importNames; + + /* + * If we are not restricting to any specific import names and just the pattern itself, + * report the error and move on + */ + if (!restrictedImportNames) { + context.report({ + node, + messageId: customMessage ? "patternWithCustomMessage" : "patterns", + data: { + importSource, + customMessage + } + }); + return; + } + + if (importNames.has("*")) { + const specifierData = importNames.get("*")[0]; + + context.report({ + node, + messageId: customMessage ? "patternAndEverythingWithCustomMessage" : "patternAndEverything", + loc: specifierData.loc, + data: { + importSource, + importNames: restrictedImportNames, + customMessage + } + }); + } + + restrictedImportNames.forEach(importName => { + if (!importNames.has(importName)) { + return; } + + const specifiers = importNames.get(importName); + + specifiers.forEach(specifier => { + context.report({ + node, + messageId: customMessage ? "patternAndImportNameWithCustomMessage" : "patternAndImportName", + loc: specifier.loc, + data: { + importSource, + customMessage, + importName + } + }); + }); }); } @@ -304,7 +369,7 @@ module.exports = { checkRestrictedPathAndReport(importSource, importNames, node); restrictedPatternGroups.forEach(group => { if (isRestrictedPattern(importSource, group)) { - reportPathForPatterns(node, group); + reportPathForPatterns(node, group, importNames); } }); } diff --git a/tests/lib/rules/no-restricted-imports.js b/tests/lib/rules/no-restricted-imports.js index 07d05cbff60..5403812de36 100644 --- a/tests/lib/rules/no-restricted-imports.js +++ b/tests/lib/rules/no-restricted-imports.js @@ -262,6 +262,26 @@ ruleTester.run("no-restricted-imports", rule, { importNames: ["DisallowedObject"] }] }] + }, + { + code: "import { Bar } from '../../my/relative-module';", + options: [{ + patterns: [{ + group: ["**/my/relative-module"], + importNames: ["Foo"] + }] + }] + }, + { + + // Default import should not be reported unless importNames includes 'default' + code: "import Foo from '../../my/relative-module';", + options: [{ + patterns: [{ + group: ["**/my/relative-module"], + importNames: ["Foo"] + }] + }] } ], invalid: [{ @@ -1094,6 +1114,127 @@ ruleTester.run("no-restricted-imports", rule, { column: 1, endColumn: 45 }] + }, + { + code: "import { Foo } from '../../my/relative-module';", + options: [{ + patterns: [{ + group: ["**/my/relative-module"], + importNames: ["Foo"] + }] + }], + errors: [{ + type: "ImportDeclaration", + line: 1, + column: 10, + endColumn: 13, + message: "'Foo' import from '../../my/relative-module' is restricted from being used by a pattern." + }] + }, + { + code: "import { Foo, Bar } from '../../my/relative-module';", + options: [{ + patterns: [{ + group: ["**/my/relative-module"], + importNames: ["Foo", "Bar"], + message: "Import from @/utils instead." + }] + }], + errors: [{ + type: "ImportDeclaration", + line: 1, + column: 10, + endColumn: 13, + message: "'Foo' import from '../../my/relative-module' is restricted from being used by a pattern. Import from @/utils instead." + }, { + type: "ImportDeclaration", + line: 1, + column: 15, + endColumn: 18, + message: "'Bar' import from '../../my/relative-module' is restricted from being used by a pattern. Import from @/utils instead." + }] + }, + { + + /* + * Star import should be reported for consistency with `paths` option (see: https://github.com/eslint/eslint/pull/16059#discussion_r908749964) + * For example, import * as All allows for calling/referencing the restricted import All.Foo + */ + code: "import * as All from '../../my/relative-module';", + options: [{ + patterns: [{ + group: ["**/my/relative-module"], + importNames: ["Foo"] + }] + }], + errors: [{ + message: "* import is invalid because 'Foo' from '../../my/relative-module' is restricted from being used by a pattern.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 16 + }] + }, + { + + /* + * Star import should be reported for consistency with `paths` option (see: https://github.com/eslint/eslint/pull/16059#discussion_r908749964) + * For example, import * as All allows for calling/referencing the restricted import All.Foo + */ + code: "import * as AllWithCustomMessage from '../../my/relative-module';", + options: [{ + patterns: [{ + group: ["**/my/relative-module"], + importNames: ["Foo"], + message: "Import from @/utils instead." + }] + }], + errors: [{ + message: "* import is invalid because 'Foo' from '../../my/relative-module' is restricted from being used by a pattern. Import from @/utils instead.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 33 + }] + }, + { + code: "import def, * as ns from 'mod';", + options: [{ + patterns: [{ + group: ["mod"], + importNames: ["default"] + }] + }], + errors: [{ + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 11, + message: "'default' import from 'mod' is restricted from being used by a pattern." + }, + { + type: "ImportDeclaration", + line: 1, + column: 13, + endColumn: 20, + message: "* import is invalid because 'default' from 'mod' is restricted from being used by a pattern." + }] + }, + { + code: "import Foo from 'mod';", + options: [{ + patterns: [{ + group: ["mod"], + importNames: ["default"] + }] + }], + errors: [{ + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 11, + message: "'default' import from 'mod' is restricted from being used by a pattern." + }] } ] });