Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add new extended rule no-restricted-imports #3775

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -216,6 +216,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
| [`@typescript-eslint/no-loss-of-precision`](./docs/rules/no-loss-of-precision.md) | Disallow literal numbers that lose precision | | | |
| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | |
| [`@typescript-eslint/no-redeclare`](./docs/rules/no-redeclare.md) | Disallow variable redeclaration | | | |
| [`@typescript-eslint/no-restricted-imports`](./docs/rules/no-restricted-imports.md) | Disallow specified modules when loaded by `import` | | | |
| [`@typescript-eslint/no-shadow`](./docs/rules/no-shadow.md) | Disallow variable declarations from shadowing variables declared in the outer scope | | | |
| [`@typescript-eslint/no-throw-literal`](./docs/rules/no-throw-literal.md) | Disallow throwing literals as exceptions | | | :thought_balloon: |
| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | |
Expand Down
64 changes: 64 additions & 0 deletions packages/eslint-plugin/docs/rules/no-restricted-imports.md
@@ -0,0 +1,64 @@
# Disallow specified modules when loaded by `import` (`no-restricted-imports`)

## Rule Details

This rule extends the base [`eslint/no-restricted-imports`](https://eslint.org/docs/rules/no-restricted-imports) rule.

## How to use

```jsonc
{
// note you must disable the base rule as it can report incorrect errors
"no-restricted-imports": "off",
"@typescript-eslint/no-restricted-imports": "off"
}
```

## Options

See [`eslint/no-restricted-imports` options](https://eslint.org/docs/rules/no-restricted-imports#options).
This rule adds the following options:

### `allowTypeImports`

(default: `false`)

You can specify this option for a specific path or pattern as follows:

```jsonc
"@typescript-eslint/no-restricted-imports": ["error", {
"paths": [{
"name": "import-foo",
"message": "Please use import-bar instead.",
"allowTypeImports": true
}, {
"name": "import-baz",
"message": "Please use import-quux instead.",
"allowTypeImports": true
}]
}]
```

When set to `true`, the rule will allow [Type-Only Imports](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export).

Examples of **correct** code with the above config:

```ts
import { foo } from 'other-module';

import type foo from 'import-foo';
export type { Foo } from 'import-foo';

import type baz from 'import-baz';
export type { Baz } from 'import-baz';
```

Example of **incorrect** code with the above config:

```ts
import foo from 'import-foo';
export { Foo } from 'import-foo';

import baz from 'import-baz';
export { Baz } from 'import-baz';
```
3 changes: 2 additions & 1 deletion packages/eslint-plugin/package.json
Expand Up @@ -50,7 +50,8 @@
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.1.0",
"semver": "^7.3.5",
"tsutils": "^3.21.0"
"tsutils": "^3.21.0",
"ignore": "^5.1.8"
},
"devDependencies": {
"@types/debug": "*",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -89,6 +89,8 @@ export = {
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-require-imports': 'error',
'no-restricted-imports': 'off',
'@typescript-eslint/no-restricted-imports': 'error',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-this-alias': 'error',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -60,6 +60,7 @@ import noNonNullAssertion from './no-non-null-assertion';
import noParameterProperties from './no-parameter-properties';
import noRedeclare from './no-redeclare';
import noRequireImports from './no-require-imports';
import noRestrictedImports from './no-restricted-imports';
import noShadow from './no-shadow';
import noThisAlias from './no-this-alias';
import noThrowLiteral from './no-throw-literal';
Expand Down Expand Up @@ -182,6 +183,7 @@ export default {
'no-parameter-properties': noParameterProperties,
'no-redeclare': noRedeclare,
'no-require-imports': noRequireImports,
'no-restricted-imports': noRestrictedImports,
'no-shadow': noShadow,
'no-this-alias': noThisAlias,
'no-throw-literal': noThrowLiteral,
Expand Down
191 changes: 191 additions & 0 deletions packages/eslint-plugin/src/rules/no-restricted-imports.ts
@@ -0,0 +1,191 @@
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
import baseRule, {
ArrayOfStringOrObjectPatterns,
ArrayOfStringOrObject,
} from 'eslint/lib/rules/no-restricted-imports';
import ignore, { Ignore } from 'ignore';
import {
InferOptionsTypeFromRule,
InferMessageIdsTypeFromRule,
createRule,
deepMerge,
} from '../util';

export type Options = InferOptionsTypeFromRule<typeof baseRule>;
export type MessageIds = InferMessageIdsTypeFromRule<typeof baseRule>;

const allowTypeImportsOptionSchema = {
allowTypeImports: {
type: 'boolean',
default: false,
},
};
const schemaForMergeArrayOfStringsOrObjects = {
items: {
anyOf: [
{},
{
properties: allowTypeImportsOptionSchema,
},
],
},
};
const schemaForMergeArrayOfStringsOrObjectPatterns = {
anyOf: [
{},
{
items: {
properties: allowTypeImportsOptionSchema,
},
},
],
};
const schema = deepMerge(
{ ...baseRule.meta.schema },
{
anyOf: [
schemaForMergeArrayOfStringsOrObjects,
{
items: {
properties: {
paths: schemaForMergeArrayOfStringsOrObjects,
patterns: schemaForMergeArrayOfStringsOrObjectPatterns,
},
},
},
],
},
);

function isObjectOfPaths(
obj: unknown,
): obj is { paths: ArrayOfStringOrObject } {
return Object.prototype.hasOwnProperty.call(obj, 'paths');
}

function isObjectOfPatterns(
obj: unknown,
): obj is { patterns: ArrayOfStringOrObjectPatterns } {
return Object.prototype.hasOwnProperty.call(obj, 'patterns');
}

function isOptionsArrayOfStringOrObject(
options: Options,
): options is ArrayOfStringOrObject {
if (isObjectOfPaths(options[0])) {
return false;
}
if (isObjectOfPatterns(options[0])) {
return false;
}
return true;
}

function getRestrictedPaths(options: Options): ArrayOfStringOrObject {
if (isOptionsArrayOfStringOrObject(options)) {
return options;
}
if (isObjectOfPaths(options[0])) {
return options[0].paths;
}
return [];
}

function getRestrictedPatterns(
options: Options,
): ArrayOfStringOrObjectPatterns {
if (isObjectOfPatterns(options[0])) {
return options[0].patterns;
}
return [];
}

export default createRule<Options, MessageIds>({
name: 'no-restricted-imports',
meta: {
type: 'suggestion',
docs: {
description: 'Disallow specified modules when loaded by `import`',
category: 'Best Practices',
recommended: false,
extendsBaseRule: true,
},
messages: baseRule.meta.messages,
fixable: baseRule.meta.fixable,
schema,
},
defaultOptions: [],
create(context) {
const rules = baseRule.create(context);
const { options } = context;

const restrictedPaths = getRestrictedPaths(options);
const allowedTypeImportPathNameSet: Set<string> = new Set();
for (const restrictedPath of restrictedPaths) {
if (
typeof restrictedPath === 'object' &&
restrictedPath.allowTypeImports
) {
allowedTypeImportPathNameSet.add(restrictedPath.name);
}
}
function isAllowedTypeImportPath(importSource: string): boolean {
return allowedTypeImportPathNameSet.has(importSource);
}

const restrictedPatterns = getRestrictedPatterns(options);
const allowedImportTypeMatchers: Ignore[] = [];
for (const restrictedPattern of restrictedPatterns) {
if (
typeof restrictedPattern === 'object' &&
restrictedPattern.allowTypeImports
) {
allowedImportTypeMatchers.push(ignore().add(restrictedPattern.group));
}
}
function isAllowedTypeImportPattern(importSource: string): boolean {
return allowedImportTypeMatchers.every(matcher => {
return matcher.ignores(importSource);
});
}

return {
ImportDeclaration(node): void {
if (typeof node.source.value !== 'string') {
return;
}
if (node.importKind === 'type') {
const importSource = node.source.value.trim();
if (
!isAllowedTypeImportPath(importSource) &&
!isAllowedTypeImportPattern(importSource)
) {
return rules.ImportDeclaration(node);
}
} else {
return rules.ImportDeclaration(node);
}
},
ExportNamedDeclaration(node): void {
if (
node.source?.type !== AST_NODE_TYPES.Literal ||
typeof node.source.value !== 'string'
) {
return;
}
if (node.exportKind === 'type') {
const importSource = node.source.value.trim();
if (
!isAllowedTypeImportPath(importSource) &&
!isAllowedTypeImportPattern(importSource)
) {
return rules.ExportNamedDeclaration(node);
}
} else {
return rules.ExportNamedDeclaration(node);
}
},
ExportAllDeclaration: rules.ExportAllDeclaration,
};
},
});