Skip to content

Commit

Permalink
Update: add importNames option to no-restricted-imports (#9506)
Browse files Browse the repository at this point in the history
  • Loading branch information
brgibson authored and not-an-aardvark committed Nov 14, 2017
1 parent 332c214 commit 0cf081e
Show file tree
Hide file tree
Showing 3 changed files with 498 additions and 30 deletions.
58 changes: 58 additions & 0 deletions docs/rules/no-restricted-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ or like this:
}]
```

or like this if you need to restrict only certain imports from a module:

```json
"no-restricted-imports": ["error", {
"paths": [{
"name": "import-foo",
"importNames": ["Bar"],
"message": "Please use Bar from /import-bar/baz/ instead."
}]
}]
```

The custom message will be appended to the default error message. Please note that you may not specify custom error messages for restricted patterns as a particular import may match more than one pattern.

To restrict the use of all Node.js core imports (via https://github.com/nodejs/node/tree/master/lib):
Expand Down Expand Up @@ -87,6 +99,36 @@ import cluster from 'cluster';
import pick from 'lodash/pick';
```

```js
/*eslint no-restricted-imports: ["error", { paths: [{
name: "foo",
importNames: ["default"],
message: "Please use the default import from '/bar/baz/' instead."
}]}]*/

import DisallowedObject from "foo";
```

```js
/*eslint no-restricted-imports: ["error", { paths: [{
name: "foo",
importNames: ["DisallowedObject"],
message: "Please import 'DisallowedObject' from '/bar/baz/' instead."
}]}]*/

import { DisallowedObject as AllowedObject } from "foo";
```

```js
/*eslint no-restricted-imports: ["error", { paths: [{
name: "foo",
importNames: ["DisallowedObject"],
message: "Please import 'DisallowedObject' from '/bar/baz/' instead."
}]}]*/

import * as Foo from "foo";
```

Examples of **correct** code for this rule:

```js
Expand All @@ -102,6 +144,22 @@ import crypto from 'crypto';
import eslint from 'eslint';
```

```js
/*eslint no-restricted-imports: ["error", { paths: [{ name: "foo", importNames: ["DisallowedObject"] }] }]*/

import DisallowedObject from "foo"
```

```js
/*eslint no-restricted-imports: ["error", { paths: [{
name: "foo",
importNames: ["DisallowedObject"],
message: "Please import 'DisallowedObject' from '/bar/baz/' instead."
}]}]*/

import { AllowedObject as DisallowedObject } from "foo";
```

## 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.
165 changes: 137 additions & 28 deletions lib/rules/no-restricted-imports.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
// Helpers
//------------------------------------------------------------------------------

const DEFAULT_MESSAGE_TEMPLATE = "'{{importName}}' import is restricted from being used.";
const CUSTOM_MESSAGE_TEMPLATE = "'{{importName}}' import is restricted from being used. {{customMessage}}";
const DEFAULT_MESSAGE_TEMPLATE = "'{{importSource}}' import is restricted from being used.";
const CUSTOM_MESSAGE_TEMPLATE = "'{{importSource}}' import is restricted from being used. {{customMessage}}";

//------------------------------------------------------------------------------
// Rule Definition
Expand All @@ -35,6 +35,12 @@ const arrayOfStringsOrObjects = {
message: {
type: "string",
minLength: 1
},
importNames: {
type: "array",
items: {
type: "string"
}
}
},
additionalProperties: false,
Expand Down Expand Up @@ -81,11 +87,14 @@ module.exports = {
const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];

const restrictedPathMessages = restrictedPaths.reduce((memo, importName) => {
if (typeof importName === "string") {
memo[importName] = null;
const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
if (typeof importSource === "string") {
memo[importSource] = { message: null };
} else {
memo[importName.name] = importName.message;
memo[importSource.name] = {
message: importSource.message,
importNames: importSource.importNames
};
}
return memo;
}, {});
Expand All @@ -95,7 +104,16 @@ module.exports = {
return {};
}

const ig = ignore().add(restrictedPatterns);
const restrictedPatternsMatcher = ignore().add(restrictedPatterns);

/**
* Checks to see if "*" is being used to import everything.
* @param {Set.<string>} importNames - Set of import names that are being imported
* @returns {boolean} whether everything is imported or not
*/
function isEverythingImported(importNames) {
return importNames.has("*");
}

/**
* Report a restricted path.
Expand All @@ -104,8 +122,8 @@ module.exports = {
* @private
*/
function reportPath(node) {
const importName = node.source.value.trim();
const customMessage = restrictedPathMessages[importName];
const importSource = node.source.value.trim();
const customMessage = restrictedPathMessages[importSource] && restrictedPathMessages[importSource].message;
const message = customMessage
? CUSTOM_MESSAGE_TEMPLATE
: DEFAULT_MESSAGE_TEMPLATE;
Expand All @@ -114,39 +132,130 @@ module.exports = {
node,
message,
data: {
importName,
importSource,
customMessage
}
});
}

