📄 consistent-type-imports¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 11 |
| 📦 Imports | 14 |
| 📐 Interfaces | 2 |
| 📑 Type Aliases | 4 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/consistent-type-imports.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'consistent-type-imports' |
meta.type |
'suggestion' |
meta.docs.description |
'Enforce consistent usage of type imports' |
meta.fixable |
'code' |
meta.messages.avoidImportType |
'Use an import instead of an import type.' |
meta.messages.noImportTypeAnnotations |
'import() type annotations are forbidden.' |
meta.messages.someImportsAreOnlyTypes |
'Imports {{typeImports}} are only used as type.' |
meta.messages.typeOverValue |
'All imports in the declaration are only used as types. Use import type.' |
meta.schema |
[ { type: 'object', additionalProperties: false, properties: { disallowTypeAnnotations: { type: 'boolean', descriptio... |
defaultOptions |
[ { disallowTypeAnnotations: true, fixStyle: 'separate-type-imports', prefer: 'type-imports', }, ] |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
TSESLint |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
RuleListener |
@typescript-eslint/utils/eslint-utils |
AST_NODE_TYPES |
@typescript-eslint/utils |
createRule |
../util |
formatWordList |
../util |
getParserServices |
../util |
isClosingBraceToken |
../util |
isCommaToken |
../util |
isImportKeyword |
../util |
isOpeningBraceToken |
../util |
isTypeKeyword |
../util |
nullThrows |
../util |
NullThrowsReasons |
../util |
Functions¶
create(context: any, [option]: any): any¶
Parameters:
contextany[option]any
Returns: any
Calls:
context.reportfixRemoveTypeSpecifierFromImportDeclarationfixRemoveTypeSpecifierFromImportSpecifiergetParserServices (from ../util)node.specifiers.everynode.specifiers.someinlineTypeSpecifiers.pushcontext.sourceCode.getDeclaredVariablesunusedSpecifiers.pushvariable.references.everytypeSpecifiers.pushvalueSpecifiers.pushsourceImports.reportValueImports.pushObject.valuesfixToTypeImportDeclarationreport.typeSpecifiers.mapcomplex_call_13904formatWordList (from ../util)node.specifiers.findnode.specifiers.filternullThrows (from ../util)context.sourceCode.getTokenBeforeNullThrowsReasons.MissingTokencontext.sourceCode.getFirstTokenBetweenremoveTypeNamedSpecifiers.pushfixer.removeRangetypeNamedSpecifiersTexts.pushcontext.sourceCode.text.slicesubsetNamedSpecifiers.includesgroup.pushnamedSpecifierGroups.pushgetNamedSpecifierRangestypeNamedSpecifiersTexts.joinisCommaToken (from ../util)context.sourceCode.getTokenAftercontext.sourceCode.getFirstTokenisOpeningBraceToken (from ../util)fixer.insertTextBeforefixer.replaceTextRangeclassifySpecifiernamedSpecifiers.filterreport.typeSpecifiers.includesfixInsertTypeKeywordInNamedSpecifierListfixInsertTypeSpecifierForImportDeclarationfixInlineTypeImportDeclarationnamedSpecifiers.somenamedSpecifiers.everygetFixesNamedSpecifiersfixInsertNamedSpecifiersInNamedSpecifierListafterFixes.pushtypeNamedSpecifiers .map(spec => { const insertText = context.sourceCode.text.slice( ...spec.range, ); returntype ${insertText}; }) .joincontext.sourceCode.getTextfixesRemoveTypeNamespaceSpecifier.pushfixer.insertTextAftercontext.sourceCode.text .slice(defaultSpecifier.range[0], commaToken.range[0]) .trim
Internal Comments:
// prefer type imports (x2)
// sourceImports is the object containing all the specifics for a particular import source, type or value (x4)
// definitely import type { TypeX } (x4)
/**
* keep origin import kind when export
* export { Type }
* export default Type;
* export = Type;
*/
// CASE 1:
// `type T = typeof foo` will create a value reference because "foo" must be a value type
// however this value reference is safe to use with type-only imports
// TSTypeQuery must have a TSESTree.EntityName as its child, so we can filter here and break early
// END CASE 1
//////////////
// CASE 2:
// `type T = { [foo]: string }` will create a value reference because "foo" must be a value type
// however this value reference is safe to use with type-only imports.
// Also this is represented as a non-type AST - hence it uses MemberExpression
// END CASE 2
// Experimental decorator metadata is bowl of poop that cannot be
// supported based on pure syntactic analysis.
// (x7)
// So we can do one of two things:
// 1) add type-information to the rule in a breaking change and
// prevent users from using it so that we can fully support this
// case.
// 2) make the rule ignore all imports that are used in a file that
// might have decorator metadata.
// (1) is has huge impact and prevents the rule from being used by 99%
// of users Frankly - it's a straight-up bad option. So instead we
// choose with option (2) and just avoid reporting on any imports in a
// file with both emitDecoratorMetadata AND decorators
// For more context see the discussion in this issue and its linked
// issues:
// https://github.com/typescript-eslint/typescript-eslint/issues/5468
// NOTE - in TS 5.0 `experimentalDecorators` became the legacy option,
// replaced with un-flagged, stable decorators and thus the type-aware
// emitDecoratorMetadata implementation also became legacy. in TS 5.2
// support for the new, stable decorator metadata proposal was added -
// however this proposal does not include type information
// PHEW. So TL;DR what does all this mean?
// - if you use experimentalDecorators:true,
// emitDecoratorMetadata:true, and have a decorator in the file -
// the rule will do nothing in the file out of an abundance of
// caution.
// - else the rule will work as normal.
// nothing to fix. value specifiers and type specifiers are correctly written
/**
* checks if import has type assertions
* @example
* ```ts
* import * as type from 'mod' assert \{ type: 'json' \};
* ```
* https://github.com/typescript-eslint/typescript-eslint/issues/7527
*/
// we have a mixed type/value import or just value imports, so we need to split them out into multiple imports if separate-type-imports is configured (x2)
// take all the typeSpecifiers and put them on a new line (x2)
/**
* Returns information for fixing named specifiers, type or value
*/
// import Foo, {Type1, Type2} from 'foo' (x2)
// import DefType, {Type1, Type2} from 'foo' (x2)
// import DefType, {...} from 'foo' (x4)
// ^^^^^^^ remove (x4)
/**
* Returns ranges for fixing named specifier.
*/
/**
* insert specifiers to named import node.
* e.g.
* import type { Already, Type1, Type2 } from 'foo'
* ^^^^^^^^^^^^^ insert
*/
/**
* insert type keyword to named import node.
* e.g.
* import ADefault, { Already, type Type1, type Type2 } from 'foo'
* ^^^^ insert
*/
// For a value import, will only add an inline type to named specifiers (x2)
// add import named type specifiers to its value import (x2)
// import ValueA, { type A } (x2)
// ^^^^ insert (x2)
// import * as types from 'foo'
// checks for presence of import assertions
// import Type from 'foo' (x2)
// if there is a default specifier but it isn't a type specifier, then just add the inline type modifier to the named specifiers (x2)
// import AValue, {BValue, Type1, Type2} from 'foo' (x2)
// import {AValue, Type1, Type2} from 'foo' (x2)
// import {Type1, Type2} from 'foo' (x2)
// The import is both default and named. Insert named on new line because can't mix default type import and named type imports
// eslint-disable-next-line no-lonely-if
// import Foo, * as Type from 'foo' (x2)
// import DefType, * as Type from 'foo' (x4)
// import Def, * as Ns from 'foo' (x4)
// ^^^^^^^^^ remove (x4)
// import type * as Ns from 'foo' (x2)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ insert (x2)
// import type Type from 'foo' (x2)
// ^^^^ insert (x2)
// import Type , {...} from 'foo' (x4)
// ^^^^^ pick (x2)
// ^^^^^^^ remove (x2)
// import type Foo from 'foo' (x4)
// ^^^^^ insert (x2)
// Has default import (x2)
// Only braces. e.g. import Foo, {} from 'foo' (x2)
// import type Foo, {} from 'foo' (x2)
// ^^ remove (x2)
// make sure we don't do anything like `import type {type T} from 'foo';`
// ^^^^ remove (x2)
// import { type Foo } from 'foo' (x2)
// ^^^^ remove (x2)
Code
create(context, [option]) {
const prefer = option.prefer ?? 'type-imports';
const disallowTypeAnnotations = option.disallowTypeAnnotations !== false;
const selectors: RuleListener = {};
if (disallowTypeAnnotations) {
selectors.TSImportType = (node): void => {
context.report({
node,
messageId: 'noImportTypeAnnotations',
});
};
}
if (prefer === 'no-type-imports') {
return {
...selectors,
'ImportDeclaration[importKind = "type"]'(
node: TSESTree.ImportDeclaration,
): void {
context.report({
node,
messageId: 'avoidImportType',
fix(fixer) {
return fixRemoveTypeSpecifierFromImportDeclaration(fixer, node);
},
});
},
'ImportSpecifier[importKind = "type"]'(
node: TSESTree.ImportSpecifier,
): void {
context.report({
node,
messageId: 'avoidImportType',
fix(fixer) {
return fixRemoveTypeSpecifierFromImportSpecifier(fixer, node);
},
});
},
};
}
// prefer type imports
const fixStyle = option.fixStyle ?? 'separate-type-imports';
let hasDecoratorMetadata = false;
const sourceImportsMap: Record<string, SourceImports> = {};
const emitDecoratorMetadata =
getParserServices(context, true).emitDecoratorMetadata ?? false;
const experimentalDecorators =
getParserServices(context, true).experimentalDecorators ?? false;
if (experimentalDecorators && emitDecoratorMetadata) {
selectors.Decorator = (): void => {
hasDecoratorMetadata = true;
};
}
return {
...selectors,
ImportDeclaration(node): void {
const source = node.source.value;
// sourceImports is the object containing all the specifics for a particular import source, type or value
sourceImportsMap[source] ??= {
reportValueImports: [], // if there is a mismatch where type importKind but value specifiers
source,
typeOnlyNamedImport: null, // if only type imports
valueImport: null, // if only value imports
valueOnlyNamedImport: null, // if only value imports with named specifiers
};
const sourceImports = sourceImportsMap[source];
if (node.importKind === 'type') {
if (
!sourceImports.typeOnlyNamedImport &&
node.specifiers.every(
specifier => specifier.type === AST_NODE_TYPES.ImportSpecifier,
)
) {
// definitely import type { TypeX }
sourceImports.typeOnlyNamedImport = node;
}
} else if (
!sourceImports.valueOnlyNamedImport &&
node.specifiers.length &&
node.specifiers.every(
specifier => specifier.type === AST_NODE_TYPES.ImportSpecifier,
)
) {
sourceImports.valueOnlyNamedImport = node;
sourceImports.valueImport = node;
} else if (
!sourceImports.valueImport &&
node.specifiers.some(
specifier =>
specifier.type === AST_NODE_TYPES.ImportDefaultSpecifier,
)
) {
sourceImports.valueImport = node;
}
const typeSpecifiers: TSESTree.ImportClause[] = [];
const inlineTypeSpecifiers: TSESTree.ImportSpecifier[] = [];
const valueSpecifiers: TSESTree.ImportClause[] = [];
const unusedSpecifiers: TSESTree.ImportClause[] = [];
for (const specifier of node.specifiers) {
if (
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
specifier.importKind === 'type'
) {
inlineTypeSpecifiers.push(specifier);
continue;
}
const [variable] = context.sourceCode.getDeclaredVariables(specifier);
if (variable.references.length === 0) {
unusedSpecifiers.push(specifier);
} else {
const onlyHasTypeReferences = variable.references.every(ref => {
/**
* keep origin import kind when export
* export { Type }
* export default Type;
* export = Type;
*/
if (
(ref.identifier.parent.type ===
AST_NODE_TYPES.ExportSpecifier ||
ref.identifier.parent.type ===
AST_NODE_TYPES.ExportDefaultDeclaration ||
ref.identifier.parent.type ===
AST_NODE_TYPES.TSExportAssignment) &&
ref.isValueReference &&
ref.isTypeReference
) {
return node.importKind === 'type';
}
if (ref.isValueReference) {
let parent = ref.identifier.parent as TSESTree.Node | undefined;
let child: TSESTree.Node = ref.identifier;
while (parent) {
switch (parent.type) {
// CASE 1:
// `type T = typeof foo` will create a value reference because "foo" must be a value type
// however this value reference is safe to use with type-only imports
case AST_NODE_TYPES.TSTypeQuery:
return true;
case AST_NODE_TYPES.TSQualifiedName:
// TSTypeQuery must have a TSESTree.EntityName as its child, so we can filter here and break early
if (parent.left !== child) {
return false;
}
child = parent;
parent = parent.parent;
continue;
// END CASE 1
//////////////
// CASE 2:
// `type T = { [foo]: string }` will create a value reference because "foo" must be a value type
// however this value reference is safe to use with type-only imports.
// Also this is represented as a non-type AST - hence it uses MemberExpression
case AST_NODE_TYPES.TSPropertySignature:
return parent.key === child;
case AST_NODE_TYPES.MemberExpression:
if (parent.object !== child) {
return false;
}
child = parent;
parent = parent.parent;
continue;
// END CASE 2
default:
return false;
}
}
}
return ref.isTypeReference;
});
if (onlyHasTypeReferences) {
typeSpecifiers.push(specifier);
} else {
valueSpecifiers.push(specifier);
}
}
}
if (node.importKind === 'value' && typeSpecifiers.length) {
sourceImports.reportValueImports.push({
node,
inlineTypeSpecifiers,
typeSpecifiers,
unusedSpecifiers,
valueSpecifiers,
});
}
},
'Program:exit'(): void {
if (hasDecoratorMetadata) {
// Experimental decorator metadata is bowl of poop that cannot be
// supported based on pure syntactic analysis.
//
// So we can do one of two things:
// 1) add type-information to the rule in a breaking change and
// prevent users from using it so that we can fully support this
// case.
// 2) make the rule ignore all imports that are used in a file that
// might have decorator metadata.
//
// (1) is has huge impact and prevents the rule from being used by 99%
// of users Frankly - it's a straight-up bad option. So instead we
// choose with option (2) and just avoid reporting on any imports in a
// file with both emitDecoratorMetadata AND decorators
//
// For more context see the discussion in this issue and its linked
// issues:
// https://github.com/typescript-eslint/typescript-eslint/issues/5468
//
//
// NOTE - in TS 5.0 `experimentalDecorators` became the legacy option,
// replaced with un-flagged, stable decorators and thus the type-aware
// emitDecoratorMetadata implementation also became legacy. in TS 5.2
// support for the new, stable decorator metadata proposal was added -
// however this proposal does not include type information
//
//
// PHEW. So TL;DR what does all this mean?
// - if you use experimentalDecorators:true,
// emitDecoratorMetadata:true, and have a decorator in the file -
// the rule will do nothing in the file out of an abundance of
// caution.
// - else the rule will work as normal.
return;
}
for (const sourceImports of Object.values(sourceImportsMap)) {
if (sourceImports.reportValueImports.length === 0) {
// nothing to fix. value specifiers and type specifiers are correctly written
continue;
}
for (const report of sourceImports.reportValueImports) {
if (
report.valueSpecifiers.length === 0 &&
report.unusedSpecifiers.length === 0 &&
report.node.importKind !== 'type'
) {
/**
* checks if import has type assertions
* @example
* ```ts
* import * as type from 'mod' assert \{ type: 'json' \};
* ```
* https://github.com/typescript-eslint/typescript-eslint/issues/7527
*/
if (report.node.attributes.length === 0) {
context.report({
node: report.node,
messageId: 'typeOverValue',
*fix(fixer) {
yield* fixToTypeImportDeclaration(
fixer,
report,
sourceImports,
);
},
});
}
} else {
// we have a mixed type/value import or just value imports, so we need to split them out into multiple imports if separate-type-imports is configured
const importNames = report.typeSpecifiers.map(
specifier => `"${specifier.local.name}"`,
);
const message = ((): {
data: Record<string, unknown>;
messageId: MessageIds;
} => {
const typeImports = formatWordList(importNames);
if (importNames.length === 1) {
return {
messageId: 'someImportsAreOnlyTypes',
data: {
typeImports,
},
};
}
return {
messageId: 'someImportsAreOnlyTypes',
data: {
typeImports,
},
};
})();
context.report({
node: report.node,
...message,
*fix(fixer) {
// take all the typeSpecifiers and put them on a new line
yield* fixToTypeImportDeclaration(
fixer,
report,
sourceImports,
);
},
});
}
}
}
},
};
function classifySpecifier(node: TSESTree.ImportDeclaration): {
defaultSpecifier: TSESTree.ImportDefaultSpecifier | null;
namedSpecifiers: TSESTree.ImportSpecifier[];
namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null;
} {
const defaultSpecifier =
node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier
? node.specifiers[0]
: null;
const namespaceSpecifier =
node.specifiers.find(
(specifier): specifier is TSESTree.ImportNamespaceSpecifier =>
specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
) ?? null;
const namedSpecifiers = node.specifiers.filter(
(specifier): specifier is TSESTree.ImportSpecifier =>
specifier.type === AST_NODE_TYPES.ImportSpecifier,
);
return {
defaultSpecifier,
namedSpecifiers,
namespaceSpecifier,
};
}
/**
* Returns information for fixing named specifiers, type or value
*/
function getFixesNamedSpecifiers(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportDeclaration,
subsetNamedSpecifiers: TSESTree.ImportSpecifier[],
allNamedSpecifiers: TSESTree.ImportSpecifier[],
): {
removeTypeNamedSpecifiers: TSESLint.RuleFix[];
typeNamedSpecifiersText: string;
} {
if (allNamedSpecifiers.length === 0) {
return {
removeTypeNamedSpecifiers: [],
typeNamedSpecifiersText: '',
};
}
const typeNamedSpecifiersTexts: string[] = [];
const removeTypeNamedSpecifiers: TSESLint.RuleFix[] = [];
if (subsetNamedSpecifiers.length === allNamedSpecifiers.length) {
// import Foo, {Type1, Type2} from 'foo'
// import DefType, {Type1, Type2} from 'foo'
const openingBraceToken = nullThrows(
context.sourceCode.getTokenBefore(
subsetNamedSpecifiers[0],
isOpeningBraceToken,
),
NullThrowsReasons.MissingToken('{', node.type),
);
const commaToken = nullThrows(
context.sourceCode.getTokenBefore(openingBraceToken, isCommaToken),
NullThrowsReasons.MissingToken(',', node.type),
);
const closingBraceToken = nullThrows(
context.sourceCode.getFirstTokenBetween(
openingBraceToken,
node.source,
isClosingBraceToken,
),
NullThrowsReasons.MissingToken('}', node.type),
);
// import DefType, {...} from 'foo'
// ^^^^^^^ remove
removeTypeNamedSpecifiers.push(
fixer.removeRange([commaToken.range[0], closingBraceToken.range[1]]),
);
typeNamedSpecifiersTexts.push(
context.sourceCode.text.slice(
openingBraceToken.range[1],
closingBraceToken.range[0],
),
);
} else {
const namedSpecifierGroups: TSESTree.ImportSpecifier[][] = [];
let group: TSESTree.ImportSpecifier[] = [];
for (const namedSpecifier of allNamedSpecifiers) {
if (subsetNamedSpecifiers.includes(namedSpecifier)) {
group.push(namedSpecifier);
} else if (group.length) {
namedSpecifierGroups.push(group);
group = [];
}
}
if (group.length) {
namedSpecifierGroups.push(group);
}
for (const namedSpecifiers of namedSpecifierGroups) {
const { removeRange, textRange } = getNamedSpecifierRanges(
namedSpecifiers,
allNamedSpecifiers,
);
removeTypeNamedSpecifiers.push(fixer.removeRange(removeRange));
typeNamedSpecifiersTexts.push(
context.sourceCode.text.slice(...textRange),
);
}
}
return {
removeTypeNamedSpecifiers,
typeNamedSpecifiersText: typeNamedSpecifiersTexts.join(','),
};
}
/**
* Returns ranges for fixing named specifier.
*/
function getNamedSpecifierRanges(
namedSpecifierGroup: TSESTree.ImportSpecifier[],
allNamedSpecifiers: TSESTree.ImportSpecifier[],
): {
removeRange: TSESTree.Range;
textRange: TSESTree.Range;
} {
const first = namedSpecifierGroup[0];
const last = namedSpecifierGroup[namedSpecifierGroup.length - 1];
const removeRange: TSESTree.Range = [first.range[0], last.range[1]];
const textRange: TSESTree.Range = [...removeRange];
const before = nullThrows(
context.sourceCode.getTokenBefore(first),
NullThrowsReasons.MissingToken('token', 'first specifier'),
);
textRange[0] = before.range[1];
if (isCommaToken(before)) {
removeRange[0] = before.range[0];
} else {
removeRange[0] = before.range[1];
}
const isFirst = allNamedSpecifiers[0] === first;
const isLast = allNamedSpecifiers[allNamedSpecifiers.length - 1] === last;
const after = nullThrows(
context.sourceCode.getTokenAfter(last),
NullThrowsReasons.MissingToken('token', 'last specifier'),
);
textRange[1] = after.range[0];
if ((isFirst || isLast) && isCommaToken(after)) {
removeRange[1] = after.range[1];
}
return {
removeRange,
textRange,
};
}
/**
* insert specifiers to named import node.
* e.g.
* import type { Already, Type1, Type2 } from 'foo'
* ^^^^^^^^^^^^^ insert
*/
function fixInsertNamedSpecifiersInNamedSpecifierList(
fixer: TSESLint.RuleFixer,
target: TSESTree.ImportDeclaration,
insertText: string,
): TSESLint.RuleFix {
const closingBraceToken = nullThrows(
context.sourceCode.getFirstTokenBetween(
nullThrows(
context.sourceCode.getFirstToken(target),
NullThrowsReasons.MissingToken('token before', 'import'),
),
target.source,
isClosingBraceToken,
),
NullThrowsReasons.MissingToken('}', target.type),
);
const before = nullThrows(
context.sourceCode.getTokenBefore(closingBraceToken),
NullThrowsReasons.MissingToken('token before', 'closing brace'),
);
if (!isCommaToken(before) && !isOpeningBraceToken(before)) {
insertText = `,${insertText}`;
}
return fixer.insertTextBefore(closingBraceToken, insertText);
}
/**
* insert type keyword to named import node.
* e.g.
* import ADefault, { Already, type Type1, type Type2 } from 'foo'
* ^^^^ insert
*/
function* fixInsertTypeKeywordInNamedSpecifierList(
fixer: TSESLint.RuleFixer,
typeSpecifiers: TSESTree.ImportSpecifier[],
): IterableIterator<TSESLint.RuleFix> {
for (const spec of typeSpecifiers) {
const insertText = context.sourceCode.text.slice(...spec.range);
yield fixer.replaceTextRange(spec.range, `type ${insertText}`);
}
}
function* fixInlineTypeImportDeclaration(
fixer: TSESLint.RuleFixer,
report: ReportValueImport,
sourceImports: SourceImports,
): IterableIterator<TSESLint.RuleFix> {
const { node } = report;
// For a value import, will only add an inline type to named specifiers
const { namedSpecifiers } = classifySpecifier(node);
const typeNamedSpecifiers = namedSpecifiers.filter(specifier =>
report.typeSpecifiers.includes(specifier),
);
if (sourceImports.valueImport) {
// add import named type specifiers to its value import
// import ValueA, { type A }
// ^^^^ insert
const { namedSpecifiers: valueImportNamedSpecifiers } =
classifySpecifier(sourceImports.valueImport);
if (
sourceImports.valueOnlyNamedImport ||
valueImportNamedSpecifiers.length
) {
yield* fixInsertTypeKeywordInNamedSpecifierList(
fixer,
typeNamedSpecifiers,
);
}
}
}
function* fixToTypeImportDeclaration(
fixer: TSESLint.RuleFixer,
report: ReportValueImport,
sourceImports: SourceImports,
): IterableIterator<TSESLint.RuleFix> {
const { node } = report;
const { defaultSpecifier, namedSpecifiers, namespaceSpecifier } =
classifySpecifier(node);
if (namespaceSpecifier && !defaultSpecifier) {
// import * as types from 'foo'
// checks for presence of import assertions
if (node.attributes.length === 0) {
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false);
}
return;
}
if (defaultSpecifier) {
if (
report.typeSpecifiers.includes(defaultSpecifier) &&
namedSpecifiers.length === 0 &&
!namespaceSpecifier
) {
// import Type from 'foo'
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, true);
return;
}
if (
fixStyle === 'inline-type-imports' &&
!report.typeSpecifiers.includes(defaultSpecifier) &&
namedSpecifiers.length > 0 &&
!namespaceSpecifier
) {
// if there is a default specifier but it isn't a type specifier, then just add the inline type modifier to the named specifiers
// import AValue, {BValue, Type1, Type2} from 'foo'
yield* fixInlineTypeImportDeclaration(fixer, report, sourceImports);
return;
}
} else if (!namespaceSpecifier) {
if (
fixStyle === 'inline-type-imports' &&
namedSpecifiers.some(specifier =>
report.typeSpecifiers.includes(specifier),
)
) {
// import {AValue, Type1, Type2} from 'foo'
yield* fixInlineTypeImportDeclaration(fixer, report, sourceImports);
return;
}
if (
namedSpecifiers.every(specifier =>
report.typeSpecifiers.includes(specifier),
)
) {
// import {Type1, Type2} from 'foo'
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false);
return;
}
}
const typeNamedSpecifiers = namedSpecifiers.filter(specifier =>
report.typeSpecifiers.includes(specifier),
);
const fixesNamedSpecifiers = getFixesNamedSpecifiers(
fixer,
node,
typeNamedSpecifiers,
namedSpecifiers,
);
const afterFixes: TSESLint.RuleFix[] = [];
if (typeNamedSpecifiers.length) {
if (sourceImports.typeOnlyNamedImport) {
const insertTypeNamedSpecifiers =
fixInsertNamedSpecifiersInNamedSpecifierList(
fixer,
sourceImports.typeOnlyNamedImport,
fixesNamedSpecifiers.typeNamedSpecifiersText,
);
if (sourceImports.typeOnlyNamedImport.range[1] <= node.range[0]) {
yield insertTypeNamedSpecifiers;
} else {
afterFixes.push(insertTypeNamedSpecifiers);
}
} else {
// The import is both default and named. Insert named on new line because can't mix default type import and named type imports
// eslint-disable-next-line no-lonely-if
if (fixStyle === 'inline-type-imports') {
yield fixer.insertTextBefore(
node,
`import {${typeNamedSpecifiers
.map(spec => {
const insertText = context.sourceCode.text.slice(
...spec.range,
);
return `type ${insertText}`;
})
.join(
', ',
)}} from ${context.sourceCode.getText(node.source)};\n`,
);
} else {
yield fixer.insertTextBefore(
node,
`import type {${
fixesNamedSpecifiers.typeNamedSpecifiersText
}} from ${context.sourceCode.getText(node.source)};\n`,
);
}
}
}
const fixesRemoveTypeNamespaceSpecifier: TSESLint.RuleFix[] = [];
if (
namespaceSpecifier &&
report.typeSpecifiers.includes(namespaceSpecifier)
) {
// import Foo, * as Type from 'foo'
// import DefType, * as Type from 'foo'
// import DefType, * as Type from 'foo'
const commaToken = nullThrows(
context.sourceCode.getTokenBefore(namespaceSpecifier, isCommaToken),
NullThrowsReasons.MissingToken(',', node.type),
);
// import Def, * as Ns from 'foo'
// ^^^^^^^^^ remove
fixesRemoveTypeNamespaceSpecifier.push(
fixer.removeRange([commaToken.range[0], namespaceSpecifier.range[1]]),
);
// import type * as Ns from 'foo'
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ insert
yield fixer.insertTextBefore(
node,
`import type ${context.sourceCode.getText(
namespaceSpecifier,
)} from ${context.sourceCode.getText(node.source)};\n`,
);
}
if (
defaultSpecifier &&
report.typeSpecifiers.includes(defaultSpecifier)
) {
if (report.typeSpecifiers.length === node.specifiers.length) {
const importToken = nullThrows(
context.sourceCode.getFirstToken(node, isImportKeyword),
NullThrowsReasons.MissingToken('import', node.type),
);
// import type Type from 'foo'
// ^^^^ insert
yield fixer.insertTextAfter(importToken, ' type');
} else {
const commaToken = nullThrows(
context.sourceCode.getTokenAfter(defaultSpecifier, isCommaToken),
NullThrowsReasons.MissingToken(',', defaultSpecifier.type),
);
// import Type , {...} from 'foo'
// ^^^^^ pick
const defaultText = context.sourceCode.text
.slice(defaultSpecifier.range[0], commaToken.range[0])
.trim();
yield fixer.insertTextBefore(
node,
`import type ${defaultText} from ${context.sourceCode.getText(
node.source,
)};\n`,
);
const afterToken = nullThrows(
context.sourceCode.getTokenAfter(commaToken, {
includeComments: true,
}),
NullThrowsReasons.MissingToken('any token', node.type),
);
// import Type , {...} from 'foo'
// ^^^^^^^ remove
yield fixer.removeRange([
defaultSpecifier.range[0],
afterToken.range[0],
]);
}
}
yield* fixesNamedSpecifiers.removeTypeNamedSpecifiers;
yield* fixesRemoveTypeNamespaceSpecifier;
yield* afterFixes;
}
function* fixInsertTypeSpecifierForImportDeclaration(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportDeclaration,
isDefaultImport: boolean,
): IterableIterator<TSESLint.RuleFix> {
// import type Foo from 'foo'
// ^^^^^ insert
const importToken = nullThrows(
context.sourceCode.getFirstToken(node, isImportKeyword),
NullThrowsReasons.MissingToken('import', node.type),
);
yield fixer.insertTextAfter(importToken, ' type');
if (isDefaultImport) {
// Has default import
const openingBraceToken = context.sourceCode.getFirstTokenBetween(
importToken,
node.source,
isOpeningBraceToken,
);
if (openingBraceToken) {
// Only braces. e.g. import Foo, {} from 'foo'
const commaToken = nullThrows(
context.sourceCode.getTokenBefore(openingBraceToken, isCommaToken),
NullThrowsReasons.MissingToken(',', node.type),
);
const closingBraceToken = nullThrows(
context.sourceCode.getFirstTokenBetween(
openingBraceToken,
node.source,
isClosingBraceToken,
),
NullThrowsReasons.MissingToken('}', node.type),
);
// import type Foo, {} from 'foo'
// ^^ remove
yield fixer.removeRange([
commaToken.range[0],
closingBraceToken.range[1],
]);
const specifiersText = context.sourceCode.text.slice(
commaToken.range[1],
closingBraceToken.range[1],
);
if (node.specifiers.length > 1) {
yield fixer.insertTextAfter(
node,
`\nimport type${specifiersText} from ${context.sourceCode.getText(
node.source,
)};`,
);
}
}
}
// make sure we don't do anything like `import type {type T} from 'foo';`
for (const specifier of node.specifiers) {
if (
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
specifier.importKind === 'type'
) {
yield* fixRemoveTypeSpecifierFromImportSpecifier(fixer, specifier);
}
}
}
function* fixRemoveTypeSpecifierFromImportDeclaration(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportDeclaration,
): IterableIterator<TSESLint.RuleFix> {
// import type Foo from 'foo'
// ^^^^ remove
const importToken = nullThrows(
context.sourceCode.getFirstToken(node, isImportKeyword),
NullThrowsReasons.MissingToken('import', node.type),
);
const typeToken = nullThrows(
context.sourceCode.getFirstTokenBetween(
importToken,
node.specifiers[0]?.local ?? node.source,
isTypeKeyword,
),
NullThrowsReasons.MissingToken('type', node.type),
);
const afterToken = nullThrows(
context.sourceCode.getTokenAfter(typeToken, { includeComments: true }),
NullThrowsReasons.MissingToken('any token', node.type),
);
yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]);
}
function* fixRemoveTypeSpecifierFromImportSpecifier(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportSpecifier,
): IterableIterator<TSESLint.RuleFix> {
// import { type Foo } from 'foo'
// ^^^^ remove
const typeToken = nullThrows(
context.sourceCode.getFirstToken(node, isTypeKeyword),
NullThrowsReasons.MissingToken('type', node.type),
);
const afterToken = nullThrows(
context.sourceCode.getTokenAfter(typeToken, { includeComments: true }),
NullThrowsReasons.MissingToken('any token', node.type),
);
yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]);
}
}
Internal helpers¶
Declared inside another function in this file.
classifySpecifier(node: TSESTree.ImportDeclaration): { defaultSpecifier: TSESTree.ImportDefaultSpecifier | null;…¶
Parameters:
nodeTSESTree.ImportDeclaration
Returns: {
defaultSpecifier: TSESTree.ImportDefaultSpecifier | null;
namedSpecifiers: TSESTree.ImportSpecifier[];
namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null;
}
Calls:
node.specifiers.findnode.specifiers.filter
Code
function classifySpecifier(node: TSESTree.ImportDeclaration): {
defaultSpecifier: TSESTree.ImportDefaultSpecifier | null;
namedSpecifiers: TSESTree.ImportSpecifier[];
namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null;
} {
const defaultSpecifier =
node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier
? node.specifiers[0]
: null;
const namespaceSpecifier =
node.specifiers.find(
(specifier): specifier is TSESTree.ImportNamespaceSpecifier =>
specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
) ?? null;
const namedSpecifiers = node.specifiers.filter(
(specifier): specifier is TSESTree.ImportSpecifier =>
specifier.type === AST_NODE_TYPES.ImportSpecifier,
);
return {
defaultSpecifier,
namedSpecifiers,
namespaceSpecifier,
};
}
getFixesNamedSpecifiers(…): { removeTypeNamedSpecifiers: TSESLint.RuleFix[]; typeNamedS…¶
Returns information for fixing named specifiers, type or value
Calls:
nullThrows (from ../util)context.sourceCode.getTokenBeforeNullThrowsReasons.MissingTokencontext.sourceCode.getFirstTokenBetweenremoveTypeNamedSpecifiers.pushfixer.removeRangetypeNamedSpecifiersTexts.pushcontext.sourceCode.text.slicesubsetNamedSpecifiers.includesgroup.pushnamedSpecifierGroups.pushgetNamedSpecifierRangestypeNamedSpecifiersTexts.join
Internal Comments:
// import Foo, {Type1, Type2} from 'foo' (x2)
// import DefType, {Type1, Type2} from 'foo' (x2)
// import DefType, {...} from 'foo' (x4)
// ^^^^^^^ remove (x4)
Code
function getFixesNamedSpecifiers(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportDeclaration,
subsetNamedSpecifiers: TSESTree.ImportSpecifier[],
allNamedSpecifiers: TSESTree.ImportSpecifier[],
): {
removeTypeNamedSpecifiers: TSESLint.RuleFix[];
typeNamedSpecifiersText: string;
} {
if (allNamedSpecifiers.length === 0) {
return {
removeTypeNamedSpecifiers: [],
typeNamedSpecifiersText: '',
};
}
const typeNamedSpecifiersTexts: string[] = [];
const removeTypeNamedSpecifiers: TSESLint.RuleFix[] = [];
if (subsetNamedSpecifiers.length === allNamedSpecifiers.length) {
// import Foo, {Type1, Type2} from 'foo'
// import DefType, {Type1, Type2} from 'foo'
const openingBraceToken = nullThrows(
context.sourceCode.getTokenBefore(
subsetNamedSpecifiers[0],
isOpeningBraceToken,
),
NullThrowsReasons.MissingToken('{', node.type),
);
const commaToken = nullThrows(
context.sourceCode.getTokenBefore(openingBraceToken, isCommaToken),
NullThrowsReasons.MissingToken(',', node.type),
);
const closingBraceToken = nullThrows(
context.sourceCode.getFirstTokenBetween(
openingBraceToken,
node.source,
isClosingBraceToken,
),
NullThrowsReasons.MissingToken('}', node.type),
);
// import DefType, {...} from 'foo'
// ^^^^^^^ remove
removeTypeNamedSpecifiers.push(
fixer.removeRange([commaToken.range[0], closingBraceToken.range[1]]),
);
typeNamedSpecifiersTexts.push(
context.sourceCode.text.slice(
openingBraceToken.range[1],
closingBraceToken.range[0],
),
);
} else {
const namedSpecifierGroups: TSESTree.ImportSpecifier[][] = [];
let group: TSESTree.ImportSpecifier[] = [];
for (const namedSpecifier of allNamedSpecifiers) {
if (subsetNamedSpecifiers.includes(namedSpecifier)) {
group.push(namedSpecifier);
} else if (group.length) {
namedSpecifierGroups.push(group);
group = [];
}
}
if (group.length) {
namedSpecifierGroups.push(group);
}
for (const namedSpecifiers of namedSpecifierGroups) {
const { removeRange, textRange } = getNamedSpecifierRanges(
namedSpecifiers,
allNamedSpecifiers,
);
removeTypeNamedSpecifiers.push(fixer.removeRange(removeRange));
typeNamedSpecifiersTexts.push(
context.sourceCode.text.slice(...textRange),
);
}
}
return {
removeTypeNamedSpecifiers,
typeNamedSpecifiersText: typeNamedSpecifiersTexts.join(','),
};
}
getNamedSpecifierRanges(…): { removeRange: TSESTree.Range; textRange: TSESTree.Range; }¶
Returns ranges for fixing named specifier.
Calls:
nullThrows (from ../util)context.sourceCode.getTokenBeforeNullThrowsReasons.MissingTokenisCommaToken (from ../util)context.sourceCode.getTokenAfter
Code
function getNamedSpecifierRanges(
namedSpecifierGroup: TSESTree.ImportSpecifier[],
allNamedSpecifiers: TSESTree.ImportSpecifier[],
): {
removeRange: TSESTree.Range;
textRange: TSESTree.Range;
} {
const first = namedSpecifierGroup[0];
const last = namedSpecifierGroup[namedSpecifierGroup.length - 1];
const removeRange: TSESTree.Range = [first.range[0], last.range[1]];
const textRange: TSESTree.Range = [...removeRange];
const before = nullThrows(
context.sourceCode.getTokenBefore(first),
NullThrowsReasons.MissingToken('token', 'first specifier'),
);
textRange[0] = before.range[1];
if (isCommaToken(before)) {
removeRange[0] = before.range[0];
} else {
removeRange[0] = before.range[1];
}
const isFirst = allNamedSpecifiers[0] === first;
const isLast = allNamedSpecifiers[allNamedSpecifiers.length - 1] === last;
const after = nullThrows(
context.sourceCode.getTokenAfter(last),
NullThrowsReasons.MissingToken('token', 'last specifier'),
);
textRange[1] = after.range[0];
if ((isFirst || isLast) && isCommaToken(after)) {
removeRange[1] = after.range[1];
}
return {
removeRange,
textRange,
};
}
fixInsertNamedSpecifiersInNamedSpecifierList(fixer: TSESLint.RuleFixer, target: TSESTree.ImportDeclaration, insertText: string): TSESLint.RuleFix¶
insert specifiers to named import node. e.g. import type { Already, Type1, Type2 } from 'foo' ^^^^^^^^^^^^^ insert
Raw JSDoc
Calls:
nullThrows (from ../util)context.sourceCode.getFirstTokenBetweencontext.sourceCode.getFirstTokenNullThrowsReasons.MissingTokencontext.sourceCode.getTokenBeforeisCommaToken (from ../util)isOpeningBraceToken (from ../util)fixer.insertTextBefore
Code
function fixInsertNamedSpecifiersInNamedSpecifierList(
fixer: TSESLint.RuleFixer,
target: TSESTree.ImportDeclaration,
insertText: string,
): TSESLint.RuleFix {
const closingBraceToken = nullThrows(
context.sourceCode.getFirstTokenBetween(
nullThrows(
context.sourceCode.getFirstToken(target),
NullThrowsReasons.MissingToken('token before', 'import'),
),
target.source,
isClosingBraceToken,
),
NullThrowsReasons.MissingToken('}', target.type),
);
const before = nullThrows(
context.sourceCode.getTokenBefore(closingBraceToken),
NullThrowsReasons.MissingToken('token before', 'closing brace'),
);
if (!isCommaToken(before) && !isOpeningBraceToken(before)) {
insertText = `,${insertText}`;
}
return fixer.insertTextBefore(closingBraceToken, insertText);
}
fixInsertTypeKeywordInNamedSpecifierList(fixer: TSESLint.RuleFixer, typeSpecifiers: TSESTree.ImportSpecifier[]): IterableIterator<TSESLint.RuleFix>¶
insert type keyword to named import node. e.g. import ADefault, { Already, type Type1, type Type2 } from 'foo' ^^^^ insert
Raw JSDoc
Calls:
context.sourceCode.text.slicefixer.replaceTextRange
Code
function* fixInsertTypeKeywordInNamedSpecifierList(
fixer: TSESLint.RuleFixer,
typeSpecifiers: TSESTree.ImportSpecifier[],
): IterableIterator<TSESLint.RuleFix> {
for (const spec of typeSpecifiers) {
const insertText = context.sourceCode.text.slice(...spec.range);
yield fixer.replaceTextRange(spec.range, `type ${insertText}`);
}
}
fixInlineTypeImportDeclaration(fixer: TSESLint.RuleFixer, report: ReportValueImport, sourceImports: SourceImports): IterableIterator<TSESLint.RuleFix>¶
Parameters:
fixerTSESLint.RuleFixerreportReportValueImportsourceImportsSourceImports
Returns: IterableIterator<TSESLint.RuleFix>
Calls:
classifySpecifiernamedSpecifiers.filterreport.typeSpecifiers.includesfixInsertTypeKeywordInNamedSpecifierList
Internal Comments:
// For a value import, will only add an inline type to named specifiers (x2)
// add import named type specifiers to its value import (x2)
// import ValueA, { type A } (x2)
// ^^^^ insert (x2)
Code
function* fixInlineTypeImportDeclaration(
fixer: TSESLint.RuleFixer,
report: ReportValueImport,
sourceImports: SourceImports,
): IterableIterator<TSESLint.RuleFix> {
const { node } = report;
// For a value import, will only add an inline type to named specifiers
const { namedSpecifiers } = classifySpecifier(node);
const typeNamedSpecifiers = namedSpecifiers.filter(specifier =>
report.typeSpecifiers.includes(specifier),
);
if (sourceImports.valueImport) {
// add import named type specifiers to its value import
// import ValueA, { type A }
// ^^^^ insert
const { namedSpecifiers: valueImportNamedSpecifiers } =
classifySpecifier(sourceImports.valueImport);
if (
sourceImports.valueOnlyNamedImport ||
valueImportNamedSpecifiers.length
) {
yield* fixInsertTypeKeywordInNamedSpecifierList(
fixer,
typeNamedSpecifiers,
);
}
}
}
fixToTypeImportDeclaration(fixer: TSESLint.RuleFixer, report: ReportValueImport, sourceImports: SourceImports): IterableIterator<TSESLint.RuleFix>¶
Parameters:
fixerTSESLint.RuleFixerreportReportValueImportsourceImportsSourceImports
Returns: IterableIterator<TSESLint.RuleFix>
Calls:
classifySpecifierfixInsertTypeSpecifierForImportDeclarationreport.typeSpecifiers.includesfixInlineTypeImportDeclarationnamedSpecifiers.somenamedSpecifiers.everynamedSpecifiers.filtergetFixesNamedSpecifiersfixInsertNamedSpecifiersInNamedSpecifierListafterFixes.pushfixer.insertTextBeforetypeNamedSpecifiers .map(spec => { const insertText = context.sourceCode.text.slice( ...spec.range, ); returntype ${insertText}; }) .joincontext.sourceCode.getTextnullThrows (from ../util)context.sourceCode.getTokenBeforeNullThrowsReasons.MissingTokenfixesRemoveTypeNamespaceSpecifier.pushfixer.removeRangecontext.sourceCode.getFirstTokenfixer.insertTextAftercontext.sourceCode.getTokenAftercontext.sourceCode.text .slice(defaultSpecifier.range[0], commaToken.range[0]) .trim
Internal Comments:
// import * as types from 'foo'
// checks for presence of import assertions
// import Type from 'foo' (x2)
// if there is a default specifier but it isn't a type specifier, then just add the inline type modifier to the named specifiers (x2)
// import AValue, {BValue, Type1, Type2} from 'foo' (x2)
// import {AValue, Type1, Type2} from 'foo' (x2)
// import {Type1, Type2} from 'foo' (x2)
// The import is both default and named. Insert named on new line because can't mix default type import and named type imports
// eslint-disable-next-line no-lonely-if
// import Foo, * as Type from 'foo' (x2)
// import DefType, * as Type from 'foo' (x4)
// import Def, * as Ns from 'foo' (x4)
// ^^^^^^^^^ remove (x4)
// import type * as Ns from 'foo' (x2)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ insert (x2)
// import type Type from 'foo' (x2)
// ^^^^ insert (x2)
// import Type , {...} from 'foo' (x4)
// ^^^^^ pick (x2)
// ^^^^^^^ remove (x2)
Code
function* fixToTypeImportDeclaration(
fixer: TSESLint.RuleFixer,
report: ReportValueImport,
sourceImports: SourceImports,
): IterableIterator<TSESLint.RuleFix> {
const { node } = report;
const { defaultSpecifier, namedSpecifiers, namespaceSpecifier } =
classifySpecifier(node);
if (namespaceSpecifier && !defaultSpecifier) {
// import * as types from 'foo'
// checks for presence of import assertions
if (node.attributes.length === 0) {
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false);
}
return;
}
if (defaultSpecifier) {
if (
report.typeSpecifiers.includes(defaultSpecifier) &&
namedSpecifiers.length === 0 &&
!namespaceSpecifier
) {
// import Type from 'foo'
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, true);
return;
}
if (
fixStyle === 'inline-type-imports' &&
!report.typeSpecifiers.includes(defaultSpecifier) &&
namedSpecifiers.length > 0 &&
!namespaceSpecifier
) {
// if there is a default specifier but it isn't a type specifier, then just add the inline type modifier to the named specifiers
// import AValue, {BValue, Type1, Type2} from 'foo'
yield* fixInlineTypeImportDeclaration(fixer, report, sourceImports);
return;
}
} else if (!namespaceSpecifier) {
if (
fixStyle === 'inline-type-imports' &&
namedSpecifiers.some(specifier =>
report.typeSpecifiers.includes(specifier),
)
) {
// import {AValue, Type1, Type2} from 'foo'
yield* fixInlineTypeImportDeclaration(fixer, report, sourceImports);
return;
}
if (
namedSpecifiers.every(specifier =>
report.typeSpecifiers.includes(specifier),
)
) {
// import {Type1, Type2} from 'foo'
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false);
return;
}
}
const typeNamedSpecifiers = namedSpecifiers.filter(specifier =>
report.typeSpecifiers.includes(specifier),
);
const fixesNamedSpecifiers = getFixesNamedSpecifiers(
fixer,
node,
typeNamedSpecifiers,
namedSpecifiers,
);
const afterFixes: TSESLint.RuleFix[] = [];
if (typeNamedSpecifiers.length) {
if (sourceImports.typeOnlyNamedImport) {
const insertTypeNamedSpecifiers =
fixInsertNamedSpecifiersInNamedSpecifierList(
fixer,
sourceImports.typeOnlyNamedImport,
fixesNamedSpecifiers.typeNamedSpecifiersText,
);
if (sourceImports.typeOnlyNamedImport.range[1] <= node.range[0]) {
yield insertTypeNamedSpecifiers;
} else {
afterFixes.push(insertTypeNamedSpecifiers);
}
} else {
// The import is both default and named. Insert named on new line because can't mix default type import and named type imports
// eslint-disable-next-line no-lonely-if
if (fixStyle === 'inline-type-imports') {
yield fixer.insertTextBefore(
node,
`import {${typeNamedSpecifiers
.map(spec => {
const insertText = context.sourceCode.text.slice(
...spec.range,
);
return `type ${insertText}`;
})
.join(
', ',
)}} from ${context.sourceCode.getText(node.source)};\n`,
);
} else {
yield fixer.insertTextBefore(
node,
`import type {${
fixesNamedSpecifiers.typeNamedSpecifiersText
}} from ${context.sourceCode.getText(node.source)};\n`,
);
}
}
}
const fixesRemoveTypeNamespaceSpecifier: TSESLint.RuleFix[] = [];
if (
namespaceSpecifier &&
report.typeSpecifiers.includes(namespaceSpecifier)
) {
// import Foo, * as Type from 'foo'
// import DefType, * as Type from 'foo'
// import DefType, * as Type from 'foo'
const commaToken = nullThrows(
context.sourceCode.getTokenBefore(namespaceSpecifier, isCommaToken),
NullThrowsReasons.MissingToken(',', node.type),
);
// import Def, * as Ns from 'foo'
// ^^^^^^^^^ remove
fixesRemoveTypeNamespaceSpecifier.push(
fixer.removeRange([commaToken.range[0], namespaceSpecifier.range[1]]),
);
// import type * as Ns from 'foo'
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ insert
yield fixer.insertTextBefore(
node,
`import type ${context.sourceCode.getText(
namespaceSpecifier,
)} from ${context.sourceCode.getText(node.source)};\n`,
);
}
if (
defaultSpecifier &&
report.typeSpecifiers.includes(defaultSpecifier)
) {
if (report.typeSpecifiers.length === node.specifiers.length) {
const importToken = nullThrows(
context.sourceCode.getFirstToken(node, isImportKeyword),
NullThrowsReasons.MissingToken('import', node.type),
);
// import type Type from 'foo'
// ^^^^ insert
yield fixer.insertTextAfter(importToken, ' type');
} else {
const commaToken = nullThrows(
context.sourceCode.getTokenAfter(defaultSpecifier, isCommaToken),
NullThrowsReasons.MissingToken(',', defaultSpecifier.type),
);
// import Type , {...} from 'foo'
// ^^^^^ pick
const defaultText = context.sourceCode.text
.slice(defaultSpecifier.range[0], commaToken.range[0])
.trim();
yield fixer.insertTextBefore(
node,
`import type ${defaultText} from ${context.sourceCode.getText(
node.source,
)};\n`,
);
const afterToken = nullThrows(
context.sourceCode.getTokenAfter(commaToken, {
includeComments: true,
}),
NullThrowsReasons.MissingToken('any token', node.type),
);
// import Type , {...} from 'foo'
// ^^^^^^^ remove
yield fixer.removeRange([
defaultSpecifier.range[0],
afterToken.range[0],
]);
}
}
yield* fixesNamedSpecifiers.removeTypeNamedSpecifiers;
yield* fixesRemoveTypeNamespaceSpecifier;
yield* afterFixes;
}
fixInsertTypeSpecifierForImportDeclaration(fixer: TSESLint.RuleFixer, node: TSESTree.ImportDeclaration, isDefaultImport: boolean): IterableIterator<TSESLint.RuleFix>¶
Parameters:
fixerTSESLint.RuleFixernodeTSESTree.ImportDeclarationisDefaultImportboolean
Returns: IterableIterator<TSESLint.RuleFix>
Calls:
nullThrows (from ../util)context.sourceCode.getFirstTokenNullThrowsReasons.MissingTokenfixer.insertTextAftercontext.sourceCode.getFirstTokenBetweencontext.sourceCode.getTokenBeforefixer.removeRangecontext.sourceCode.text.slicecontext.sourceCode.getTextfixRemoveTypeSpecifierFromImportSpecifier
Internal Comments:
// import type Foo from 'foo' (x2)
// ^^^^^ insert (x2)
// Has default import (x2)
// Only braces. e.g. import Foo, {} from 'foo' (x2)
// import type Foo, {} from 'foo' (x2)
// ^^ remove (x2)
// make sure we don't do anything like `import type {type T} from 'foo';`
Code
function* fixInsertTypeSpecifierForImportDeclaration(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportDeclaration,
isDefaultImport: boolean,
): IterableIterator<TSESLint.RuleFix> {
// import type Foo from 'foo'
// ^^^^^ insert
const importToken = nullThrows(
context.sourceCode.getFirstToken(node, isImportKeyword),
NullThrowsReasons.MissingToken('import', node.type),
);
yield fixer.insertTextAfter(importToken, ' type');
if (isDefaultImport) {
// Has default import
const openingBraceToken = context.sourceCode.getFirstTokenBetween(
importToken,
node.source,
isOpeningBraceToken,
);
if (openingBraceToken) {
// Only braces. e.g. import Foo, {} from 'foo'
const commaToken = nullThrows(
context.sourceCode.getTokenBefore(openingBraceToken, isCommaToken),
NullThrowsReasons.MissingToken(',', node.type),
);
const closingBraceToken = nullThrows(
context.sourceCode.getFirstTokenBetween(
openingBraceToken,
node.source,
isClosingBraceToken,
),
NullThrowsReasons.MissingToken('}', node.type),
);
// import type Foo, {} from 'foo'
// ^^ remove
yield fixer.removeRange([
commaToken.range[0],
closingBraceToken.range[1],
]);
const specifiersText = context.sourceCode.text.slice(
commaToken.range[1],
closingBraceToken.range[1],
);
if (node.specifiers.length > 1) {
yield fixer.insertTextAfter(
node,
`\nimport type${specifiersText} from ${context.sourceCode.getText(
node.source,
)};`,
);
}
}
}
// make sure we don't do anything like `import type {type T} from 'foo';`
for (const specifier of node.specifiers) {
if (
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
specifier.importKind === 'type'
) {
yield* fixRemoveTypeSpecifierFromImportSpecifier(fixer, specifier);
}
}
}
fixRemoveTypeSpecifierFromImportDeclaration(fixer: TSESLint.RuleFixer, node: TSESTree.ImportDeclaration): IterableIterator<TSESLint.RuleFix>¶
Parameters:
fixerTSESLint.RuleFixernodeTSESTree.ImportDeclaration
Returns: IterableIterator<TSESLint.RuleFix>
Calls:
nullThrows (from ../util)context.sourceCode.getFirstTokenNullThrowsReasons.MissingTokencontext.sourceCode.getFirstTokenBetweencontext.sourceCode.getTokenAfterfixer.removeRange
Internal Comments:
Code
function* fixRemoveTypeSpecifierFromImportDeclaration(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportDeclaration,
): IterableIterator<TSESLint.RuleFix> {
// import type Foo from 'foo'
// ^^^^ remove
const importToken = nullThrows(
context.sourceCode.getFirstToken(node, isImportKeyword),
NullThrowsReasons.MissingToken('import', node.type),
);
const typeToken = nullThrows(
context.sourceCode.getFirstTokenBetween(
importToken,
node.specifiers[0]?.local ?? node.source,
isTypeKeyword,
),
NullThrowsReasons.MissingToken('type', node.type),
);
const afterToken = nullThrows(
context.sourceCode.getTokenAfter(typeToken, { includeComments: true }),
NullThrowsReasons.MissingToken('any token', node.type),
);
yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]);
}
fixRemoveTypeSpecifierFromImportSpecifier(fixer: TSESLint.RuleFixer, node: TSESTree.ImportSpecifier): IterableIterator<TSESLint.RuleFix>¶
Parameters:
fixerTSESLint.RuleFixernodeTSESTree.ImportSpecifier
Returns: IterableIterator<TSESLint.RuleFix>
Calls:
nullThrows (from ../util)context.sourceCode.getFirstTokenNullThrowsReasons.MissingTokencontext.sourceCode.getTokenAfterfixer.removeRange
Internal Comments:
Code
function* fixRemoveTypeSpecifierFromImportSpecifier(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportSpecifier,
): IterableIterator<TSESLint.RuleFix> {
// import { type Foo } from 'foo'
// ^^^^ remove
const typeToken = nullThrows(
context.sourceCode.getFirstToken(node, isTypeKeyword),
NullThrowsReasons.MissingToken('type', node.type),
);
const afterToken = nullThrows(
context.sourceCode.getTokenAfter(typeToken, { includeComments: true }),
NullThrowsReasons.MissingToken('any token', node.type),
);
yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]);
}
Interfaces¶
SourceImports¶
Interface Code
interface SourceImports {
reportValueImports: ReportValueImport[];
source: string;
// ImportDeclaration for type-only import only with named imports.
typeOnlyNamedImport: TSESTree.ImportDeclaration | null;
// ImportDeclaration for value-only import only with default imports and/or named imports.
valueImport: TSESTree.ImportDeclaration | null;
// ImportDeclaration for value-only import only with named imports.
valueOnlyNamedImport: TSESTree.ImportDeclaration | null;
}
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
reportValueImports |
ReportValueImport[] |
✗ | not shown |
source |
string |
✗ | not shown |
typeOnlyNamedImport |
TSESTree.ImportDeclaration \| null |
✗ | not shown |
valueImport |
TSESTree.ImportDeclaration \| null |
✗ | not shown |
valueOnlyNamedImport |
TSESTree.ImportDeclaration \| null |
✗ | not shown |
ReportValueImport¶
Interface Code
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
inlineTypeSpecifiers |
TSESTree.ImportSpecifier[] |
✗ | not shown |
node |
TSESTree.ImportDeclaration |
✗ | not shown |
typeSpecifiers |
TSESTree.ImportClause[] |
✗ | not shown |
unusedSpecifiers |
TSESTree.ImportClause[] |
✗ | not shown |
valueSpecifiers |
TSESTree.ImportClause[] |
✗ | not shown |
Type Aliases¶
Prefer¶
FixStyle¶
Options¶
MessageIds¶
type MessageIds = | 'avoidImportType'
| 'noImportTypeAnnotations'
| 'someImportsAreOnlyTypes'
| 'typeOverValue';
Generated by Syntax Scribe