Type-only import specifiers (#45998)

* Parse type-only import specifiers

* Add type-only export specifiers

* Update transform and emit

* Update checking

* Fix elision when combined with importsNotUsedAsValues=preserve

* Accept baselines

* Add test

* WIP auto imports updates

* First auto-imports test working

* More auto-import tests

* Fix auto imports of type-only exports

* Add test for promoting type-only import

* Sort import/export specifiers by type-onlyness

* Update completions for `import { type |`

* Update other completions tests

* Respect organize imports sorting when promoting type-only to regular while adding a specifier

* Fix comment mistakes

* Update src/services/codefixes/importFixes.ts

Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>

* Rearrange some order of assignments in parser

* Split huge if statement

* Remove redundant check

* Update new transformer

* Fix import statement completions

* Fix type keyword completions good grief

* Fix last tests

Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>
This commit is contained in:
Andrew Branch
2021-09-27 12:38:30 -07:00
committed by GitHub
co-authored by Daniel Rosenwasser
parent 26aef89a72
commit e160bc8c0d
80 changed files with 1842 additions and 349 deletions
+4 -4
View File
@@ -294,7 +294,7 @@ namespace ts.codefix {
*/
const newNodes = [
makeConst(/*modifiers*/ undefined, rename, assignment.right),
makeExportDeclaration([factory.createExportSpecifier(rename, text)]),
makeExportDeclaration([factory.createExportSpecifier(/*isTypeOnly*/ false, rename, text)]),
];
changes.replaceNodeWithNodes(sourceFile, assignment.parent, newNodes);
}
@@ -317,7 +317,7 @@ namespace ts.codefix {
return makeExportDeclaration(/*exportClause*/ undefined, moduleSpecifier);
}
function reExportDefault(moduleSpecifier: string): ExportDeclaration {
return makeExportDeclaration([factory.createExportSpecifier(/*propertyName*/ undefined, "default")], moduleSpecifier);
return makeExportDeclaration([factory.createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, "default")], moduleSpecifier);
}
function convertExportsPropertyAssignment({ left, right, parent }: BinaryExpression & { left: PropertyAccessExpression }, sourceFile: SourceFile, changes: textChanges.ChangeTracker): void {
@@ -480,7 +480,7 @@ namespace ts.codefix {
}
const namedBindings = namedBindingsNames.size === 0 ? undefined : arrayFrom(mapIterator(namedBindingsNames.entries(), ([propertyName, idName]) =>
factory.createImportSpecifier(propertyName === idName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(idName))));
factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName === idName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(idName))));
if (!namedBindings) {
// If it was unused, ensure that we at least import *something*.
needDefaultImport = true;
@@ -573,7 +573,7 @@ namespace ts.codefix {
}
function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier {
return factory.createImportSpecifier(propertyName !== undefined && propertyName !== name ? factory.createIdentifier(propertyName) : undefined, factory.createIdentifier(name));
return factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName !== undefined && propertyName !== name ? factory.createIdentifier(propertyName) : undefined, factory.createIdentifier(name));
}
function makeConst(modifiers: readonly Modifier[] | undefined, name: string | BindingName, init: Expression): VariableStatement {
+1 -1
View File
@@ -532,6 +532,6 @@ namespace ts.codefix {
}
export function importSymbols(importAdder: ImportAdder, symbols: readonly Symbol[]) {
symbols.forEach(s => importAdder.addImportFromExportedSymbol(s, /*usageIsTypeOnly*/ true));
symbols.forEach(s => importAdder.addImportFromExportedSymbol(s, /*isValidTypeOnlyUseSite*/ true));
}
}
+327 -121
View File
@@ -16,12 +16,12 @@ namespace ts.codefix {
registerCodeFix({
errorCodes,
getCodeActions(context) {
const { errorCode, preferences, sourceFile, span } = context;
const { errorCode, preferences, sourceFile, span, program } = context;
const info = getFixesInfo(context, errorCode, span.start, /*useAutoImportProvider*/ true);
if (!info) return undefined;
const { fixes, symbolName } = info;
const quotePreference = getQuotePreference(sourceFile, preferences);
return fixes.map(fix => codeActionForFix(context, sourceFile, symbolName, fix, quotePreference));
return fixes.map(fix => codeActionForFix(context, sourceFile, symbolName, fix, quotePreference, program.getCompilerOptions()));
},
fixIds: [importFixId],
getAllCodeActions: context => {
@@ -34,7 +34,7 @@ namespace ts.codefix {
export interface ImportAdder {
addImportFromDiagnostic: (diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) => void;
addImportFromExportedSymbol: (exportedSymbol: Symbol, usageIsTypeOnly?: boolean) => void;
addImportFromExportedSymbol: (exportedSymbol: Symbol, isValidTypeOnlyUseSite?: boolean) => void;
writeFixes: (changeTracker: textChanges.ChangeTracker) => void;
}
@@ -42,14 +42,23 @@ namespace ts.codefix {
return createImportAdderWorker(sourceFile, program, /*useAutoImportProvider*/ false, preferences, host);
}
interface AddToExistingState {
readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern;
defaultImport: Import | undefined;
readonly namedImports: ESMap<string, AddAsTypeOnly>;
}
function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost): ImportAdder {
const compilerOptions = program.getCompilerOptions();
// Namespace fixes don't conflict, so just build a list.
const addToNamespace: FixUseNamespaceImport[] = [];
const importType: FixUseImportType[] = [];
// Keys are import clause node IDs.
const addToExisting = new Map<string, { readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern, defaultImport: string | undefined; readonly namedImports: string[], canUseTypeOnlyImport: boolean }>();
const newImports = new Map<string, Mutable<ImportsCollection & { useRequire: boolean }>>();
const importType: FixAddJsdocTypeImport[] = [];
/** Keys are import clause node IDs. */
const addToExisting = new Map<string, AddToExistingState>();
type NewImportsKey = `${0 | 1}|${string}`;
/** Use `getNewImportEntry` for access */
const newImports = new Map<NewImportsKey, Mutable<ImportsCollection & { useRequire: boolean }>>();
return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes };
function addImportFromDiagnostic(diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) {
@@ -58,15 +67,14 @@ namespace ts.codefix {
addImport(info);
}
function addImportFromExportedSymbol(exportedSymbol: Symbol, usageIsTypeOnly?: boolean) {
function addImportFromExportedSymbol(exportedSymbol: Symbol, isValidTypeOnlyUseSite?: boolean) {
const moduleSymbol = Debug.checkDefined(exportedSymbol.parent);
const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions));
const checker = program.getTypeChecker();
const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker));
const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, host, program, preferences, useAutoImportProvider);
const preferTypeOnlyImport = !!usageIsTypeOnly && compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error;
const useRequire = shouldUseRequire(sourceFile, program);
const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, /*position*/ undefined, preferTypeOnlyImport, useRequire, host, preferences);
const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences);
if (fix) {
addImport({ fixes: [fix], symbolName });
}
@@ -79,47 +87,47 @@ namespace ts.codefix {
case ImportFixKind.UseNamespace:
addToNamespace.push(fix);
break;
case ImportFixKind.ImportType:
case ImportFixKind.JsdocTypeImport:
importType.push(fix);
break;
case ImportFixKind.AddToExisting: {
const { importClauseOrBindingPattern, importKind, canUseTypeOnlyImport } = fix;
const { importClauseOrBindingPattern, importKind, addAsTypeOnly } = fix;
const key = String(getNodeId(importClauseOrBindingPattern));
let entry = addToExisting.get(key);
if (!entry) {
addToExisting.set(key, entry = { importClauseOrBindingPattern, defaultImport: undefined, namedImports: [], canUseTypeOnlyImport });
addToExisting.set(key, entry = { importClauseOrBindingPattern, defaultImport: undefined, namedImports: new Map() });
}
if (importKind === ImportKind.Named) {
pushIfUnique(entry.namedImports, symbolName);
const prevValue = entry?.namedImports.get(symbolName);
entry.namedImports.set(symbolName, reduceAddAsTypeOnlyValues(prevValue, addAsTypeOnly));
}
else {
Debug.assert(entry.defaultImport === undefined || entry.defaultImport === symbolName, "(Add to Existing) Default import should be missing or match symbolName");
entry.defaultImport = symbolName;
Debug.assert(entry.defaultImport === undefined || entry.defaultImport.name === symbolName, "(Add to Existing) Default import should be missing or match symbolName");
entry.defaultImport = {
name: symbolName,
addAsTypeOnly: reduceAddAsTypeOnlyValues(entry.defaultImport?.addAsTypeOnly, addAsTypeOnly),
};
}
break;
}
case ImportFixKind.AddNew: {
const { moduleSpecifier, importKind, useRequire, typeOnly } = fix;
let entry = newImports.get(moduleSpecifier);
if (!entry) {
newImports.set(moduleSpecifier, entry = { namedImports: [], namespaceLikeImport: undefined, typeOnly, useRequire });
}
else {
// An import clause can only be type-only if every import fix contributing to it can be type-only.
entry.typeOnly = entry.typeOnly && typeOnly;
}
const { moduleSpecifier, importKind, useRequire, addAsTypeOnly } = fix;
const entry = getNewImportEntry(moduleSpecifier, importKind, useRequire, addAsTypeOnly);
Debug.assert(entry.useRequire === useRequire, "(Add new) Tried to add an `import` and a `require` for the same module");
switch (importKind) {
case ImportKind.Default:
Debug.assert(entry.defaultImport === undefined || entry.defaultImport === symbolName, "(Add new) Default import should be missing or match symbolName");
entry.defaultImport = symbolName;
Debug.assert(entry.defaultImport === undefined || entry.defaultImport.name === symbolName, "(Add new) Default import should be missing or match symbolName");
entry.defaultImport = { name: symbolName, addAsTypeOnly: reduceAddAsTypeOnlyValues(entry.defaultImport?.addAsTypeOnly, addAsTypeOnly) };
break;
case ImportKind.Named:
pushIfUnique(entry.namedImports || (entry.namedImports = []), symbolName);
const prevValue = (entry.namedImports ||= new Map()).get(symbolName);
entry.namedImports.set(symbolName, reduceAddAsTypeOnlyValues(prevValue, addAsTypeOnly));
break;
case ImportKind.CommonJS:
case ImportKind.Namespace:
Debug.assert(entry.namespaceLikeImport === undefined || entry.namespaceLikeImport.name === symbolName, "Namespacelike import shoudl be missing or match symbolName");
entry.namespaceLikeImport = { importKind, name: symbolName };
entry.namespaceLikeImport = { importKind, name: symbolName, addAsTypeOnly };
break;
}
break;
@@ -127,6 +135,52 @@ namespace ts.codefix {
default:
Debug.assertNever(fix, `fix wasn't never - got kind ${(fix as ImportFix).kind}`);
}
function reduceAddAsTypeOnlyValues(prevValue: AddAsTypeOnly | undefined, newValue: AddAsTypeOnly): AddAsTypeOnly {
// `NotAllowed` overrides `Required` because one addition of a new import might be required to be type-only
// because of `--importsNotUsedAsValues=error`, but if a second addition of the same import is `NotAllowed`
// to be type-only, the reason the first one was `Required` - the unused runtime dependency - is now moot.
// Alternatively, if one addition is `Required` because it has no value meaning under `--preserveValueImports`
// and `--isolatedModules`, it should be impossible for another addition to be `NotAllowed` since that would
// mean a type is being referenced in a value location.
return Math.max(prevValue ?? 0, newValue);
}
function getNewImportEntry(moduleSpecifier: string, importKind: ImportKind, useRequire: boolean, addAsTypeOnly: AddAsTypeOnly): Mutable<ImportsCollection & { useRequire: boolean }> {
// A default import that requires type-only makes the whole import type-only.
// (We could add `default` as a named import, but that style seems undesirable.)
// Under `--preserveValueImports` and `--importsNotUsedAsValues=error`, if a
// module default-exports a type but named-exports some values (weird), you would
// have to use a type-only default import and non-type-only named imports. These
// require two separate import declarations, so we build this into the map key.
const typeOnlyKey = newImportsKey(moduleSpecifier, /*topLevelTypeOnly*/ true);
const nonTypeOnlyKey = newImportsKey(moduleSpecifier, /*topLevelTypeOnly*/ false);
const typeOnlyEntry = newImports.get(typeOnlyKey);
const nonTypeOnlyEntry = newImports.get(nonTypeOnlyKey);
const newEntry: ImportsCollection & { useRequire: boolean } = {
defaultImport: undefined,
namedImports: undefined,
namespaceLikeImport: undefined,
useRequire
};
if (importKind === ImportKind.Default && addAsTypeOnly === AddAsTypeOnly.Required) {
if (typeOnlyEntry) return typeOnlyEntry;
newImports.set(typeOnlyKey, newEntry);
return newEntry;
}
if (addAsTypeOnly === AddAsTypeOnly.Allowed && (typeOnlyEntry || nonTypeOnlyEntry)) {
return (typeOnlyEntry || nonTypeOnlyEntry)!;
}
if (nonTypeOnlyEntry) {
return nonTypeOnlyEntry;
}
newImports.set(nonTypeOnlyKey, newEntry);
return newEntry;
}
function newImportsKey(moduleSpecifier: string, topLevelTypeOnly: boolean): NewImportsKey {
return `${topLevelTypeOnly ? 1 : 0}|${moduleSpecifier}`;
}
}
function writeFixes(changeTracker: textChanges.ChangeTracker) {
@@ -137,14 +191,27 @@ namespace ts.codefix {
for (const fix of importType) {
addImportType(changeTracker, sourceFile, fix, quotePreference);
}
addToExisting.forEach(({ importClauseOrBindingPattern, defaultImport, namedImports, canUseTypeOnlyImport }) => {
doAddExistingFix(changeTracker, sourceFile, importClauseOrBindingPattern, defaultImport, namedImports, canUseTypeOnlyImport);
addToExisting.forEach(({ importClauseOrBindingPattern, defaultImport, namedImports }) => {
doAddExistingFix(
changeTracker,
sourceFile,
importClauseOrBindingPattern,
defaultImport,
arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })),
compilerOptions);
});
let newDeclarations: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[] | undefined;
newImports.forEach(({ useRequire, ...imports }, moduleSpecifier) => {
newImports.forEach(({ useRequire, defaultImport, namedImports, namespaceLikeImport }, key) => {
const moduleSpecifier = key.slice(2); // From `${0 | 1}|${moduleSpecifier}` format
const getDeclarations = useRequire ? getNewRequires : getNewImports;
newDeclarations = combine(newDeclarations, getDeclarations(moduleSpecifier, quotePreference, imports));
const declarations = getDeclarations(
moduleSpecifier,
quotePreference,
defaultImport,
namedImports && arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })),
namespaceLikeImport);
newDeclarations = combine(newDeclarations, declarations);
});
if (newDeclarations) {
insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true);
@@ -153,16 +220,25 @@ namespace ts.codefix {
}
// Sorted with the preferred fix coming first.
const enum ImportFixKind { UseNamespace, ImportType, AddToExisting, AddNew }
type ImportFix = FixUseNamespaceImport | FixUseImportType | FixAddToExistingImport | FixAddNewImport;
const enum ImportFixKind { UseNamespace, JsdocTypeImport, AddToExisting, AddNew }
// These should not be combined as bitflags, but are given powers of 2 values to
// easily detect conflicts between `NotAllowed` and `Required` by giving them a unique sum.
// They're also ordered in terms of increasing priority for a fix-all scenario (see
// `reduceAddAsTypeOnlyValues`).
const enum AddAsTypeOnly {
Allowed = 1 << 0,
Required = 1 << 1,
NotAllowed = 1 << 2,
}
type ImportFix = FixUseNamespaceImport | FixAddJsdocTypeImport | FixAddToExistingImport | FixAddNewImport;
interface FixUseNamespaceImport {
readonly kind: ImportFixKind.UseNamespace;
readonly namespacePrefix: string;
readonly position: number;
readonly moduleSpecifier: string;
}
interface FixUseImportType {
readonly kind: ImportFixKind.ImportType;
interface FixAddJsdocTypeImport {
readonly kind: ImportFixKind.JsdocTypeImport;
readonly moduleSpecifier: string;
readonly position: number;
readonly exportInfo: SymbolExportInfo;
@@ -172,13 +248,13 @@ namespace ts.codefix {
readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern;
readonly moduleSpecifier: string;
readonly importKind: ImportKind.Default | ImportKind.Named;
readonly canUseTypeOnlyImport: boolean;
readonly addAsTypeOnly: AddAsTypeOnly;
}
interface FixAddNewImport {
readonly kind: ImportFixKind.AddNew;
readonly moduleSpecifier: string;
readonly importKind: ImportKind;
readonly typeOnly: boolean;
readonly addAsTypeOnly: AddAsTypeOnly;
readonly useRequire: boolean;
readonly exportInfo?: SymbolExportInfo;
}
@@ -187,6 +263,8 @@ namespace ts.codefix {
interface FixAddToExistingImportInfo {
readonly declaration: AnyImportOrRequire;
readonly importKind: ImportKind;
readonly targetFlags: SymbolFlags;
readonly symbol: Symbol;
}
export function getImportCompletionAction(
@@ -205,14 +283,22 @@ namespace ts.codefix {
? [getSymbolExportInfoForSymbol(exportedSymbol, moduleSymbol, program, host)]
: getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, preferences, /*useAutoImportProvider*/ true);
const useRequire = shouldUseRequire(sourceFile, program);
const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && !isSourceFileJS(sourceFile) && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, useRequire, host, preferences));
return { moduleSpecifier: fix.moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) };
const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, isValidTypeOnlyUseSite, useRequire, host, preferences));
return {
moduleSpecifier: fix.moduleSpecifier,
codeAction: codeFixActionToCodeAction(codeActionForFix(
{ host, formatContext, preferences },
sourceFile,
symbolName,
fix,
getQuotePreference(sourceFile, preferences), compilerOptions))
};
}
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol || info.symbol.parent === moduleSymbol), "Some exportInfo should match the specified moduleSymbol");
return getBestFix(getImportFixes(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, program, host, preferences);
return getBestFix(getImportFixes(exportInfos, symbolName, position, isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences), sourceFile, program, host, preferences);
}
function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction {
@@ -284,7 +370,7 @@ namespace ts.codefix {
program,
importingFile,
/*position*/ undefined,
/*preferTypeOnlyImport*/ false,
/*isValidTypeOnlyUseSite*/ false,
/*useRequire*/ false,
exportInfo,
host,
@@ -294,16 +380,12 @@ namespace ts.codefix {
return result && { ...result, computedWithoutCacheCount };
}
function isTypeOnlyPosition(sourceFile: SourceFile, position: number) {
return isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
}
function getImportFixes(
exportInfos: readonly SymbolExportInfo[],
symbolName: string,
/** undefined only for missing JSX namespace */
position: number | undefined,
preferTypeOnlyImport: boolean,
isValidTypeOnlyUseSite: boolean,
useRequire: boolean,
program: Program,
sourceFile: SourceFile,
@@ -313,9 +395,9 @@ namespace ts.codefix {
const checker = program.getTypeChecker();
const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, checker, sourceFile, program.getCompilerOptions()));
const useNamespace = position === undefined ? undefined : tryUseExistingNamespaceImport(existingImports, symbolName, position, checker);
const addToExisting = tryAddToExistingImport(existingImports, position !== undefined && isTypeOnlyPosition(sourceFile, position));
const addToExisting = tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions());
// Don't bother providing an action to add a new import if we can add to an existing one.
const addImport = addToExisting ? [addToExisting] : getFixesForAddImport(exportInfos, existingImports, program, sourceFile, position, preferTypeOnlyImport, useRequire, host, preferences);
const addImport = addToExisting ? [addToExisting] : getFixesForAddImport(exportInfos, existingImports, program, sourceFile, position, isValidTypeOnlyUseSite, useRequire, host, preferences);
return [...(useNamespace ? [useNamespace] : emptyArray), ...addImport];
}
@@ -371,37 +453,88 @@ namespace ts.codefix {
}
}
function tryAddToExistingImport(existingImports: readonly FixAddToExistingImportInfo[], canUseTypeOnlyImport: boolean): FixAddToExistingImport | undefined {
return firstDefined(existingImports, ({ declaration, importKind }): FixAddToExistingImport | undefined => {
if (declaration.kind === SyntaxKind.ImportEqualsDeclaration) return undefined;
function getAddAsTypeOnly(
isValidTypeOnlyUseSite: boolean,
isForNewImportDeclaration: boolean,
symbol: Symbol,
targetFlags: SymbolFlags,
checker: TypeChecker,
compilerOptions: CompilerOptions
) {
if (!isValidTypeOnlyUseSite) {
// Can't use a type-only import if the usage is an emitting position
return AddAsTypeOnly.NotAllowed;
}
if (isForNewImportDeclaration && compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error) {
// Not writing a (top-level) type-only import here would create an error because the runtime dependency is unnecessary
return AddAsTypeOnly.Required;
}
if (compilerOptions.isolatedModules && compilerOptions.preserveValueImports &&
(!(targetFlags & SymbolFlags.Value) || !!checker.getTypeOnlyAliasDeclaration(symbol))
) {
// A type-only import is required for this symbol if under these settings if the symbol will
// be erased, which will happen if the target symbol is purely a type or if it was exported/imported
// as type-only already somewhere between this import and the target.
return AddAsTypeOnly.Required;
}
return AddAsTypeOnly.Allowed;
}
function tryAddToExistingImport(existingImports: readonly FixAddToExistingImportInfo[], isValidTypeOnlyUseSite: boolean, checker: TypeChecker, compilerOptions: CompilerOptions): FixAddToExistingImport | undefined {
return firstDefined(existingImports, ({ declaration, importKind, symbol, targetFlags }): FixAddToExistingImport | undefined => {
if (importKind === ImportKind.CommonJS || importKind === ImportKind.Namespace || declaration.kind === SyntaxKind.ImportEqualsDeclaration) {
// These kinds of imports are not combinable with anything
return undefined;
}
if (declaration.kind === SyntaxKind.VariableDeclaration) {
return (importKind === ImportKind.Named || importKind === ImportKind.Default) && declaration.name.kind === SyntaxKind.ObjectBindingPattern
? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: declaration.name, importKind, moduleSpecifier: declaration.initializer.arguments[0].text, canUseTypeOnlyImport: false }
? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: declaration.name, importKind, moduleSpecifier: declaration.initializer.arguments[0].text, addAsTypeOnly: AddAsTypeOnly.NotAllowed }
: undefined;
}
const { importClause } = declaration;
if (!importClause || !isStringLiteralLike(declaration.moduleSpecifier)) return undefined;
const { name, namedBindings } = importClause;
// A type-only import may not have both a default and named imports, so the only way a name can
// be added to an existing type-only import is adding a named import to existing named bindings.
if (importClause.isTypeOnly && !(importKind === ImportKind.Named && namedBindings)) return undefined;
return importKind === ImportKind.Default && !name || importKind === ImportKind.Named && (!namedBindings || namedBindings.kind === SyntaxKind.NamedImports)
? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: importClause, importKind, moduleSpecifier: declaration.moduleSpecifier.text, canUseTypeOnlyImport }
: undefined;
// N.B. we don't have to figure out whether to use the main program checker
// or the AutoImportProvider checker because we're adding to an existing import; the existence of
// the import guarantees the symbol came from the main program.
const addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, /*isForNewImportDeclaration*/ false, symbol, targetFlags, checker, compilerOptions);
if (importKind === ImportKind.Default && (
name || // Cannot add a default import to a declaration that already has one
addAsTypeOnly === AddAsTypeOnly.Required && namedBindings // Cannot add a default import as type-only if the import already has named bindings
)) return undefined;
if (
importKind === ImportKind.Named &&
namedBindings?.kind === SyntaxKind.NamespaceImport // Cannot add a named import to a declaration that has a namespace import
) return undefined;
return {
kind: ImportFixKind.AddToExisting,
importClauseOrBindingPattern: importClause,
importKind,
moduleSpecifier: declaration.moduleSpecifier.text,
addAsTypeOnly,
};
});
}
function getExistingImportDeclarations({ moduleSymbol, exportKind, targetFlags }: SymbolExportInfo, checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions): readonly FixAddToExistingImportInfo[] {
function getExistingImportDeclarations({ moduleSymbol, exportKind, targetFlags, symbol }: SymbolExportInfo, checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions): readonly FixAddToExistingImportInfo[] {
// Can't use an es6 import for a type in JS.
if (!(targetFlags & SymbolFlags.Value) && isSourceFileJS(importingFile)) return emptyArray;
const importKind = getImportKind(importingFile, exportKind, compilerOptions);
return mapDefined(importingFile.imports, (moduleSpecifier): FixAddToExistingImportInfo | undefined => {
const i = importFromModuleSpecifier(moduleSpecifier);
if (isRequireVariableDeclaration(i.parent)) {
return checker.resolveExternalModuleName(moduleSpecifier) === moduleSymbol ? { declaration: i.parent, importKind } : undefined;
return checker.resolveExternalModuleName(moduleSpecifier) === moduleSymbol ? { declaration: i.parent, importKind, symbol, targetFlags } : undefined;
}
if (i.kind === SyntaxKind.ImportDeclaration || i.kind === SyntaxKind.ImportEqualsDeclaration) {
return checker.getSymbolAtLocation(moduleSpecifier) === moduleSymbol ? { declaration: i, importKind } : undefined;
return checker.getSymbolAtLocation(moduleSpecifier) === moduleSymbol ? { declaration: i, importKind, symbol, targetFlags } : undefined;
}
});
}
@@ -437,35 +570,38 @@ namespace ts.codefix {
program: Program,
sourceFile: SourceFile,
position: number | undefined,
preferTypeOnlyImport: boolean,
isValidTypeOnlyUseSite: boolean,
useRequire: boolean,
moduleSymbols: readonly SymbolExportInfo[],
host: LanguageServiceHost,
preferences: UserPreferences,
fromCacheOnly?: boolean,
): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixUseImportType)[] } {
): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixAddJsdocTypeImport)[] } {
const isJs = isSourceFileJS(sourceFile);
const compilerOptions = program.getCompilerOptions();
const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host);
const checker = program.getTypeChecker();
const getChecker = memoizeOne((isFromPackageJson: boolean) => isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker());
const getModuleSpecifiers = fromCacheOnly
? (moduleSymbol: Symbol) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false })
: (moduleSymbol: Symbol) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences);
: (moduleSymbol: Symbol, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences);
let computedWithoutCacheCount = 0;
const fixes = flatMap(moduleSymbols, exportInfo => {
const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers(exportInfo.moduleSymbol);
const checker = getChecker(exportInfo.isFromPackageJson);
const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers(exportInfo.moduleSymbol, checker);
const importedSymbolHasValueMeaning = !!(exportInfo.targetFlags & SymbolFlags.Value);
const addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, /*isForNewImportDeclaration*/ true, exportInfo.symbol, exportInfo.targetFlags, checker, compilerOptions);
computedWithoutCacheCount += computedWithoutCache ? 1 : 0;
return moduleSpecifiers?.map((moduleSpecifier): FixAddNewImport | FixUseImportType =>
return moduleSpecifiers?.map((moduleSpecifier): FixAddNewImport | FixAddJsdocTypeImport =>
// `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types.
!(exportInfo.targetFlags & SymbolFlags.Value) && isJs && position !== undefined
? { kind: ImportFixKind.ImportType, moduleSpecifier, position, exportInfo }
!importedSymbolHasValueMeaning && isJs && position !== undefined
? { kind: ImportFixKind.JsdocTypeImport, moduleSpecifier, position, exportInfo }
: {
kind: ImportFixKind.AddNew,
moduleSpecifier,
importKind: getImportKind(sourceFile, exportInfo.exportKind, compilerOptions),
useRequire,
typeOnly: preferTypeOnlyImport,
addAsTypeOnly,
exportInfo,
}
);
@@ -480,20 +616,29 @@ namespace ts.codefix {
program: Program,
sourceFile: SourceFile,
position: number | undefined,
preferTypeOnlyImport: boolean,
isValidTypeOnlyUseSite: boolean,
useRequire: boolean,
host: LanguageServiceHost,
preferences: UserPreferences,
): readonly (FixAddNewImport | FixUseImportType)[] {
const existingDeclaration = firstDefined(existingImports, info => newImportInfoFromExistingSpecifier(info, preferTypeOnlyImport, useRequire));
return existingDeclaration ? [existingDeclaration] : getNewImportFixes(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences).fixes;
): readonly (FixAddNewImport | FixAddJsdocTypeImport)[] {
const existingDeclaration = firstDefined(existingImports, info => newImportInfoFromExistingSpecifier(info, isValidTypeOnlyUseSite, useRequire, program.getTypeChecker(), program.getCompilerOptions()));
return existingDeclaration ? [existingDeclaration] : getNewImportFixes(program, sourceFile, position, isValidTypeOnlyUseSite, useRequire, exportInfos, host, preferences).fixes;
}
function newImportInfoFromExistingSpecifier({ declaration, importKind }: FixAddToExistingImportInfo, preferTypeOnlyImport: boolean, useRequire: boolean): FixAddNewImport | undefined {
function newImportInfoFromExistingSpecifier(
{ declaration, importKind, symbol, targetFlags }: FixAddToExistingImportInfo,
isValidTypeOnlyUseSite: boolean,
useRequire: boolean,
checker: TypeChecker,
compilerOptions: CompilerOptions
): FixAddNewImport | undefined {
const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration);
return moduleSpecifier
? { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, typeOnly: preferTypeOnlyImport, useRequire }
: undefined;
if (moduleSpecifier) {
const addAsTypeOnly = useRequire
? AddAsTypeOnly.NotAllowed
: getAddAsTypeOnly(isValidTypeOnlyUseSite, /*isForNewImportDeclaration*/ true, symbol, targetFlags, checker, compilerOptions);
return { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, addAsTypeOnly, useRequire };
}
}
interface FixesInfo { readonly fixes: readonly ImportFix[]; readonly symbolName: string; }
@@ -545,7 +690,7 @@ namespace ts.codefix {
const symbolName = umdSymbol.name;
const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, moduleFileName: undefined, exportKind: ExportKind.UMD, targetFlags: symbol.flags, isFromPackageJson: false }];
const useRequire = shouldUseRequire(sourceFile, program);
const fixes = getImportFixes(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences);
const fixes = getImportFixes(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*isValidTypeOnlyUseSite*/ false, useRequire, program, sourceFile, host, preferences);
return { fixes, symbolName };
}
function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined {
@@ -612,11 +757,11 @@ namespace ts.codefix {
// "default" is a keyword and not a legal identifier for the import, so we don't expect it here
Debug.assert(symbolName !== InternalSymbolName.Default, "'default' isn't a legal identifier and couldn't occur here");
const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && isValidTypeOnlyAliasUseSite(symbolToken);
const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(symbolToken);
const useRequire = shouldUseRequire(sourceFile, program);
const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host, preferences);
const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) =>
getImportFixes(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences)));
getImportFixes(exportInfos, symbolName, symbolToken.getStart(sourceFile), isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences)));
return { fixes, symbolName };
}
@@ -707,34 +852,44 @@ namespace ts.codefix {
return allowSyntheticDefaults ? ImportKind.Default : ImportKind.CommonJS;
}
function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: SourceFile, symbolName: string, fix: ImportFix, quotePreference: QuotePreference): CodeFixAction {
function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: SourceFile, symbolName: string, fix: ImportFix, quotePreference: QuotePreference, compilerOptions: CompilerOptions): CodeFixAction {
let diag!: DiagnosticAndArguments;
const changes = textChanges.ChangeTracker.with(context, tracker => {
diag = codeActionForFixWorker(tracker, sourceFile, symbolName, fix, quotePreference);
diag = codeActionForFixWorker(tracker, sourceFile, symbolName, fix, quotePreference, compilerOptions);
});
return createCodeFixAction(importFixName, changes, diag, importFixId, Diagnostics.Add_all_missing_imports);
}
function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, quotePreference: QuotePreference): DiagnosticAndArguments {
function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, quotePreference: QuotePreference, compilerOptions: CompilerOptions): DiagnosticAndArguments {
switch (fix.kind) {
case ImportFixKind.UseNamespace:
addNamespaceQualifier(changes, sourceFile, fix);
return [Diagnostics.Change_0_to_1, symbolName, `${fix.namespacePrefix}.${symbolName}`];
case ImportFixKind.ImportType:
case ImportFixKind.JsdocTypeImport:
addImportType(changes, sourceFile, fix, quotePreference);
return [Diagnostics.Change_0_to_1, symbolName, getImportTypePrefix(fix.moduleSpecifier, quotePreference) + symbolName];
case ImportFixKind.AddToExisting: {
const { importClauseOrBindingPattern, importKind, canUseTypeOnlyImport, moduleSpecifier } = fix;
doAddExistingFix(changes, sourceFile, importClauseOrBindingPattern, importKind === ImportKind.Default ? symbolName : undefined, importKind === ImportKind.Named ? [symbolName] : emptyArray, canUseTypeOnlyImport);
const { importClauseOrBindingPattern, importKind, addAsTypeOnly, moduleSpecifier } = fix;
doAddExistingFix(
changes,
sourceFile,
importClauseOrBindingPattern,
importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined,
importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : emptyArray,
compilerOptions);
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier);
return [importKind === ImportKind.Default ? Diagnostics.Add_default_import_0_to_existing_import_declaration_from_1 : Diagnostics.Add_0_to_existing_import_declaration_from_1, symbolName, moduleSpecifierWithoutQuotes]; // you too!
return [
importKind === ImportKind.Default ? Diagnostics.Add_default_import_0_to_existing_import_declaration_from_1 : Diagnostics.Add_0_to_existing_import_declaration_from_1,
symbolName,
moduleSpecifierWithoutQuotes
]; // you too!
}
case ImportFixKind.AddNew: {
const { importKind, moduleSpecifier, typeOnly, useRequire } = fix;
const { importKind, moduleSpecifier, addAsTypeOnly, useRequire } = fix;
const getDeclarations = useRequire ? getNewRequires : getNewImports;
const importsCollection = importKind === ImportKind.Default ? { defaultImport: symbolName, typeOnly } :
importKind === ImportKind.Named ? { namedImports: [symbolName], typeOnly } :
{ namespaceLikeImport: { importKind, name: symbolName }, typeOnly };
insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, importsCollection), /*blankLineBetween*/ true);
const defaultImport: Import | undefined = importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined;
const namedImports: Import[] | undefined = importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : undefined;
const namespaceLikeImport = importKind === ImportKind.Namespace || importKind === ImportKind.CommonJS ? { importKind, name: symbolName, addAsTypeOnly } : undefined;
insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport), /*blankLineBetween*/ true);
return [importKind === ImportKind.Default ? Diagnostics.Import_default_0_from_module_1 : Diagnostics.Import_0_from_module_1, symbolName, moduleSpecifier];
}
default:
@@ -742,32 +897,55 @@ namespace ts.codefix {
}
}
function doAddExistingFix(changes: textChanges.ChangeTracker, sourceFile: SourceFile, clause: ImportClause | ObjectBindingPattern, defaultImport: string | undefined, namedImports: readonly string[], canUseTypeOnlyImport: boolean): void {
function doAddExistingFix(
changes: textChanges.ChangeTracker,
sourceFile: SourceFile,
clause: ImportClause | ObjectBindingPattern,
defaultImport: Import | undefined,
namedImports: readonly Import[],
compilerOptions: CompilerOptions,
): void {
if (clause.kind === SyntaxKind.ObjectBindingPattern) {
if (defaultImport) {
addElementToBindingPattern(clause, defaultImport, "default");
addElementToBindingPattern(clause, defaultImport.name, "default");
}
for (const specifier of namedImports) {
addElementToBindingPattern(clause, specifier, /*propertyName*/ undefined);
addElementToBindingPattern(clause, specifier.name, /*propertyName*/ undefined);
}
return;
}
const convertTypeOnlyToRegular = !canUseTypeOnlyImport && clause.isTypeOnly;
const promoteFromTypeOnly = clause.isTypeOnly && some([defaultImport, ...namedImports], i => i?.addAsTypeOnly === AddAsTypeOnly.NotAllowed);
const existingSpecifiers = clause.namedBindings && tryCast(clause.namedBindings, isNamedImports)?.elements;
// If we are promoting from a type-only import and `--isolatedModules` and `--preserveValueImports`
// are enabled, we need to make every existing import specifier type-only. It may be possible that
// some of them don't strictly need to be marked type-only (if they have a value meaning and are
// never used in an emitting position). These are allowed to be imported without being type-only,
// but the user has clearly already signified that they don't need them to be present at runtime
// by placing them in a type-only import. So, just mark each specifier as type-only.
const convertExistingToTypeOnly = promoteFromTypeOnly && compilerOptions.preserveValueImports && compilerOptions.isolatedModules;
if (defaultImport) {
Debug.assert(!clause.name, "Cannot add a default import to an import clause that already has one");
changes.insertNodeAt(sourceFile, clause.getStart(sourceFile), factory.createIdentifier(defaultImport), { suffix: ", " });
changes.insertNodeAt(sourceFile, clause.getStart(sourceFile), factory.createIdentifier(defaultImport.name), { suffix: ", " });
}
if (namedImports.length) {
const existingSpecifiers = clause.namedBindings && cast(clause.namedBindings, isNamedImports).elements;
const newSpecifiers = stableSort(
namedImports.map(name => factory.createImportSpecifier(/*propertyName*/ undefined, factory.createIdentifier(name))),
namedImports.map(namedImport => factory.createImportSpecifier(
(!clause.isTypeOnly || promoteFromTypeOnly) && needsTypeOnly(namedImport),
/*propertyName*/ undefined,
factory.createIdentifier(namedImport.name))),
OrganizeImports.compareImportOrExportSpecifiers);
if (existingSpecifiers?.length && OrganizeImports.importSpecifiersAreSorted(existingSpecifiers)) {
for (const spec of newSpecifiers) {
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec);
// Organize imports puts type-only import specifiers last, so if we're
// adding a non-type-only specifier and converting all the other ones to
// type-only, there's no need to ask for the insertion index - it's 0.
const insertionIndex = convertExistingToTypeOnly && !spec.isTypeOnly
? 0
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec);
const prevSpecifier = (clause.namedBindings as NamedImports).elements[insertionIndex - 1];
if (prevSpecifier) {
changes.insertNodeInListAfter(sourceFile, prevSpecifier, spec);
@@ -799,8 +977,13 @@ namespace ts.codefix {
}
}
if (convertTypeOnlyToRegular) {
if (promoteFromTypeOnly) {
changes.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(clause, sourceFile));
if (convertExistingToTypeOnly && existingSpecifiers) {
for (const specifier of existingSpecifiers) {
changes.insertModifierBefore(sourceFile, SyntaxKind.TypeKeyword, specifier);
}
}
}
function addElementToBindingPattern(bindingPattern: ObjectBindingPattern, name: string, propertyName: string | undefined) {
@@ -818,7 +1001,7 @@ namespace ts.codefix {
changes.insertText(sourceFile, position, namespacePrefix + ".");
}
function addImportType(changes: textChanges.ChangeTracker, sourceFile: SourceFile, { moduleSpecifier, position }: FixUseImportType, quotePreference: QuotePreference): void {
function addImportType(changes: textChanges.ChangeTracker, sourceFile: SourceFile, { moduleSpecifier, position }: FixAddJsdocTypeImport, quotePreference: QuotePreference): void {
changes.insertText(sourceFile, position, getImportTypePrefix(moduleSpecifier, quotePreference));
}
@@ -827,37 +1010,60 @@ namespace ts.codefix {
return `import(${quote}${moduleSpecifier}${quote}).`;
}
interface Import {
readonly name: string;
readonly addAsTypeOnly: AddAsTypeOnly;
}
interface ImportsCollection {
readonly typeOnly: boolean;
readonly defaultImport?: string;
readonly namedImports?: string[];
readonly defaultImport?: Import;
readonly namedImports?: ESMap<string, AddAsTypeOnly>;
readonly namespaceLikeImport?: {
readonly importKind: ImportKind.CommonJS | ImportKind.Namespace;
readonly name: string;
readonly addAsTypeOnly: AddAsTypeOnly;
};
}
function getNewImports(moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection): AnyImportSyntax | readonly AnyImportSyntax[] {
function needsTypeOnly({ addAsTypeOnly }: { addAsTypeOnly: AddAsTypeOnly }): boolean {
return addAsTypeOnly === AddAsTypeOnly.Required;
}
function getNewImports(
moduleSpecifier: string,
quotePreference: QuotePreference,
defaultImport: Import | undefined,
namedImports: readonly Import[] | undefined,
namespaceLikeImport: Import & { importKind: ImportKind.CommonJS | ImportKind.Namespace } | undefined
): AnyImportSyntax | readonly AnyImportSyntax[] {
const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference);
let statements: AnyImportSyntax | readonly AnyImportSyntax[] | undefined;
if (imports.defaultImport !== undefined || imports.namedImports?.length) {
if (defaultImport !== undefined || namedImports?.length) {
const topLevelTypeOnly = (!defaultImport || needsTypeOnly(defaultImport)) && every(namedImports, needsTypeOnly);
statements = combine(statements, makeImport(
imports.defaultImport === undefined ? undefined : factory.createIdentifier(imports.defaultImport),
imports.namedImports?.map(n => factory.createImportSpecifier(/*propertyName*/ undefined, factory.createIdentifier(n))), moduleSpecifier, quotePreference, imports.typeOnly));
defaultImport && factory.createIdentifier(defaultImport.name),
namedImports?.map(({ addAsTypeOnly, name }) => factory.createImportSpecifier(
!topLevelTypeOnly && addAsTypeOnly === AddAsTypeOnly.Required,
/*propertyName*/ undefined,
factory.createIdentifier(name))),
moduleSpecifier,
quotePreference,
topLevelTypeOnly));
}
const { namespaceLikeImport, typeOnly } = imports;
if (namespaceLikeImport) {
const declaration = namespaceLikeImport.importKind === ImportKind.CommonJS
? factory.createImportEqualsDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
typeOnly,
needsTypeOnly(namespaceLikeImport),
factory.createIdentifier(namespaceLikeImport.name),
factory.createExternalModuleReference(quotedModuleSpecifier))
: factory.createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
factory.createImportClause(
typeOnly,
needsTypeOnly(namespaceLikeImport),
/*name*/ undefined,
factory.createNamespaceImport(factory.createIdentifier(namespaceLikeImport.name))),
quotedModuleSpecifier,
@@ -867,21 +1073,21 @@ namespace ts.codefix {
return Debug.checkDefined(statements);
}
function getNewRequires(moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection): RequireVariableStatement | readonly RequireVariableStatement[] {
function getNewRequires(moduleSpecifier: string, quotePreference: QuotePreference, defaultImport: Import | undefined, namedImports: readonly Import[] | undefined, namespaceLikeImport: Import | undefined): RequireVariableStatement | readonly RequireVariableStatement[] {
const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference);
let statements: RequireVariableStatement | readonly RequireVariableStatement[] | undefined;
// const { default: foo, bar, etc } = require('./mod');
if (imports.defaultImport || imports.namedImports?.length) {
const bindingElements = imports.namedImports?.map(name => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name)) || [];
if (imports.defaultImport) {
bindingElements.unshift(factory.createBindingElement(/*dotDotDotToken*/ undefined, "default", imports.defaultImport));
if (defaultImport || namedImports?.length) {
const bindingElements = namedImports?.map(({ name }) => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name)) || [];
if (defaultImport) {
bindingElements.unshift(factory.createBindingElement(/*dotDotDotToken*/ undefined, "default", defaultImport.name));
}
const declaration = createConstEqualsRequireDeclaration(factory.createObjectBindingPattern(bindingElements), quotedModuleSpecifier);
statements = combine(statements, declaration);
}
// const foo = require('./mod');
if (imports.namespaceLikeImport) {
const declaration = createConstEqualsRequireDeclaration(imports.namespaceLikeImport.name, quotedModuleSpecifier);
if (namespaceLikeImport) {
const declaration = createConstEqualsRequireDeclaration(namespaceLikeImport.name, quotedModuleSpecifier);
statements = combine(statements, declaration);
}
return Debug.checkDefined(statements);
+1 -1
View File
@@ -62,7 +62,7 @@ namespace ts.codefix {
if (!isIdentifier(element.name) || element.initializer) {
return undefined;
}
importSpecifiers.push(factory.createImportSpecifier(tryCast(element.propertyName, isIdentifier), element.name));
importSpecifiers.push(factory.createImportSpecifier(/*isTypeOnly*/ false, tryCast(element.propertyName, isIdentifier), element.name));
}
if (importSpecifiers.length) {
+186 -55
View File
@@ -149,6 +149,7 @@ namespace ts.Completions {
FunctionLikeBodyKeywords, // Keywords at function like body
TypeAssertionKeywords,
TypeKeywords,
TypeKeyword, // Literally just `type`
Last = TypeKeywords
}
@@ -287,7 +288,7 @@ namespace ts.Completions {
case CompletionDataKind.JsDocParameterName:
return jsdocCompletionInfo(JsDoc.getJSDocParameterNameCompletions(completionData.tag));
case CompletionDataKind.Keywords:
return specificKeywordCompletionInfo(completionData.keywords);
return specificKeywordCompletionInfo(completionData.keywordCompletions, completionData.isNewIdentifierLocation);
default:
return Debug.assertNever(completionData);
}
@@ -368,20 +369,39 @@ namespace ts.Completions {
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
}
function specificKeywordCompletionInfo(keywords: readonly SyntaxKind[]): CompletionInfo {
function keywordToCompletionEntry(keyword: TokenSyntaxKind) {
return {
name: tokenToString(keyword)!,
kind: ScriptElementKind.keyword,
kindModifiers: ScriptElementKindModifier.none,
sortText: SortText.GlobalsOrKeywords,
};
}
function specificKeywordCompletionInfo(entries: readonly CompletionEntry[], isNewIdentifierLocation: boolean): CompletionInfo {
return {
isGlobalCompletion: false,
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: keywords.map(k => ({
name: tokenToString(k)!,
kind: ScriptElementKind.keyword,
kindModifiers: ScriptElementKindModifier.none,
sortText: SortText.GlobalsOrKeywords,
})),
isNewIdentifierLocation,
entries: entries.slice(),
};
}
function keywordCompletionData(keywordFilters: KeywordCompletionFilters, filterOutTsOnlyKeywords: boolean, isNewIdentifierLocation: boolean): Request {
return {
kind: CompletionDataKind.Keywords,
keywordCompletions: getKeywordCompletions(keywordFilters, filterOutTsOnlyKeywords),
isNewIdentifierLocation,
};
}
function keywordFiltersFromSyntaxKind(keywordCompletion: TokenSyntaxKind): KeywordCompletionFilters {
switch (keywordCompletion) {
case SyntaxKind.TypeKeyword: return KeywordCompletionFilters.TypeKeyword;
default: Debug.fail("Unknown mapping from SyntaxKind to KeywordCompletionFilters");
}
}
function getOptionalReplacementSpan(location: Node | undefined) {
// StringLiteralLike locations are handled separately in stringCompletions.ts
return location?.kind === SyntaxKind.Identifier ? createTextSpanFromNode(location) : undefined;
@@ -390,6 +410,7 @@ namespace ts.Completions {
function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined {
const {
symbols,
contextToken,
completionKind,
isInSnippetScope,
isNewIdentifierLocation,
@@ -422,7 +443,8 @@ namespace ts.Completions {
const uniqueNames = getCompletionEntriesFromSymbols(
symbols,
entries,
/* contextToken */ undefined,
/*replacementToken*/ undefined,
contextToken,
location,
sourceFile,
typeChecker,
@@ -450,7 +472,8 @@ namespace ts.Completions {
getCompletionEntriesFromSymbols(
symbols,
entries,
/* contextToken */ undefined,
/*replacementToken*/ undefined,
contextToken,
location,
sourceFile,
typeChecker,
@@ -591,6 +614,7 @@ namespace ts.Completions {
function createCompletionEntry(
symbol: Symbol,
sortText: SortText,
replacementToken: Node | undefined,
contextToken: Node | undefined,
location: Node,
sourceFile: SourceFile,
@@ -602,13 +626,12 @@ namespace ts.Completions {
propertyAccessToConvert: PropertyAccessExpression | undefined,
isJsxInitializer: IsJsxInitializer | undefined,
importCompletionNode: Node | undefined,
isTypeOnlyImport: boolean,
useSemicolons: boolean,
options: CompilerOptions,
preferences: UserPreferences,
): CompletionEntry | undefined {
let insertText: string | undefined;
let replacementSpan = getReplacementSpanForContextToken(contextToken);
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
let data: CompletionEntryData | undefined;
let isSnippet: true | undefined;
let sourceDisplay;
@@ -662,7 +685,7 @@ namespace ts.Completions {
if (originIsResolvedExport(origin)) {
sourceDisplay = [textPart(origin.moduleSpecifier)];
if (importCompletionNode) {
({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, isTypeOnlyImport, origin, useSemicolons, options, preferences));
({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, contextToken, origin, useSemicolons, options, preferences));
isSnippet = preferences.includeCompletionsWithSnippetText ? true : undefined;
}
}
@@ -747,9 +770,9 @@ namespace ts.Completions {
};
}
function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, isTypeOnly: boolean | undefined, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences) {
function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, contextToken: Node | undefined, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences) {
const sourceFile = importCompletionNode.getSourceFile();
const replacementSpan = createTextSpanFromNode(importCompletionNode, sourceFile);
const replacementSpan = createTextSpanFromNode(findAncestor(importCompletionNode, or(isImportDeclaration, isImportEqualsDeclaration)) || importCompletionNode, sourceFile);
const quotedModuleSpecifier = quote(sourceFile, preferences, origin.moduleSpecifier);
const exportKind =
origin.isDefaultExport ? ExportKind.Default :
@@ -757,13 +780,16 @@ namespace ts.Completions {
ExportKind.Named;
const tabStop = preferences.includeCompletionsWithSnippetText ? "$1" : "";
const importKind = codefix.getImportKind(sourceFile, exportKind, options, /*forceImportKeyword*/ true);
const typeOnlyPrefix = isTypeOnly ? ` ${tokenToString(SyntaxKind.TypeKeyword)} ` : " ";
const isTopLevelTypeOnly = tryCast(importCompletionNode, isImportDeclaration)?.importClause?.isTypeOnly || tryCast(importCompletionNode, isImportEqualsDeclaration)?.isTypeOnly;
const isImportSpecifierTypeOnly = couldBeTypeOnlyImportSpecifier(importCompletionNode, contextToken);
const topLevelTypeOnlyText = isTopLevelTypeOnly ? ` ${tokenToString(SyntaxKind.TypeKeyword)} ` : " ";
const importSpecifierTypeOnlyText = isImportSpecifierTypeOnly ? `${tokenToString(SyntaxKind.TypeKeyword)} ` : "";
const suffix = useSemicolons ? ";" : "";
switch (importKind) {
case ImportKind.CommonJS: return { replacementSpan, insertText: `import${typeOnlyPrefix}${escapeSnippetText(name)}${tabStop} = require(${quotedModuleSpecifier})${suffix}` };
case ImportKind.Default: return { replacementSpan, insertText: `import${typeOnlyPrefix}${escapeSnippetText(name)}${tabStop} from ${quotedModuleSpecifier}${suffix}` };
case ImportKind.Namespace: return { replacementSpan, insertText: `import${typeOnlyPrefix}* as ${escapeSnippetText(name)} from ${quotedModuleSpecifier}${suffix}` };
case ImportKind.Named: return { replacementSpan, insertText: `import${typeOnlyPrefix}{ ${escapeSnippetText(name)}${tabStop} } from ${quotedModuleSpecifier}${suffix}` };
case ImportKind.CommonJS: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}${escapeSnippetText(name)}${tabStop} = require(${quotedModuleSpecifier})${suffix}` };
case ImportKind.Default: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}${escapeSnippetText(name)}${tabStop} from ${quotedModuleSpecifier}${suffix}` };
case ImportKind.Namespace: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}* as ${escapeSnippetText(name)} from ${quotedModuleSpecifier}${suffix}` };
case ImportKind.Named: return { replacementSpan, insertText: `import${topLevelTypeOnlyText}{ ${importSpecifierTypeOnlyText}${escapeSnippetText(name)}${tabStop} } from ${quotedModuleSpecifier}${suffix}` };
}
}
@@ -795,6 +821,7 @@ namespace ts.Completions {
export function getCompletionEntriesFromSymbols(
symbols: readonly Symbol[],
entries: Push<CompletionEntry>,
replacementToken: Node | undefined,
contextToken: Node | undefined,
location: Node,
sourceFile: SourceFile,
@@ -816,7 +843,6 @@ namespace ts.Completions {
const start = timestamp();
const variableDeclaration = getVariableDeclaration(location);
const useSemicolons = probablyUsesSemicolons(sourceFile);
const isTypeOnlyImport = !!importCompletionNode && isTypeOnlyImportOrExportDeclaration(location.parent);
// Tracks unique names.
// Value is set to false for global variables or completions from external module exports, because we can have multiple of those;
// true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports.
@@ -836,6 +862,7 @@ namespace ts.Completions {
const entry = createCompletionEntry(
symbol,
sortText,
replacementToken,
contextToken,
location,
sourceFile,
@@ -847,7 +874,6 @@ namespace ts.Completions {
propertyAccessToConvert,
isJsxInitializer,
importCompletionNode,
isTypeOnlyImport,
useSemicolons,
compilerOptions,
preferences
@@ -1050,7 +1076,7 @@ namespace ts.Completions {
case CompletionDataKind.JsDocParameterName:
return JsDoc.getJSDocParameterNameCompletionDetails(name);
case CompletionDataKind.Keywords:
return request.keywords.indexOf(stringToToken(name)!) > -1 ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined;
return some(request.keywordCompletions, c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined;
default:
return Debug.assertNever(request);
}
@@ -1107,7 +1133,7 @@ namespace ts.Completions {
): CodeActionsAndSourceDisplay {
if (data?.moduleSpecifier) {
const { contextToken, previousToken } = getRelevantTokens(position, sourceFile);
if (previousToken && getImportCompletionNode(contextToken || previousToken)) {
if (previousToken && getImportStatementCompletionInfo(contextToken || previousToken).replacementNode) {
// Import statement completion: 'import c|'
return { codeActions: undefined, sourceDisplay: [textPart(data.moduleSpecifier)] };
}
@@ -1164,6 +1190,7 @@ namespace ts.Completions {
readonly symbolToOriginInfoMap: SymbolOriginInfoMap;
readonly recommendedCompletion: Symbol | undefined;
readonly previousToken: Node | undefined;
readonly contextToken: Node | undefined;
readonly isJsxInitializer: IsJsxInitializer;
readonly insideJsDocTagTypeExpression: boolean;
readonly symbolToSortTextIdMap: SymbolSortTextIdMap;
@@ -1176,7 +1203,7 @@ namespace ts.Completions {
type Request =
| { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag }
| { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag }
| { readonly kind: CompletionDataKind.Keywords, keywords: readonly SyntaxKind[] };
| { readonly kind: CompletionDataKind.Keywords, keywordCompletions: readonly CompletionEntry[], isNewIdentifierLocation: boolean };
export const enum CompletionKind {
ObjectPropertyDeclaration,
@@ -1332,6 +1359,7 @@ namespace ts.Completions {
start = timestamp();
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
// Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file
const isJsOnlyLocation = !insideJsDocTagTypeExpression && isSourceFileJS(sourceFile);
const tokens = getRelevantTokens(position, sourceFile);
const previousToken = tokens.previousToken!;
let contextToken = tokens.contextToken!;
@@ -1350,23 +1378,35 @@ namespace ts.Completions {
let isJsxIdentifierExpected = false;
let importCompletionNode: Node | undefined;
let location = getTouchingPropertyName(sourceFile, position);
let keywordFilters = KeywordCompletionFilters.None;
let isNewIdentifierLocation = false;
if (contextToken) {
const importCompletionCandidate = getImportCompletionNode(contextToken);
if (importCompletionCandidate === SyntaxKind.FromKeyword) {
return { kind: CompletionDataKind.Keywords, keywords: [SyntaxKind.FromKeyword] };
const importStatementCompletion = getImportStatementCompletionInfo(contextToken);
isNewIdentifierLocation = importStatementCompletion.isNewIdentifierLocation;
if (importStatementCompletion.keywordCompletion) {
if (importStatementCompletion.isKeywordOnlyCompletion) {
return {
kind: CompletionDataKind.Keywords,
keywordCompletions: [keywordToCompletionEntry(importStatementCompletion.keywordCompletion)],
isNewIdentifierLocation,
};
}
keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletion.keywordCompletion);
}
// Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier`
// added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature
// is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients
// to opt in with the `includeCompletionsForImportStatements` user preference.
if (importCompletionCandidate && preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) {
importCompletionNode = importCompletionCandidate;
if (importStatementCompletion.replacementNode && preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) {
// Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier`
// added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature
// is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients
// to opt in with the `includeCompletionsForImportStatements` user preference.
importCompletionNode = importStatementCompletion.replacementNode;
}
// Bail out if this is a known invalid completion location
if (!importCompletionNode && isCompletionListBlocker(contextToken)) {
log("Returning an empty list because completion was requested in an invalid position.");
return undefined;
return keywordFilters
? keywordCompletionData(keywordFilters, isJsOnlyLocation, isNewIdentifierDefinitionLocation())
: undefined;
}
let parent = contextToken.parent;
@@ -1495,10 +1535,8 @@ namespace ts.Completions {
const semanticStart = timestamp();
let completionKind = CompletionKind.None;
let isNewIdentifierLocation = false;
let isNonContextualObjectLiteral = false;
let hasUnresolvedAutoImports = false;
let keywordFilters = KeywordCompletionFilters.None;
// This also gets mutated in nested-functions after the return
let symbols: Symbol[] = [];
const symbolToOriginInfoMap: SymbolOriginInfoMap = [];
@@ -1533,7 +1571,9 @@ namespace ts.Completions {
// global symbols in scope. These results should be valid for either language as
// the set of symbols that can be referenced from this location.
if (!tryGetGlobalSymbols()) {
return undefined;
return keywordFilters
? keywordCompletionData(keywordFilters, isJsOnlyLocation, isNewIdentifierLocation)
: undefined;
}
}
@@ -1558,6 +1598,7 @@ namespace ts.Completions {
symbolToOriginInfoMap,
recommendedCompletion,
previousToken,
contextToken,
isJsxInitializer,
insideJsDocTagTypeExpression,
symbolToSortTextIdMap,
@@ -1971,10 +2012,18 @@ namespace ts.Completions {
return;
}
const moduleSpecifierCache = host.getModuleSpecifierCache?.();
const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : "";
const exportInfo = getExportInfoMap(sourceFile, host, program, cancellationToken);
// import { type | -> token text should be blank
const isAfterTypeOnlyImportSpecifierModifier = previousToken === contextToken
&& importCompletionNode
&& couldBeTypeOnlyImportSpecifier(importCompletionNode, contextToken);
const lowerCaseTokenText =
isAfterTypeOnlyImportSpecifierModifier ? "" :
previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() :
"";
const moduleSpecifierCache = host.getModuleSpecifierCache?.();
const exportInfo = getExportInfoMap(sourceFile, host, program, cancellationToken);
const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.();
const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host);
resolvingModuleSpecifiers(
@@ -2130,6 +2179,7 @@ namespace ts.Completions {
case SyntaxKind.ModuleKeyword: // module |
case SyntaxKind.NamespaceKeyword: // namespace |
case SyntaxKind.ImportKeyword: // import |
return true;
case SyntaxKind.DotToken:
@@ -2292,22 +2342,42 @@ namespace ts.Completions {
* Relevant symbols are stored in the captured 'symbols' variable.
*/
function tryGetImportOrExportClauseCompletionSymbols(): GlobalsSearch {
// `import { |` or `import { a as 0, | }`
const namedImportsOrExports = contextToken && (contextToken.kind === SyntaxKind.OpenBraceToken || contextToken.kind === SyntaxKind.CommaToken)
? tryCast(contextToken.parent, isNamedImportsOrExports) : undefined;
if (!contextToken) return GlobalsSearch.Continue;
// `import { |` or `import { a as 0, | }` or `import { type | }`
const namedImportsOrExports =
contextToken.kind === SyntaxKind.OpenBraceToken || contextToken.kind === SyntaxKind.CommaToken ? tryCast(contextToken.parent, isNamedImportsOrExports) :
isTypeKeywordTokenOrIdentifier(contextToken) ? tryCast(contextToken.parent.parent, isNamedImportsOrExports) : undefined;
if (!namedImportsOrExports) return GlobalsSearch.Continue;
// We can at least offer `type` at `import { |`
if (!isTypeKeywordTokenOrIdentifier(contextToken)) {
keywordFilters = KeywordCompletionFilters.TypeKeyword;
}
// try to show exported member for imported/re-exported module
const { moduleSpecifier } = namedImportsOrExports.kind === SyntaxKind.NamedImports ? namedImportsOrExports.parent.parent : namedImportsOrExports.parent;
if (!moduleSpecifier) return namedImportsOrExports.kind === SyntaxKind.NamedImports ? GlobalsSearch.Fail : GlobalsSearch.Continue;
if (!moduleSpecifier) {
isNewIdentifierLocation = true;
return namedImportsOrExports.kind === SyntaxKind.NamedImports ? GlobalsSearch.Fail : GlobalsSearch.Continue;
}
const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier); // TODO: GH#18217
if (!moduleSpecifierSymbol) return GlobalsSearch.Fail;
if (!moduleSpecifierSymbol) {
isNewIdentifierLocation = true;
return GlobalsSearch.Fail;
}
completionKind = CompletionKind.MemberLike;
isNewIdentifierLocation = false;
const exports = typeChecker.getExportsAndPropertiesOfModule(moduleSpecifierSymbol);
const existing = new Set((namedImportsOrExports.elements as NodeArray<ImportOrExportSpecifier>).filter(n => !isCurrentlyEditingNode(n)).map(n => (n.propertyName || n.name).escapedText));
symbols = concatenate(symbols, exports.filter(e => e.escapedName !== InternalSymbolName.Default && !existing.has(e.escapedName)));
const uniques = exports.filter(e => e.escapedName !== InternalSymbolName.Default && !existing.has(e.escapedName));
symbols = concatenate(symbols, uniques);
if (!uniques.length) {
// If there's nothing else to import, don't offer `type` either
keywordFilters = KeywordCompletionFilters.None;
}
return GlobalsSearch.Success;
}
@@ -2597,6 +2667,16 @@ namespace ts.Completions {
case SyntaxKind.SetKeyword:
return !isFromObjectTypeDeclaration(contextToken);
case SyntaxKind.Identifier:
if (containingNodeKind === SyntaxKind.ImportSpecifier &&
contextToken === (parent as ImportSpecifier).name &&
(contextToken as Identifier).text === "type"
) {
// import { type | }
return false;
}
break;
case SyntaxKind.ClassKeyword:
case SyntaxKind.EnumKeyword:
case SyntaxKind.InterfaceKeyword:
@@ -2606,9 +2686,12 @@ namespace ts.Completions {
case SyntaxKind.LetKeyword:
case SyntaxKind.ConstKeyword:
case SyntaxKind.InferKeyword:
case SyntaxKind.TypeKeyword: // type htm|
return true;
case SyntaxKind.TypeKeyword:
// import { type foo| }
return containingNodeKind !== SyntaxKind.ImportSpecifier;
case SyntaxKind.AsteriskToken:
return isFunctionLike(contextToken.parent) && !isMethodDeclaration(contextToken.parent);
}
@@ -3014,6 +3097,8 @@ namespace ts.Completions {
return isTypeKeyword(kind) || kind === SyntaxKind.ConstKeyword;
case KeywordCompletionFilters.TypeKeywords:
return isTypeKeyword(kind);
case KeywordCompletionFilters.TypeKeyword:
return kind === SyntaxKind.TypeKeyword;
default:
return Debug.assertNever(keywordFilter);
}
@@ -3311,36 +3396,81 @@ namespace ts.Completions {
return undefined;
}
function getImportCompletionNode(contextToken: Node) {
interface ImportStatementCompletionInfo {
isKeywordOnlyCompletion: boolean;
keywordCompletion: TokenSyntaxKind | undefined;
isNewIdentifierLocation: boolean;
replacementNode: ImportEqualsDeclaration | ImportDeclaration | ImportSpecifier | Token<SyntaxKind.ImportKeyword> | undefined;
}
function getImportStatementCompletionInfo(contextToken: Node): ImportStatementCompletionInfo {
let keywordCompletion: TokenSyntaxKind | undefined;
let isKeywordOnlyCompletion = false;
const candidate = getCandidate();
return candidate === SyntaxKind.FromKeyword || candidate && rangeIsOnSingleLine(candidate, candidate.getSourceFile()) ? candidate : undefined;
return {
isKeywordOnlyCompletion,
keywordCompletion,
isNewIdentifierLocation: !!(candidate || keywordCompletion === SyntaxKind.TypeKeyword),
replacementNode: candidate && rangeIsOnSingleLine(candidate, candidate.getSourceFile())
? candidate
: undefined
};
function getCandidate() {
const parent = contextToken.parent;
if (isImportEqualsDeclaration(parent)) {
keywordCompletion = contextToken.kind === SyntaxKind.TypeKeyword ? undefined : SyntaxKind.TypeKeyword;
return isModuleSpecifierMissingOrEmpty(parent.moduleReference) ? parent : undefined;
}
if (couldBeTypeOnlyImportSpecifier(parent, contextToken) && canCompleteFromNamedBindings(parent.parent)) {
return parent;
}
if (isNamedImports(parent) || isNamespaceImport(parent)) {
if (isModuleSpecifierMissingOrEmpty(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) && !parent.parent.name) {
if (!parent.parent.isTypeOnly && (
contextToken.kind === SyntaxKind.OpenBraceToken ||
contextToken.kind === SyntaxKind.ImportKeyword ||
contextToken.kind === SyntaxKind.CommaToken
)) {
keywordCompletion = SyntaxKind.TypeKeyword;
}
if (canCompleteFromNamedBindings(parent)) {
// At `import { ... } |` or `import * as Foo |`, the only possible completion is `from`
return contextToken.kind === SyntaxKind.CloseBraceToken || contextToken.kind === SyntaxKind.Identifier
? SyntaxKind.FromKeyword
: parent.parent.parent;
if (contextToken.kind === SyntaxKind.CloseBraceToken || contextToken.kind === SyntaxKind.Identifier) {
isKeywordOnlyCompletion = true;
keywordCompletion = SyntaxKind.FromKeyword;
}
else {
return parent.parent.parent;
}
}
return undefined;
}
if (isImportKeyword(contextToken) && isSourceFile(parent)) {
// A lone import keyword with nothing following it does not parse as a statement at all
keywordCompletion = SyntaxKind.TypeKeyword;
return contextToken as Token<SyntaxKind.ImportKeyword>;
}
if (isImportKeyword(contextToken) && isImportDeclaration(parent)) {
// `import s| from`
keywordCompletion = SyntaxKind.TypeKeyword;
return isModuleSpecifierMissingOrEmpty(parent.moduleSpecifier) ? parent : undefined;
}
return undefined;
}
}
function couldBeTypeOnlyImportSpecifier(importSpecifier: Node, contextToken: Node | undefined): importSpecifier is ImportSpecifier {
return isImportSpecifier(importSpecifier)
&& (importSpecifier.isTypeOnly || contextToken === importSpecifier.name && isTypeKeywordTokenOrIdentifier(contextToken));
}
function canCompleteFromNamedBindings(namedBindings: NamedImportBindings) {
return isModuleSpecifierMissingOrEmpty(namedBindings.parent.parent.moduleSpecifier)
&& (isNamespaceImport(namedBindings) || namedBindings.elements.length < 2)
&& !namedBindings.parent.name;
}
function isModuleSpecifierMissingOrEmpty(specifier: ModuleReference | Expression) {
if (nodeIsMissing(specifier)) return true;
return !tryCast(isExternalModuleReference(specifier) ? specifier.expression : specifier, isStringLiteralLike)?.text;
@@ -3426,3 +3556,4 @@ namespace ts.Completions {
return charCode;
}
}
+3 -2
View File
@@ -232,7 +232,7 @@ namespace ts.OrganizeImports {
else {
for (const defaultImport of defaultImports) {
newImportSpecifiers.push(
factory.createImportSpecifier(factory.createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217
factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217
}
}
@@ -416,7 +416,8 @@ namespace ts.OrganizeImports {
}
export function compareImportOrExportSpecifiers<T extends ImportOrExportSpecifier>(s1: T, s2: T) {
return compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name)
return compareBooleans(s1.isTypeOnly, s2.isTypeOnly)
|| compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name)
|| compareIdentifiers(s1.name, s2.name);
}
+2 -2
View File
@@ -252,10 +252,10 @@ namespace ts.refactor {
}
function makeImportSpecifier(propertyName: string, name: string): ImportSpecifier {
return factory.createImportSpecifier(propertyName === name ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name));
return factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name));
}
function makeExportSpecifier(propertyName: string, name: string): ExportSpecifier {
return factory.createExportSpecifier(propertyName === name ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name));
return factory.createExportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name));
}
}
+2 -2
View File
@@ -116,7 +116,7 @@ namespace ts.refactor {
const importSpecifiers: ImportSpecifier[] = [];
exportNameToImportName.forEach((name, propertyName) => {
importSpecifiers.push(factory.createImportSpecifier(name === propertyName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name)));
importSpecifiers.push(factory.createImportSpecifier(/*isTypeOnly*/ false, name === propertyName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name)));
});
const importDecl = toConvert.parent.parent;
@@ -191,7 +191,7 @@ namespace ts.refactor {
changes.replaceNode(sourceFile, toConvert, factory.createNamespaceImport(factory.createIdentifier(namespaceImportName)));
if (neededNamedImports.size) {
const newNamedImports: ImportSpecifier[] = arrayFrom(neededNamedImports.values()).map(element =>
factory.createImportSpecifier(element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text)));
factory.createImportSpecifier(element.isTypeOnly, element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text)));
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, newNamedImports));
}
}
+1 -1
View File
@@ -319,7 +319,7 @@ namespace ts.refactor {
function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
path = ensurePathIsNonModuleName(path);
if (useEs6Imports) {
const specifiers = imports.map(i => factory.createImportSpecifier(/*propertyName*/ undefined, factory.createIdentifier(i)));
const specifiers = imports.map(i => factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, factory.createIdentifier(i)));
return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference);
}
else {
+1
View File
@@ -27,6 +27,7 @@ namespace ts.Completions.StringCompletions {
completion.symbols,
entries,
contextToken,
contextToken,
sourceFile,
sourceFile,
checker,
+4
View File
@@ -1795,6 +1795,10 @@ namespace ts {
return node.kind === SyntaxKind.TypeKeyword;
}
export function isTypeKeywordTokenOrIdentifier(node: Node) {
return isTypeKeywordToken(node) || isIdentifier(node) && node.text === "type";
}
/** True if the symbol is for an external module, as opposed to a namespace. */
export function isExternalModuleSymbol(moduleSymbol: Symbol): boolean {
return !!(moduleSymbol.flags & SymbolFlags.Module) && moduleSymbol.name.charCodeAt(0) === CharacterCodes.doubleQuote;