diff --git a/docs/src/rules/no-restricted-imports.md b/docs/src/rules/no-restricted-imports.md index 2ce0d8f9d1a..2211fd21c2f 100644 --- a/docs/src/rules/no-restricted-imports.md +++ b/docs/src/rules/no-restricted-imports.md @@ -230,6 +230,58 @@ import { AllowedObject as DisallowedObject } from "foo"; ::: +#### allowImportNames + +This option is an array. Inverse of `importNames`, `allowImportNames` allows the imports that are specified inside this array. So it restricts all imports from a module, except specified allowed ones. + +Note: `allowImportNames` cannot be used in combination with `importNames`. + +```json +"no-restricted-imports": ["error", { + "paths": [{ + "name": "import-foo", + "allowImportNames": ["Bar"], + "message": "Please use only Bar from import-foo." + }] +}] +``` + +Examples of **incorrect** code for `allowImportNames` in `paths`: + +Disallowing all import names except 'AllowedObject'. + +::: incorrect { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Please use only 'AllowedObject' from 'foo'." +}]}]*/ + +import { DisallowedObject } from "foo"; +``` + +::: + +Examples of **correct** code for `allowImportNames` in `paths`: + +Disallowing all import names except 'AllowedObject'. + +::: correct { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Only use 'AllowedObject' from 'foo'." +}]}]*/ + +import { AllowedObject } from "foo"; +``` + +::: + ### patterns This is also an object option whose value is an array. This option allows you to specify multiple modules to restrict using `gitignore`-style patterns. @@ -445,6 +497,54 @@ import { hasValues } from 'utils/collection-utils'; ::: +#### allowImportNames + +You can also specify `allowImportNames` on objects inside of `patterns`. In this case, the specified names are applied only to the specified `group`. + +Note: `allowImportNames` cannot be used in combination with `importNames`, `importNamePattern` or `allowImportNamePattern`. + +```json +"no-restricted-imports": ["error", { + "patterns": [{ + "group": ["utils/*"], + "allowImportNames": ["isEmpty"], + "message": "Please use only 'isEmpty' from utils." + }] +}] +``` + +Examples of **incorrect** code for `allowImportNames` in `patterns`: + +::: incorrect { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + allowImportNames: ['isEmpty'], + message: "Please use only 'isEmpty' from utils." +}]}]*/ + +import { hasValues } from 'utils/collection-utils'; +``` + +::: + +Examples of **correct** code for `allowImportNames` in `patterns`: + +::: correct { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + allowImportNames: ['isEmpty'], + message: "Please use only 'isEmpty' from utils." +}]}]*/ + +import { isEmpty } from 'utils/collection-utils'; +``` + +::: + #### importNamePattern This option allows you to use regex patterns to restrict import names: @@ -518,6 +618,51 @@ import isEmpty, { hasValue } from 'utils/collection-utils'; ::: +#### allowImportNamePattern + +This is a string option. Inverse of `importNamePattern`, this option allows imports that matches the specified regex pattern. So it restricts all imports from a module, except specified allowed patterns. + +Note: `allowImportNamePattern` cannot be used in combination with `importNames`, `importNamePattern` or `allowImportNames`. + +```json +"no-restricted-imports": ["error", { + "patterns": [{ + "group": ["import-foo/*"], + "allowImportNamePattern": "^foo", + }] +}] +``` + +Examples of **incorrect** code for `allowImportNamePattern` option: + +::: incorrect { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + allowImportNamePattern: '^has' +}]}]*/ + +import { isEmpty } from 'utils/collection-utils'; +``` + +::: + +Examples of **correct** code for `allowImportNamePattern` option: + +::: correct { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + allowImportNamePattern: '^is' +}]}]*/ + +import { isEmpty } 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 afd0bbb8ba2..062be909ef0 100644 --- a/lib/rules/no-restricted-imports.js +++ b/lib/rules/no-restricted-imports.js @@ -34,10 +34,17 @@ const arrayOfStringsOrObjects = { items: { type: "string" } + }, + allowImportNames: { + type: "array", + items: { + type: "string" + } } }, additionalProperties: false, - required: ["name"] + required: ["name"], + not: { required: ["importNames", "allowImportNames"] } } ] }, @@ -66,6 +73,14 @@ const arrayOfStringsOrObjectPatterns = { minItems: 1, uniqueItems: true }, + allowImportNames: { + type: "array", + items: { + type: "string" + }, + minItems: 1, + uniqueItems: true + }, group: { type: "array", items: { @@ -77,6 +92,9 @@ const arrayOfStringsOrObjectPatterns = { importNamePattern: { type: "string" }, + allowImportNamePattern: { + type: "string" + }, message: { type: "string", minLength: 1 @@ -86,7 +104,16 @@ const arrayOfStringsOrObjectPatterns = { } }, additionalProperties: false, - required: ["group"] + required: ["group"], + not: { + anyOf: [ + { required: ["importNames", "allowImportNames"] }, + { required: ["importNamePattern", "allowImportNamePattern"] }, + { required: ["importNames", "allowImportNamePattern"] }, + { required: ["importNamePattern", "allowImportNames"] }, + { required: ["allowImportNames", "allowImportNamePattern"] } + ] + } }, uniqueItems: true } @@ -131,7 +158,23 @@ module.exports = { importName: "'{{importName}}' import from '{{importSource}}' is restricted.", // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period - importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}" + importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}", + + allowedImportName: "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + allowedImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed. {{customMessage}}", + + everythingWithAllowImportNames: "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + everythingWithAllowImportNamesAndCustomMessage: "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed. {{customMessage}}", + + allowedImportNamePattern: "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + allowedImportNamePatternWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'. {{customMessage}}", + + everythingWithAllowedImportNamePattern: "* import is invalid because only imports that match the pattern '{{allowedImportNamePattern}}' from '{{importSource}}' are allowed.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + everythingWithAllowedImportNamePatternWithCustomMessage: "* import is invalid because only imports that match the pattern '{{allowedImportNamePattern}}' from '{{importSource}}' are allowed. {{customMessage}}" }, schema: { @@ -175,7 +218,8 @@ module.exports = { } else { memo[path].push({ message: importSource.message, - importNames: importSource.importNames + importNames: importSource.importNames, + allowImportNames: importSource.allowImportNames }); } return memo; @@ -190,12 +234,18 @@ module.exports = { } // relative paths are supported for this rule - const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive, importNames, importNamePattern }) => ({ - matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group), - customMessage: message, - importNames, - importNamePattern - })); + const restrictedPatternGroups = restrictedPatterns.map( + ({ group, message, caseSensitive, importNames, importNamePattern, allowImportNames, allowImportNamePattern }) => ( + { + matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group), + customMessage: message, + importNames, + importNamePattern, + allowImportNames, + allowImportNamePattern + } + ) + ); // if no imports are restricted we don't need to check if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) { @@ -218,42 +268,9 @@ module.exports = { groupedRestrictedPaths[importSource].forEach(restrictedPathEntry => { const customMessage = restrictedPathEntry.message; const restrictedImportNames = restrictedPathEntry.importNames; + const allowedImportNames = restrictedPathEntry.allowImportNames; - if (restrictedImportNames) { - if (importNames.has("*")) { - const specifierData = importNames.get("*")[0]; - - context.report({ - node, - messageId: customMessage ? "everythingWithCustomMessage" : "everything", - loc: specifierData.loc, - data: { - importSource, - importNames: restrictedImportNames, - customMessage - } - }); - } - - restrictedImportNames.forEach(importName => { - if (importNames.has(importName)) { - const specifiers = importNames.get(importName); - - specifiers.forEach(specifier => { - context.report({ - node, - messageId: customMessage ? "importNameWithCustomMessage" : "importName", - loc: specifier.loc, - data: { - importSource, - customMessage, - importName - } - }); - }); - } - }); - } else { + if (!restrictedImportNames && !allowedImportNames) { context.report({ node, messageId: customMessage ? "pathWithCustomMessage" : "path", @@ -262,7 +279,72 @@ module.exports = { customMessage } }); + + return; } + + importNames.forEach((specifiers, importName) => { + if (importName === "*") { + const [specifier] = specifiers; + + if (restrictedImportNames) { + context.report({ + node, + messageId: customMessage ? "everythingWithCustomMessage" : "everything", + loc: specifier.loc, + data: { + importSource, + importNames: restrictedImportNames, + customMessage + } + }); + } else if (allowedImportNames) { + context.report({ + node, + messageId: customMessage ? "everythingWithAllowImportNamesAndCustomMessage" : "everythingWithAllowImportNames", + loc: specifier.loc, + data: { + importSource, + allowedImportNames, + customMessage + } + }); + } + + return; + } + + if (restrictedImportNames && restrictedImportNames.includes(importName)) { + specifiers.forEach(specifier => { + context.report({ + node, + messageId: customMessage ? "importNameWithCustomMessage" : "importName", + loc: specifier.loc, + data: { + importSource, + customMessage, + importName + } + }); + }); + } + + if (allowedImportNames && !allowedImportNames.includes(importName)) { + specifiers.forEach(specifier => { + context.report({ + node, + loc: specifier.loc, + messageId: customMessage ? "allowedImportNameWithCustomMessage" : "allowedImportName", + data: { + importSource, + customMessage, + importName, + allowedImportNames + } + }); + }); + } + }); }); } @@ -281,12 +363,14 @@ module.exports = { const customMessage = group.customMessage; const restrictedImportNames = group.importNames; const restrictedImportNamePattern = group.importNamePattern ? new RegExp(group.importNamePattern, "u") : null; + const allowedImportNames = group.allowImportNames; + const allowedImportNamePattern = group.allowImportNamePattern ? new RegExp(group.allowImportNamePattern, "u") : null; - /* + /** * If we are not restricting to any specific import names and just the pattern itself, * report the error and move on */ - if (!restrictedImportNames && !restrictedImportNamePattern) { + if (!restrictedImportNames && !allowedImportNames && !restrictedImportNamePattern && !allowedImportNamePattern) { context.report({ node, messageId: customMessage ? "patternWithCustomMessage" : "patterns", @@ -313,6 +397,28 @@ module.exports = { customMessage } }); + } else if (allowedImportNames) { + context.report({ + node, + messageId: customMessage ? "everythingWithAllowImportNamesAndCustomMessage" : "everythingWithAllowImportNames", + loc: specifier.loc, + data: { + importSource, + allowedImportNames, + customMessage + } + }); + } else if (allowedImportNamePattern) { + context.report({ + node, + messageId: customMessage ? "everythingWithAllowedImportNamePatternWithCustomMessage" : "everythingWithAllowedImportNamePattern", + loc: specifier.loc, + data: { + importSource, + allowedImportNamePattern, + customMessage + } + }); } else { context.report({ node, @@ -346,6 +452,36 @@ module.exports = { }); }); } + + if (allowedImportNames && !allowedImportNames.includes(importName)) { + specifiers.forEach(specifier => { + context.report({ + node, + messageId: customMessage ? "allowedImportNameWithCustomMessage" : "allowedImportName", + loc: specifier.loc, + data: { + importSource, + customMessage, + importName, + allowedImportNames + } + }); + }); + } else if (allowedImportNamePattern && !allowedImportNamePattern.test(importName)) { + specifiers.forEach(specifier => { + context.report({ + node, + messageId: customMessage ? "allowedImportNamePatternWithCustomMessage" : "allowedImportNamePattern", + loc: specifier.loc, + data: { + importSource, + customMessage, + importName, + allowedImportNamePattern + } + }); + }); + } }); } diff --git a/tests/lib/rules/no-restricted-imports.js b/tests/lib/rules/no-restricted-imports.js index af50d44e6e7..e0456247d4b 100644 --- a/tests/lib/rules/no-restricted-imports.js +++ b/tests/lib/rules/no-restricted-imports.js @@ -375,6 +375,61 @@ ruleTester.run("no-restricted-imports", rule, { importNamePattern: "^Foo" }] }] + }, + { + code: "import { AllowedObject } from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Please import anything except 'AllowedObject' from /bar/ instead." + }] + }] + }, + { + code: "import { foo } from 'foo';", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["foo"] + }] + }] + }, + { + code: "import { foo } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNames: ["foo"] + }] + }] + }, + { + code: "export { bar } from 'foo';", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["bar"] + }] + }] + }, + { + code: "export { bar } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNames: ["bar"] + }] + }] + }, + { + code: "import { Foo } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNamePattern: "^Foo" + }] + }] } ], invalid: [{ @@ -1953,6 +2008,204 @@ ruleTester.run("no-restricted-imports", rule, { endColumn: 9, message: "* import is invalid because import name matching '/^Foo/u' pattern from 'foo' is restricted from being used." }] + }, + { + code: "export { Bar } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNamePattern: "^Foo" + }] + }], + errors: [{ + type: "ExportNamedDeclaration", + line: 1, + column: 10, + endColumn: 13, + message: "'Bar' import from 'foo' is restricted because only imports that match the pattern '/^Foo/u' are allowed from 'foo'." + }] + }, + { + code: "export { Bar } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNamePattern: "^Foo", + message: "Only imports that match the pattern '/^Foo/u' are allowed to be imported from 'foo'." + }] + }], + errors: [{ + type: "ExportNamedDeclaration", + line: 1, + column: 10, + endColumn: 13, + message: "'Bar' import from 'foo' is restricted because only imports that match the pattern '/^Foo/u' are allowed from 'foo'. Only imports that match the pattern '/^Foo/u' are allowed to be imported from 'foo'." + }] + }, + { + code: "import { AllowedObject, DisallowedObject } from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"] + }] + }], + errors: [{ + message: "'DisallowedObject' import from 'foo' is restricted because only 'AllowedObject' import(s) is/are allowed.", + type: "ImportDeclaration", + line: 1, + column: 25, + endColumn: 41 + }] + }, + { + code: "import { AllowedObject, DisallowedObject } from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Only 'AllowedObject' is allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "'DisallowedObject' import from 'foo' is restricted because only 'AllowedObject' import(s) is/are allowed. Only 'AllowedObject' is allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 25, + endColumn: 41 + }] + }, + { + code: "import { AllowedObject, DisallowedObject } from \"foo\";", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNames: ["AllowedObject"] + }] + }], + errors: [{ + message: "'DisallowedObject' import from 'foo' is restricted because only 'AllowedObject' import(s) is/are allowed.", + type: "ImportDeclaration", + line: 1, + column: 25, + endColumn: 41 + }] + }, + { + code: "import { AllowedObject, DisallowedObject } from \"foo\";", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNames: ["AllowedObject"], + message: "Only 'AllowedObject' is allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "'DisallowedObject' import from 'foo' is restricted because only 'AllowedObject' import(s) is/are allowed. Only 'AllowedObject' is allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 25, + endColumn: 41 + }] + }, + { + code: "import * as AllowedObject from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"] + }] + }], + errors: [{ + message: "* import is invalid because only 'AllowedObject' from 'foo' is/are allowed.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Only 'AllowedObject' is allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "* import is invalid because only 'AllowedObject' from 'foo' is/are allowed. Only 'AllowedObject' is allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo/bar\";", + options: [{ + patterns: [{ + group: ["foo/*"], + allowImportNames: ["AllowedObject"] + }] + }], + errors: [{ + message: "* import is invalid because only 'AllowedObject' from 'foo/bar' is/are allowed.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo/bar\";", + options: [{ + patterns: [{ + group: ["foo/*"], + allowImportNames: ["AllowedObject"], + message: "Only 'AllowedObject' is allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "* import is invalid because only 'AllowedObject' from 'foo/bar' is/are allowed. Only 'AllowedObject' is allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo/bar\";", + options: [{ + patterns: [{ + group: ["foo/*"], + allowImportNamePattern: "^Allow" + }] + }], + errors: [{ + message: "* import is invalid because only imports that match the pattern '/^Allow/u' from 'foo/bar' are allowed.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo/bar\";", + options: [{ + patterns: [{ + group: ["foo/*"], + allowImportNamePattern: "^Allow", + message: "Only import names starting with 'Allow' are allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "* import is invalid because only imports that match the pattern '/^Allow/u' from 'foo/bar' are allowed. Only import names starting with 'Allow' are allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] } ] });