/**
* Check if the given name is a restricted path name.
* @param {string} name name of a variable
* Report a restricted path specifically for patterns.
* @param {node} node - representing the restricted path reference
* @returns {void}
* @private
*/
function reportPathForPatterns(node) {
const importSource = node.source.value.trim();

context.report({
node,
message: "'{{importSource}}' import is restricted from being used by a pattern.",
data: {
importSource
}
});
}

/**
* Report a restricted path specifically when using the '*' import.
* @param {string} importSource - path of the import
* @param {node} node - representing the restricted path reference
* @returns {void}
* @private
*/
function reportPathForEverythingImported(importSource, node) {
const importNames = restrictedPathMessages[importSource].importNames;

context.report({
node,
message: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
data: {
importSource,
importNames
}
});
}

/**
* Check if the given importSource is restricted because '*' is being imported.
* @param {string} importSource - path of the import
* @param {Set.<string>} importNames - Set of import names that are being imported
* @returns {boolean} whether the path is restricted
* @private
*/
function isRestrictedForEverythingImported(importSource, importNames) {
return Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource) &&
restrictedPathMessages[importSource].importNames &&
isEverythingImported(importNames);
}

/**
* Check if the given importNames are restricted given a list of restrictedImportNames.
* @param {Set.<string>} importNames - Set of import names that are being imported
* @param {[string]} restrictedImportNames - array of import names that are restricted for this import
* @returns {boolean} whether the objectName is restricted
* @private
*/
function isRestrictedObject(importNames, restrictedImportNames) {
return restrictedImportNames.some(restrictedObjectName => (
importNames.has(restrictedObjectName)
));
}

/**
* Check if the given importSource is a restricted path.
* @param {string} importSource - path of the import
* @param {Set.<string>} importNames - Set of import names that are being imported
* @returns {boolean} whether the variable is a restricted path or not
* @private
*/
function isRestrictedPath(name) {
return Object.prototype.hasOwnProperty.call(restrictedPathMessages, name);
function isRestrictedPath(importSource, importNames) {
let isRestricted = false;

if (Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
if (restrictedPathMessages[importSource].importNames) {
isRestricted = isRestrictedObject(importNames, restrictedPathMessages[importSource].importNames);
} else {
isRestricted = true;
}
}

return isRestricted;
}

/**
* Check if the given importSource is restricted by a pattern.
* @param {string} importSource - path of the import
* @returns {boolean} whether the variable is a restricted pattern or not
* @private
*/
function isRestrictedPattern(importSource) {
return restrictedPatterns.length > 0 && restrictedPatternsMatcher.ignores(importSource);
}

return {
ImportDeclaration(node) {
if (node && node.source && node.source.value) {
const importName = node.source.value.trim();

if (isRestrictedPath(importName)) {
reportPath(node);
}
if (restrictedPatterns.length > 0 && ig.ignores(importName)) {
context.report({
node,
message: "'{{importName}}' import is restricted from being used by a pattern.",
data: {
importName
}
});
const importSource = node.source.value.trim();
const importNames = node.specifiers.reduce((set, specifier) => {
if (specifier.type === "ImportDefaultSpecifier") {
set.add("default");
} else if (specifier.type === "ImportNamespaceSpecifier") {
set.add("*");
} else {
set.add(specifier.imported.name);
}
return set;
}, new Set());

if (isRestrictedForEverythingImported(importSource, importNames)) {
reportPathForEverythingImported(importSource, node);
}

if (isRestrictedPath(importSource, importNames)) {
reportPath(node);
}
if (isRestrictedPattern(importSource)) {
reportPathForPatterns(node);
}
}
};
Expand Down

0 comments on commit 0cf081e

Please sign in to comment.