Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): add new extended rule
no-restricted-imports
(#…
- Loading branch information
1 parent
955d7a6
commit ec5d506
Showing
9 changed files
with
857 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
packages/eslint-plugin/docs/rules/no-restricted-imports.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
packages/eslint-plugin/src/rules/no-restricted-imports.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; | ||
}, | ||
}); |
Oops, something went wrong.