Skip to content

Commit

Permalink
feat(eslint-plugin): add new extended rule no-restricted-imports (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sosukesuzuki committed Sep 20, 2021
1 parent 955d7a6 commit ec5d506
Show file tree
Hide file tree
Showing 9 changed files with 857 additions and 2 deletions.
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,
};
},
});

0 comments on commit ec5d506

Please sign in to comment.