Skip to content

Commit

Permalink
feat: add importNames support for patterns in no-restricted-imports (#…
Browse files Browse the repository at this point in the history
…16059)

* feat: add importNames support for restricted import patterns

Fixes #14274

* refactor: Report * import as error, add new messageIds, update docs

* docs: add code examples for patterns with importNames

* refactor: handle star import case separately w/ new messageIds

* refactor: add message assertions + additional test cases, remove early return to catch default import case

* refactor: require 1 unique importName in config, remove array coalesce
  • Loading branch information
brandongregoryscott committed Jul 2, 2022
1 parent 472c368 commit 7023628
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 11 deletions.
32 changes: 32 additions & 0 deletions docs/src/rules/no-restricted-imports.md
Expand Up @@ -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 <https://github.com/nodejs/node/tree/master/lib>):

```json
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
87 changes: 76 additions & 11 deletions lib/rules/no-restricted-imports.js
Expand Up @@ -58,6 +58,14 @@ const arrayOfStringsOrObjectPatterns = {
items: {
type: "object",
properties: {
importNames: {
type: "array",
items: {
type: "string"
},
minItems: 1,
uniqueItems: true
},
group: {
type: "array",
items: {
Expand Down Expand Up @@ -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}}",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string,Object[]>} 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
}
});
});
});
}

Expand Down Expand Up @@ -304,7 +369,7 @@ module.exports = {
checkRestrictedPathAndReport(importSource, importNames, node);
restrictedPatternGroups.forEach(group => {
if (isRestrictedPattern(importSource, group)) {
reportPathForPatterns(node, group);
reportPathForPatterns(node, group, importNames);
}
});
}
Expand Down
141 changes: 141 additions & 0 deletions tests/lib/rules/no-restricted-imports.js
Expand Up @@ -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: [{
Expand Down Expand Up @@ -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."
}]
}
]
});

0 comments on commit 7023628

Please sign in to comment.