From e160bc8c0df502dcf415f0cd625c6680ff089512 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 27 Sep 2021 12:38:30 -0700 Subject: [PATCH] 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 * 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 --- src/compiler/checker.ts | 63 ++- src/compiler/diagnosticMessages.json | 8 + src/compiler/emitter.ts | 4 + src/compiler/factory/nodeFactory.ts | 22 +- src/compiler/factory/utilities.ts | 4 +- src/compiler/parser.ts | 75 ++- src/compiler/transformers/declarations.ts | 2 +- src/compiler/transformers/jsx.ts | 2 +- .../transformers/module/esnextAnd2015.ts | 6 +- src/compiler/transformers/ts.ts | 33 +- src/compiler/types.ts | 13 +- src/compiler/utilities.ts | 4 - src/compiler/utilitiesPublic.ts | 2 +- src/compiler/visitorPublic.ts | 2 + src/services/codefixes/convertToEs6Module.ts | 8 +- src/services/codefixes/helpers.ts | 2 +- src/services/codefixes/importFixes.ts | 448 +++++++++++++----- src/services/codefixes/requireInTs.ts | 2 +- src/services/completions.ts | 241 +++++++--- src/services/organizeImports.ts | 5 +- src/services/refactors/convertExport.ts | 4 +- src/services/refactors/convertImport.ts | 4 +- src/services/refactors/moveToNewFile.ts | 2 +- src/services/stringCompletions.ts | 1 + src/services/utilities.ts | 4 + src/testRunner/compilerRunner.ts | 2 +- .../unittests/services/organizeImports.ts | 7 + .../unittests/services/textChanges.ts | 20 +- src/testRunner/unittests/transform.ts | 2 +- .../reference/api/tsserverlibrary.d.ts | 18 +- tests/baselines/reference/api/typescript.d.ts | 18 +- .../reference/exportSpecifiers.errors.txt | 40 ++ tests/baselines/reference/exportSpecifiers.js | 48 ++ .../reference/exportSpecifiers.symbols | 54 +++ .../reference/exportSpecifiers.types | 57 +++ .../reference/importSpecifiers1.errors.txt | 108 +++++ .../baselines/reference/importSpecifiers1.js | 97 ++++ .../reference/importSpecifiers1.symbols | 93 ++++ .../reference/importSpecifiers1.types | 104 ++++ ...erveValueImports_importsNotUsedAsValues.js | 20 + ...alueImports_importsNotUsedAsValues.symbols | 22 + ...eValueImports_importsNotUsedAsValues.types | 22 + ...eserveValueImports_mixedImports.errors.txt | 15 + .../preserveValueImports_mixedImports.js | 19 + .../preserveValueImports_mixedImports.symbols | 17 + .../preserveValueImports_mixedImports.types | 16 + .../typeOnly/exportSpecifiers.ts | 21 + .../typeOnly/importSpecifiers1.ts | 46 ++ ...erveValueImports_importsNotUsedAsValues.ts | 14 + .../preserveValueImports_mixedImports.ts | 13 + .../completionInNamedImportLocation.ts | 16 +- .../completionListForExportEquals.ts | 2 +- .../completionListForExportEquals2.ts | 2 +- .../completionListInExportClause01.ts | 8 +- .../completionListInExportClause02.ts | 2 +- .../completionListInExportClause03.ts | 2 +- .../completionListInImportClause01.ts | 16 +- .../completionListInImportClause02.ts | 2 +- .../completionListInImportClause03.ts | 2 +- .../completionListInImportClause04.ts | 2 +- .../completionListIsGlobalCompletion.ts | 5 +- tests/cases/fourslash/completionsInExport.ts | 11 +- .../fourslash/completionsInExport_invalid.ts | 2 +- .../completionsInExport_moduleBlock.ts | 11 +- .../importNameCodeFix_importType1.ts | 28 ++ .../importNameCodeFix_importType2.ts | 51 ++ .../importNameCodeFix_importType3.ts | 18 + .../importNameCodeFix_importType4.ts | 18 + .../fourslash/importStatementCompletions1.ts | 59 +-- ...rtStatementCompletions_esModuleInterop1.ts | 3 + ...rtStatementCompletions_esModuleInterop2.ts | 3 + ...rtStatementCompletions_noPatternAmbient.ts | 5 +- .../importStatementCompletions_noSnippet.ts | 3 + .../importStatementCompletions_quotes.ts | 3 + .../importStatementCompletions_semicolons.ts | 3 + .../cases/fourslash/importTypeCompletions7.ts | 3 + .../cases/fourslash/importTypeCompletions8.ts | 26 + .../cases/fourslash/importTypeCompletions9.ts | 26 + .../importStatementCompletions_pnpm1.ts | 3 + ...portStatementCompletions_pnpmTransitive.ts | 2 +- 80 files changed, 1842 insertions(+), 349 deletions(-) create mode 100644 tests/baselines/reference/exportSpecifiers.errors.txt create mode 100644 tests/baselines/reference/exportSpecifiers.js create mode 100644 tests/baselines/reference/exportSpecifiers.symbols create mode 100644 tests/baselines/reference/exportSpecifiers.types create mode 100644 tests/baselines/reference/importSpecifiers1.errors.txt create mode 100644 tests/baselines/reference/importSpecifiers1.js create mode 100644 tests/baselines/reference/importSpecifiers1.symbols create mode 100644 tests/baselines/reference/importSpecifiers1.types create mode 100644 tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.js create mode 100644 tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.symbols create mode 100644 tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.types create mode 100644 tests/baselines/reference/preserveValueImports_mixedImports.errors.txt create mode 100644 tests/baselines/reference/preserveValueImports_mixedImports.js create mode 100644 tests/baselines/reference/preserveValueImports_mixedImports.symbols create mode 100644 tests/baselines/reference/preserveValueImports_mixedImports.types create mode 100644 tests/cases/conformance/externalModules/typeOnly/exportSpecifiers.ts create mode 100644 tests/cases/conformance/externalModules/typeOnly/importSpecifiers1.ts create mode 100644 tests/cases/conformance/externalModules/typeOnly/preserveValueImports_importsNotUsedAsValues.ts create mode 100644 tests/cases/conformance/externalModules/typeOnly/preserveValueImports_mixedImports.ts create mode 100644 tests/cases/fourslash/importNameCodeFix_importType1.ts create mode 100644 tests/cases/fourslash/importNameCodeFix_importType2.ts create mode 100644 tests/cases/fourslash/importNameCodeFix_importType3.ts create mode 100644 tests/cases/fourslash/importNameCodeFix_importType4.ts create mode 100644 tests/cases/fourslash/importTypeCompletions8.ts create mode 100644 tests/cases/fourslash/importTypeCompletions9.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 364f0de7f6..3340ad1bbc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -721,6 +721,7 @@ namespace ts { getLocalTypeParametersOfClassOrInterfaceOrTypeAlias, isDeclarationVisible, isPropertyAccessible, + getTypeOnlyAliasDeclaration, }; function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): Signature | undefined { @@ -2203,8 +2204,7 @@ namespace ts { if (!isValidTypeOnlyAliasUseSite(useSite)) { const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(symbol); if (typeOnlyDeclaration) { - const isExport = typeOnlyDeclarationIsExport(typeOnlyDeclaration); - const message = isExport + const message = typeOnlyDeclaration.kind === SyntaxKind.ExportSpecifier ? Diagnostics._0_cannot_be_used_as_a_value_because_it_was_exported_using_export_type : Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type; const unescapedName = unescapeLeadingUnderscores(name); @@ -2222,7 +2222,7 @@ namespace ts { diagnostic, createDiagnosticForNode( typeOnlyDeclaration, - typeOnlyDeclarationIsExport(typeOnlyDeclaration) ? Diagnostics._0_was_exported_here : Diagnostics._0_was_imported_here, + typeOnlyDeclaration.kind === SyntaxKind.ExportSpecifier ? Diagnostics._0_was_exported_here : Diagnostics._0_was_imported_here, unescapedName)); } @@ -2590,7 +2590,7 @@ namespace ts { function checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node: ImportEqualsDeclaration, resolved: Symbol | undefined) { if (markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false) && !node.isTypeOnly) { const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(getSymbolOfNode(node))!; - const isExport = typeOnlyDeclarationIsExport(typeOnlyDeclaration); + const isExport = typeOnlyDeclaration.kind === SyntaxKind.ExportSpecifier; const message = isExport ? Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_exported_using_export_type : Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_imported_using_import_type; @@ -2598,9 +2598,7 @@ namespace ts { ? Diagnostics._0_was_exported_here : Diagnostics._0_was_imported_here; - // Non-null assertion is safe because the optionality comes from ImportClause, - // but if an ImportClause was the typeOnlyDeclaration, it had to have a `name`. - const name = unescapeLeadingUnderscores(typeOnlyDeclaration.name!.escapedText); + const name = unescapeLeadingUnderscores(typeOnlyDeclaration.name.escapedText); addRelatedInfo(error(node.moduleReference, message), createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, name)); } } @@ -3054,13 +3052,13 @@ namespace ts { * and issue an error if so. * * @param aliasDeclaration The alias declaration not marked as type-only + * @param immediateTarget The symbol to which the alias declaration immediately resolves + * @param finalTarget The symbol to which the alias declaration ultimately resolves + * @param overwriteEmpty Checks `resolvesToSymbol` for type-only declarations even if `aliasDeclaration` * has already been marked as not resolving to a type-only alias. Used when recursively resolving qualified * names of import aliases, e.g. `import C = a.b.C`. If namespace `a` is not found to be type-only, the * import declaration will initially be marked as not resolving to a type-only symbol. But, namespace `b` * must still be checked for a type-only marker, overwriting the previous negative result if found. - * @param immediateTarget The symbol to which the alias declaration immediately resolves - * @param finalTarget The symbol to which the alias declaration ultimately resolves - * @param overwriteEmpty Checks `resolvesToSymbol` for type-only declarations even if `aliasDeclaration` */ function markSymbolOfAliasDeclarationIfTypeOnly( aliasDeclaration: Declaration | undefined, @@ -3094,7 +3092,7 @@ namespace ts { } /** Indicates that a symbol directly or indirectly resolves to a type-only import or export. */ - function getTypeOnlyAliasDeclaration(symbol: Symbol): TypeOnlyCompatibleAliasDeclaration | undefined { + function getTypeOnlyAliasDeclaration(symbol: Symbol): TypeOnlyAliasDeclaration | undefined { if (!(symbol.flags & SymbolFlags.Alias)) { return undefined; } @@ -6536,7 +6534,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, - factory.createNamedExports(map(flatMap(excessExports, e => getNamesOfDeclaration(e)), id => factory.createExportSpecifier(/*alias*/ undefined, id))), + factory.createNamedExports(map(flatMap(excessExports, e => getNamesOfDeclaration(e)), id => factory.createExportSpecifier(/*isTypeOnly*/ false, /*alias*/ undefined, id))), /*moduleSpecifier*/ undefined )]) ) @@ -6794,7 +6792,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, - factory.createNamedExports([factory.createExportSpecifier(alias, localName)]) + factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, alias, localName)]) ), ModifierFlags.None ); @@ -6832,7 +6830,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, - factory.createNamedExports([factory.createExportSpecifier(name, localName)]) + factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, name, localName)]) ), ModifierFlags.None ); @@ -6892,7 +6890,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, - factory.createNamedExports([factory.createExportSpecifier(getInternalSymbolName(symbol, symbolName), symbolName)]) + factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, getInternalSymbolName(symbol, symbolName), symbolName)]) ), ModifierFlags.None); } } @@ -7031,7 +7029,7 @@ namespace ts { const target = aliasDecl && getTargetOfAliasDeclaration(aliasDecl, /*dontRecursivelyResolve*/ true); includePrivateSymbol(target || s); const targetName = target ? getInternalSymbolName(target, unescapeLeadingUnderscores(target.escapedName)) : localName; - return factory.createExportSpecifier(name === targetName ? undefined : targetName, name); + return factory.createExportSpecifier(/*isTypeOnly*/ false, name === targetName ? undefined : targetName, name); })) )]); addResult(factory.createModuleDeclaration( @@ -7137,7 +7135,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, - factory.createNamedExports([factory.createExportSpecifier(d.expression, factory.createIdentifier(InternalSymbolName.Default))]) + factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, d.expression, factory.createIdentifier(InternalSymbolName.Default))]) ) : d); const exportModifierStripped = every(defaultReplaced, d => hasSyntacticModifier(d, ModifierFlags.Export)) ? map(defaultReplaced, removeExportModifier) : defaultReplaced; fakespace = factory.updateModuleDeclaration( @@ -7313,6 +7311,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, factory.createNamedImports([factory.createImportSpecifier( + /*isTypeOnly*/ false, propertyName && isIdentifier(propertyName) ? factory.createIdentifier(idText(propertyName)) : undefined, factory.createIdentifier(localName) )])), @@ -7424,6 +7423,7 @@ namespace ts { /*importClause*/ undefined, factory.createNamedImports([ factory.createImportSpecifier( + /*isTypeOnly*/ false, localName !== verbatimTargetName ? factory.createIdentifier(verbatimTargetName) : undefined, factory.createIdentifier(localName) ) @@ -7470,7 +7470,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, - factory.createNamedExports([factory.createExportSpecifier(localName !== targetName ? targetName : undefined, localName)]), + factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, localName !== targetName ? targetName : undefined, localName)]), specifier ), ModifierFlags.None); } @@ -39415,11 +39415,15 @@ namespace ts { } function checkGrammarExportDeclaration(node: ExportDeclaration): boolean { - const isTypeOnlyExportStar = node.isTypeOnly && node.exportClause?.kind !== SyntaxKind.NamedExports; - if (isTypeOnlyExportStar) { - grammarErrorOnNode(node, Diagnostics.Only_named_exports_may_use_export_type); + if (node.isTypeOnly) { + if (node.exportClause?.kind === SyntaxKind.NamedExports) { + return checkGrammarNamedImportsOrExports(node.exportClause); + } + else { + return grammarErrorOnNode(node, Diagnostics.Only_named_exports_may_use_export_type); + } } - return !isTypeOnlyExportStar; + return false; } function checkGrammarModuleElementContext(node: Statement, errorMessage: DiagnosticMessage): boolean { @@ -43445,9 +43449,24 @@ namespace ts { if (node.isTypeOnly && node.name && node.namedBindings) { return grammarErrorOnNode(node, Diagnostics.A_type_only_import_can_specify_a_default_import_or_named_bindings_but_not_both); } + if (node.isTypeOnly && node.namedBindings?.kind === SyntaxKind.NamedImports) { + return checkGrammarNamedImportsOrExports(node.namedBindings); + } return false; } + function checkGrammarNamedImportsOrExports(namedBindings: NamedImportsOrExports): boolean { + return !!forEach(namedBindings.elements, specifier => { + if (specifier.isTypeOnly) { + return grammarErrorOnFirstToken( + specifier, + specifier.kind === SyntaxKind.ImportSpecifier + ? Diagnostics.The_type_modifier_cannot_be_used_on_a_named_import_when_import_type_is_used_on_its_import_statement + : Diagnostics.The_type_modifier_cannot_be_used_on_a_named_export_when_export_type_is_used_on_its_export_statement); + } + }); + } + function checkGrammarImportCallExpression(node: ImportCall): boolean { if (moduleKind === ModuleKind.ES2015) { return grammarErrorOnNode(node, Diagnostics.Dynamic_imports_are_only_supported_when_the_module_flag_is_set_to_es2020_esnext_commonjs_amd_system_umd_node12_or_nodenext); diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 084295b4ee..7359c776c5 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1434,6 +1434,14 @@ "code": 2205, "elidedInCompatabilityPyramid": true }, + "The 'type' modifier cannot be used on a named import when 'import type' is used on its import statement.": { + "category": "Error", + "code": 2206 + }, + "The 'type' modifier cannot be used on a named export when 'export type' is used on its export statement.": { + "category": "Error", + "code": 2207 + }, "Duplicate identifier '{0}'.": { "category": "Error", diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 06cdfc6daf..105f09117c 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -3441,6 +3441,10 @@ namespace ts { } function emitImportOrExportSpecifier(node: ImportOrExportSpecifier) { + if (node.isTypeOnly) { + writeKeyword("type"); + writeSpace(); + } if (node.propertyName) { emit(node.propertyName); writeSpace(); diff --git a/src/compiler/factory/nodeFactory.ts b/src/compiler/factory/nodeFactory.ts index 379c0d0929..b87d54e5da 100644 --- a/src/compiler/factory/nodeFactory.ts +++ b/src/compiler/factory/nodeFactory.ts @@ -4089,8 +4089,9 @@ namespace ts { } // @api - function createImportSpecifier(propertyName: Identifier | undefined, name: Identifier) { + function createImportSpecifier(isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) { const node = createBaseNode(SyntaxKind.ImportSpecifier); + node.isTypeOnly = isTypeOnly; node.propertyName = propertyName; node.name = name; node.transformFlags |= @@ -4101,10 +4102,11 @@ namespace ts { } // @api - function updateImportSpecifier(node: ImportSpecifier, propertyName: Identifier | undefined, name: Identifier) { - return node.propertyName !== propertyName + function updateImportSpecifier(node: ImportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) { + return node.isTypeOnly !== isTypeOnly + || node.propertyName !== propertyName || node.name !== name - ? update(createImportSpecifier(propertyName, name), node) + ? update(createImportSpecifier(isTypeOnly, propertyName, name), node) : node; } @@ -4205,8 +4207,9 @@ namespace ts { } // @api - function createExportSpecifier(propertyName: string | Identifier | undefined, name: string | Identifier) { + function createExportSpecifier(isTypeOnly: boolean, propertyName: string | Identifier | undefined, name: string | Identifier) { const node = createBaseNode(SyntaxKind.ExportSpecifier); + node.isTypeOnly = isTypeOnly; node.propertyName = asName(propertyName); node.name = asName(name); node.transformFlags |= @@ -4217,10 +4220,11 @@ namespace ts { } // @api - function updateExportSpecifier(node: ExportSpecifier, propertyName: Identifier | undefined, name: Identifier) { - return node.propertyName !== propertyName + function updateExportSpecifier(node: ExportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) { + return node.isTypeOnly !== isTypeOnly + || node.propertyName !== propertyName || node.name !== name - ? update(createExportSpecifier(propertyName, name), node) + ? update(createExportSpecifier(isTypeOnly, propertyName, name), node) : node; } @@ -5488,7 +5492,7 @@ namespace ts { /*modifiers*/ undefined, /*isTypeOnly*/ false, createNamedExports([ - createExportSpecifier(/*propertyName*/ undefined, exportName) + createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, exportName) ]) ); } diff --git a/src/compiler/factory/utilities.ts b/src/compiler/factory/utilities.ts index bfcaddba23..7daa31c522 100644 --- a/src/compiler/factory/utilities.ts +++ b/src/compiler/factory/utilities.ts @@ -500,8 +500,8 @@ namespace ts { // NOTE: We don't need to care about global import collisions as this is a module. namedBindings = nodeFactory.createNamedImports( map(helperNames, name => isFileLevelUniqueName(sourceFile, name) - ? nodeFactory.createImportSpecifier(/*propertyName*/ undefined, nodeFactory.createIdentifier(name)) - : nodeFactory.createImportSpecifier(nodeFactory.createIdentifier(name), helperFactory.getUnscopedHelperName(name)) + ? nodeFactory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, nodeFactory.createIdentifier(name)) + : nodeFactory.createImportSpecifier(/*isTypeOnly*/ false, nodeFactory.createIdentifier(name), helperFactory.getUnscopedHelperName(name)) ) ); const parseNode = getOriginalNode(sourceFile, isSourceFile); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 715e0cffa3..04bc8c4b33 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -7404,27 +7404,76 @@ namespace ts { let checkIdentifierIsKeyword = isKeyword(token()) && !isIdentifier(); let checkIdentifierStart = scanner.getTokenPos(); let checkIdentifierEnd = scanner.getTextPos(); - const identifierName = parseIdentifierName(); + let isTypeOnly = false; let propertyName: Identifier | undefined; - let name: Identifier; - if (token() === SyntaxKind.AsKeyword) { - propertyName = identifierName; - parseExpected(SyntaxKind.AsKeyword); - checkIdentifierIsKeyword = isKeyword(token()) && !isIdentifier(); - checkIdentifierStart = scanner.getTokenPos(); - checkIdentifierEnd = scanner.getTextPos(); - name = parseIdentifierName(); + let canParseAsKeyword = true; + let name = parseIdentifierName(); + if (name.escapedText === "type") { + // If the first token of an import specifier is 'type', there are a lot of possibilities, + // especially if we see 'as' afterwards: + // + // import { type } from "mod"; - isTypeOnly: false, name: type + // import { type as } from "mod"; - isTypeOnly: true, name: as + // import { type as as } from "mod"; - isTypeOnly: false, name: as, propertyName: type + // import { type as as as } from "mod"; - isTypeOnly: true, name: as, propertyName: as + if (token() === SyntaxKind.AsKeyword) { + // { type as ...? } + const firstAs = parseIdentifierName(); + if (token() === SyntaxKind.AsKeyword) { + // { type as as ...? } + const secondAs = parseIdentifierName(); + if (tokenIsIdentifierOrKeyword(token())) { + // { type as as something } + isTypeOnly = true; + propertyName = firstAs; + name = parseNameWithKeywordCheck(); + canParseAsKeyword = false; + } + else { + // { type as as } + propertyName = name; + name = secondAs; + canParseAsKeyword = false; + } + } + else if (tokenIsIdentifierOrKeyword(token())) { + // { type as something } + propertyName = name; + canParseAsKeyword = false; + name = parseNameWithKeywordCheck(); + } + else { + // { type as } + isTypeOnly = true; + name = firstAs; + } + } + else if (tokenIsIdentifierOrKeyword(token())) { + // { type something ...? } + isTypeOnly = true; + name = parseNameWithKeywordCheck(); + } } - else { - name = identifierName; + + if (canParseAsKeyword && token() === SyntaxKind.AsKeyword) { + propertyName = name; + parseExpected(SyntaxKind.AsKeyword); + name = parseNameWithKeywordCheck(); } if (kind === SyntaxKind.ImportSpecifier && checkIdentifierIsKeyword) { parseErrorAt(checkIdentifierStart, checkIdentifierEnd, Diagnostics.Identifier_expected); } const node = kind === SyntaxKind.ImportSpecifier - ? factory.createImportSpecifier(propertyName, name) - : factory.createExportSpecifier(propertyName, name); + ? factory.createImportSpecifier(isTypeOnly, propertyName, name) + : factory.createExportSpecifier(isTypeOnly, propertyName, name); return finishNode(node, pos); + + function parseNameWithKeywordCheck() { + checkIdentifierIsKeyword = isKeyword(token()) && !isIdentifier(); + checkIdentifierStart = scanner.getTokenPos(); + checkIdentifierEnd = scanner.getTextPos(); + return parseIdentifierName(); + } } function parseNamespaceExport(pos: number): NamespaceExport { diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index cc34205944..6cea3b8505 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -1256,7 +1256,7 @@ namespace ts { /*modifiers*/ undefined, /*isTypeOnly*/ false, factory.createNamedExports(map(exportMappings, ([gen, exp]) => { - return factory.createExportSpecifier(gen, exp); + return factory.createExportSpecifier(/*isTypeOnly*/ false, gen, exp); })) )); } diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts index fbae93c6e6..ef296d779a 100644 --- a/src/compiler/transformers/jsx.ts +++ b/src/compiler/transformers/jsx.ts @@ -56,7 +56,7 @@ namespace ts { currentFileState.utilizedImplicitRuntimeImports.set(importSource, specifierSourceImports); } const generatedName = factory.createUniqueName(`_${name}`, GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel | GeneratedIdentifierFlags.AllowNameSubstitution); - const specifier = factory.createImportSpecifier(factory.createIdentifier(name), generatedName); + const specifier = factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier(name), generatedName); generatedName.generatedImportReference = specifier; specifierSourceImports.set(name, specifier); return generatedName; diff --git a/src/compiler/transformers/module/esnextAnd2015.ts b/src/compiler/transformers/module/esnextAnd2015.ts index 19fbdfcebf..5e755e5083 100644 --- a/src/compiler/transformers/module/esnextAnd2015.ts +++ b/src/compiler/transformers/module/esnextAnd2015.ts @@ -104,7 +104,7 @@ namespace ts { /*isTypeOnly*/ false, /*name*/ undefined, factory.createNamedImports([ - factory.createImportSpecifier(factory.createIdentifier("createRequire"), createRequireName) + factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("createRequire"), createRequireName) ]) ), factory.createStringLiteral("module") @@ -177,7 +177,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, node.isTypeOnly, - factory.createNamedExports([factory.createExportSpecifier(/*propertyName*/ undefined, idText(node.name))]) + factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, idText(node.name))]) )); } return statements; @@ -220,7 +220,7 @@ namespace ts { /*decorators*/ undefined, /*modifiers*/ undefined, /*isTypeOnly*/ false, - factory.createNamedExports([factory.createExportSpecifier(synthName, oldIdentifier)]), + factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, synthName, oldIdentifier)]), ); setOriginalNode(exportDecl, node); diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index 9dae42e335..2e3e25b12e 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -2845,9 +2845,12 @@ namespace ts { return shouldEmitAliasDeclaration(node) ? node : undefined; } else { - // Elide named imports if all of its import specifiers are elided. + // Elide named imports if all of its import specifiers are elided and settings allow. + const allowEmpty = compilerOptions.preserveValueImports && ( + compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve || + compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error); const elements = visitNodes(node.elements, visitImportSpecifier, isImportSpecifier); - return some(elements) ? factory.updateNamedImports(node, elements) : undefined; + return allowEmpty || some(elements) ? factory.updateNamedImports(node, elements) : undefined; } } @@ -2857,7 +2860,7 @@ namespace ts { * @param node The import specifier node. */ function visitImportSpecifier(node: ImportSpecifier): VisitResult { - return shouldEmitAliasDeclaration(node) ? node : undefined; + return !node.isTypeOnly && shouldEmitAliasDeclaration(node) ? node : undefined; } /** @@ -2890,13 +2893,15 @@ namespace ts { return node; } - if (!resolver.isValueAliasDeclaration(node)) { - // Elide the export declaration if it does not export a value. - return undefined; - } - // Elide the export declaration if all of its named exports are elided. - const exportClause = visitNode(node.exportClause, visitNamedExportBindings, isNamedExportBindings); + const allowEmpty = !!node.moduleSpecifier && ( + compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve || + compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error); + const exportClause = visitNode( + node.exportClause, + (bindings: NamedExportBindings) => visitNamedExportBindings(bindings, allowEmpty), + isNamedExportBindings); + return exportClause ? factory.updateExportDeclaration( node, @@ -2915,18 +2920,18 @@ namespace ts { * * @param node The named exports node. */ - function visitNamedExports(node: NamedExports): VisitResult { + function visitNamedExports(node: NamedExports, allowEmpty: boolean): VisitResult { // Elide the named exports if all of its export specifiers were elided. const elements = visitNodes(node.elements, visitExportSpecifier, isExportSpecifier); - return some(elements) ? factory.updateNamedExports(node, elements) : undefined; + return allowEmpty || some(elements) ? factory.updateNamedExports(node, elements) : undefined; } function visitNamespaceExports(node: NamespaceExport): VisitResult { return factory.updateNamespaceExport(node, visitNode(node.name, visitor, isIdentifier)); } - function visitNamedExportBindings(node: NamedExportBindings): VisitResult { - return isNamespaceExport(node) ? visitNamespaceExports(node) : visitNamedExports(node); + function visitNamedExportBindings(node: NamedExportBindings, allowEmpty: boolean): VisitResult { + return isNamespaceExport(node) ? visitNamespaceExports(node) : visitNamedExports(node, allowEmpty); } /** @@ -2936,7 +2941,7 @@ namespace ts { */ function visitExportSpecifier(node: ExportSpecifier): VisitResult { // Elide an export specifier if it does not reference a value. - return resolver.isValueAliasDeclaration(node) ? node : undefined; + return !node.isTypeOnly && resolver.isValueAliasDeclaration(node) ? node : undefined; } /** diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d048f6897b..0bcca56036 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3112,11 +3112,13 @@ namespace ts { readonly parent: NamedImports; readonly propertyName?: Identifier; // Name preceding "as" keyword (or undefined when "as" is absent) readonly name: Identifier; // Declared name + readonly isTypeOnly: boolean; } export interface ExportSpecifier extends NamedDeclaration { readonly kind: SyntaxKind.ExportSpecifier; readonly parent: NamedExports; + readonly isTypeOnly: boolean; readonly propertyName?: Identifier; // Name preceding "as" keyword (or undefined when "as" is absent) readonly name: Identifier; // Declared name } @@ -4409,6 +4411,7 @@ namespace ts { /* @internal */ getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol: Symbol): readonly TypeParameter[] | undefined; /* @internal */ isDeclarationVisible(node: Declaration | AnyImportSyntax): boolean; /* @internal */ isPropertyAccessible(node: Node, isSuper: boolean, isWrite: boolean, containingType: Type, property: Symbol): boolean; + /* @internal */ getTypeOnlyAliasDeclaration(symbol: Symbol): TypeOnlyAliasDeclaration | undefined; } /* @internal */ @@ -4938,7 +4941,7 @@ namespace ts { deferralConstituents?: Type[]; // Calculated list of constituents for a deferred type deferralParent?: Type; // Source union/intersection of a deferred type cjsExportMerged?: Symbol; // Version of the symbol with all non export= exports merged with the export= target - typeOnlyDeclaration?: TypeOnlyCompatibleAliasDeclaration | false; // First resolved alias declaration that makes the symbol only usable in type constructs + typeOnlyDeclaration?: TypeOnlyAliasDeclaration | false; // First resolved alias declaration that makes the symbol only usable in type constructs isConstructorDeclaredProperty?: boolean; // Property declared through 'this.x = ...' assignment in constructor tupleLabelDeclaration?: NamedTupleMember | ParameterDeclaration; // Declaration associated with the tuple's label accessibleChainCache?: ESMap; @@ -7392,16 +7395,16 @@ namespace ts { updateNamespaceExport(node: NamespaceExport, name: Identifier): NamespaceExport; createNamedImports(elements: readonly ImportSpecifier[]): NamedImports; updateNamedImports(node: NamedImports, elements: readonly ImportSpecifier[]): NamedImports; - createImportSpecifier(propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; - updateImportSpecifier(node: ImportSpecifier, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; + createImportSpecifier(isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; + updateImportSpecifier(node: ImportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; createExportAssignment(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isExportEquals: boolean | undefined, expression: Expression): ExportAssignment; updateExportAssignment(node: ExportAssignment, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, expression: Expression): ExportAssignment; createExportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier?: Expression, assertClause?: AssertClause): ExportDeclaration; updateExportDeclaration(node: ExportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier: Expression | undefined, assertClause: AssertClause | undefined): ExportDeclaration; createNamedExports(elements: readonly ExportSpecifier[]): NamedExports; updateNamedExports(node: NamedExports, elements: readonly ExportSpecifier[]): NamedExports; - createExportSpecifier(propertyName: string | Identifier | undefined, name: string | Identifier): ExportSpecifier; - updateExportSpecifier(node: ExportSpecifier, propertyName: Identifier | undefined, name: Identifier): ExportSpecifier; + createExportSpecifier(isTypeOnly: boolean, propertyName: string | Identifier | undefined, name: string | Identifier): ExportSpecifier; + updateExportSpecifier(node: ExportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ExportSpecifier; /* @internal*/ createMissingDeclaration(): MissingDeclaration; // diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f433cff16c..6a2e5323b1 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7095,10 +7095,6 @@ namespace ts { || !(isExpressionNode(useSite) || isShorthandPropertyNameUseSite(useSite)); } - export function typeOnlyDeclarationIsExport(typeOnlyDeclaration: Node) { - return typeOnlyDeclaration.kind === SyntaxKind.ExportSpecifier; - } - function isShorthandPropertyNameUseSite(useSite: Node) { return isIdentifier(useSite) && isShorthandPropertyAssignment(useSite.parent) && useSite.parent.name === useSite; } diff --git a/src/compiler/utilitiesPublic.ts b/src/compiler/utilitiesPublic.ts index 78189ff0fc..d638ec32a8 100644 --- a/src/compiler/utilitiesPublic.ts +++ b/src/compiler/utilitiesPublic.ts @@ -1130,7 +1130,7 @@ namespace ts { switch (node.kind) { case SyntaxKind.ImportSpecifier: case SyntaxKind.ExportSpecifier: - return (node as ImportOrExportSpecifier).parent.parent.isTypeOnly; + return (node as ImportOrExportSpecifier).isTypeOnly || (node as ImportOrExportSpecifier).parent.parent.isTypeOnly; case SyntaxKind.NamespaceImport: return (node as NamespaceImport).parent.isTypeOnly; case SyntaxKind.ImportClause: diff --git a/src/compiler/visitorPublic.ts b/src/compiler/visitorPublic.ts index 511d3b5c7e..869d4604bd 100644 --- a/src/compiler/visitorPublic.ts +++ b/src/compiler/visitorPublic.ts @@ -1116,6 +1116,7 @@ namespace ts { case SyntaxKind.ImportSpecifier: Debug.type(node); return factory.updateImportSpecifier(node, + node.isTypeOnly, nodeVisitor(node.propertyName, visitor, isIdentifier), nodeVisitor(node.name, visitor, isIdentifier)); @@ -1144,6 +1145,7 @@ namespace ts { case SyntaxKind.ExportSpecifier: Debug.type(node); return factory.updateExportSpecifier(node, + node.isTypeOnly, nodeVisitor(node.propertyName, visitor, isIdentifier), nodeVisitor(node.name, visitor, isIdentifier)); diff --git a/src/services/codefixes/convertToEs6Module.ts b/src/services/codefixes/convertToEs6Module.ts index f01209290b..94f6dfe249 100644 --- a/src/services/codefixes/convertToEs6Module.ts +++ b/src/services/codefixes/convertToEs6Module.ts @@ -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 { diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index b734b85947..5cdf6fb478 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -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)); } } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 3fc29c64b9..96cb96c7ff 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -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; + } + 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(); - const newImports = new Map>(); + const importType: FixAddJsdocTypeImport[] = []; + /** Keys are import clause node IDs. */ + const addToExisting = new Map(); + + type NewImportsKey = `${0 | 1}|${string}`; + /** Use `getNewImportEntry` for access */ + const newImports = new Map>(); 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 { + // 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; 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); diff --git a/src/services/codefixes/requireInTs.ts b/src/services/codefixes/requireInTs.ts index 2dc09cc955..da3bbd0b25 100644 --- a/src/services/codefixes/requireInTs.ts +++ b/src/services/codefixes/requireInTs.ts @@ -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) { diff --git a/src/services/completions.ts b/src/services/completions.ts index 9db32f7a9e..ce8c310b77 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -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, + 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).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 | 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; } 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; } } + diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 243cda647e..5463cf0058 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -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(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); } diff --git a/src/services/refactors/convertExport.ts b/src/services/refactors/convertExport.ts index 24cbd09283..3737f74a09 100644 --- a/src/services/refactors/convertExport.ts +++ b/src/services/refactors/convertExport.ts @@ -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)); } } diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index abe3c02d95..65a684d8c4 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -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)); } } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 5fd34d348c..9e204bb383 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -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 { diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index d0771f2e97..91c886b189 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -27,6 +27,7 @@ namespace ts.Completions.StringCompletions { completion.symbols, entries, contextToken, + contextToken, sourceFile, sourceFile, checker, diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 696e8e0233..6d48f696a4 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -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; diff --git a/src/testRunner/compilerRunner.ts b/src/testRunner/compilerRunner.ts index b0c47694a7..46c96659bb 100644 --- a/src/testRunner/compilerRunner.ts +++ b/src/testRunner/compilerRunner.ts @@ -335,4 +335,4 @@ namespace Harness { return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }; } } -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 496f2d401d..f3b222bb76 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -206,6 +206,13 @@ namespace ts { assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); + it("Sort specifiers - type-only", () => { + const sortedImports = parseImports(`import { type z, y, type x, c, type b, a } from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { a, c, y, type b, type x, type z } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine namespace re-exports", () => { const sortedExports = parseExports( `export * from "lib";`, diff --git a/src/testRunner/unittests/services/textChanges.ts b/src/testRunner/unittests/services/textChanges.ts index cea6586c31..534d57ebf8 100644 --- a/src/testRunner/unittests/services/textChanges.ts +++ b/src/testRunner/unittests/services/textChanges.ts @@ -535,7 +535,7 @@ import { x } from "bar"`; runSingleFileTest("insertNodeInListAfter10", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(factory.createIdentifier("b"), factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("b"), factory.createIdentifier("a"))); }); } { @@ -544,7 +544,7 @@ import { x // this is x } from "bar"`; runSingleFileTest("insertNodeInListAfter11", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(factory.createIdentifier("b"), factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("b"), factory.createIdentifier("a"))); }); } { @@ -554,7 +554,7 @@ import { } from "bar"`; runSingleFileTest("insertNodeInListAfter12", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(undefined, factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, factory.createIdentifier("a"))); }); } { @@ -564,7 +564,7 @@ import { } from "bar"`; runSingleFileTest("insertNodeInListAfter13", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(undefined, factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, factory.createIdentifier("a"))); }); } { @@ -574,7 +574,7 @@ import { x } from "bar"`; runSingleFileTest("insertNodeInListAfter14", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(factory.createIdentifier("b"), factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("b"), factory.createIdentifier("a"))); }); } { @@ -584,7 +584,7 @@ import { x // this is x } from "bar"`; runSingleFileTest("insertNodeInListAfter15", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(factory.createIdentifier("b"), factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("b"), factory.createIdentifier("a"))); }); } { @@ -595,7 +595,7 @@ import { } from "bar"`; runSingleFileTest("insertNodeInListAfter16", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(undefined, factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, factory.createIdentifier("a"))); }); } { @@ -606,7 +606,7 @@ import { } from "bar"`; runSingleFileTest("insertNodeInListAfter17", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(undefined, factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, factory.createIdentifier("a"))); }); } { @@ -616,14 +616,14 @@ import { } from "bar"`; runSingleFileTest("insertNodeInListAfter18", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(undefined, factory.createIdentifier("a"))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, factory.createIdentifier("a"))); }); } { const runTest = (name: string, text: string) => runSingleFileTest(name, /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { for (const specifier of ["x3", "x4", "x5"]) { // eslint-disable-next-line boolean-trivia - changeTracker.insertNodeInListAfter(sourceFile, findChild("x2", sourceFile), factory.createImportSpecifier(undefined, factory.createIdentifier(specifier))); + changeTracker.insertNodeInListAfter(sourceFile, findChild("x2", sourceFile), factory.createImportSpecifier(/*isTypeOnly*/ false, undefined, factory.createIdentifier(specifier))); } }); diff --git a/src/testRunner/unittests/transform.ts b/src/testRunner/unittests/transform.ts index 6d8aa0afa4..476dc6540d 100644 --- a/src/testRunner/unittests/transform.ts +++ b/src/testRunner/unittests/transform.ts @@ -275,7 +275,7 @@ namespace ts { if (node.kind === SyntaxKind.ExportDeclaration) { const ed = node as Node as ExportDeclaration; const exports = [{ name: "x" }]; - const exportSpecifiers = exports.map(e => factory.createExportSpecifier(e.name, e.name)); + const exportSpecifiers = exports.map(e => factory.createExportSpecifier(/*isTypeOnly*/ false, e.name, e.name)); const exportClause = factory.createNamedExports(exportSpecifiers); const newEd = factory.updateExportDeclaration(ed, ed.decorators, ed.modifiers, ed.isTypeOnly, exportClause, ed.moduleSpecifier, ed.assertClause); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 05c90b4600..ba7c44184e 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1698,10 +1698,12 @@ declare namespace ts { readonly parent: NamedImports; readonly propertyName?: Identifier; readonly name: Identifier; + readonly isTypeOnly: boolean; } export interface ExportSpecifier extends NamedDeclaration { readonly kind: SyntaxKind.ExportSpecifier; readonly parent: NamedExports; + readonly isTypeOnly: boolean; readonly propertyName?: Identifier; readonly name: Identifier; } @@ -3578,16 +3580,16 @@ declare namespace ts { updateNamespaceExport(node: NamespaceExport, name: Identifier): NamespaceExport; createNamedImports(elements: readonly ImportSpecifier[]): NamedImports; updateNamedImports(node: NamedImports, elements: readonly ImportSpecifier[]): NamedImports; - createImportSpecifier(propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; - updateImportSpecifier(node: ImportSpecifier, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; + createImportSpecifier(isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; + updateImportSpecifier(node: ImportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; createExportAssignment(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isExportEquals: boolean | undefined, expression: Expression): ExportAssignment; updateExportAssignment(node: ExportAssignment, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, expression: Expression): ExportAssignment; createExportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier?: Expression, assertClause?: AssertClause): ExportDeclaration; updateExportDeclaration(node: ExportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier: Expression | undefined, assertClause: AssertClause | undefined): ExportDeclaration; createNamedExports(elements: readonly ExportSpecifier[]): NamedExports; updateNamedExports(node: NamedExports, elements: readonly ExportSpecifier[]): NamedExports; - createExportSpecifier(propertyName: string | Identifier | undefined, name: string | Identifier): ExportSpecifier; - updateExportSpecifier(node: ExportSpecifier, propertyName: Identifier | undefined, name: Identifier): ExportSpecifier; + createExportSpecifier(isTypeOnly: boolean, propertyName: string | Identifier | undefined, name: string | Identifier): ExportSpecifier; + updateExportSpecifier(node: ExportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ExportSpecifier; createExternalModuleReference(expression: Expression): ExternalModuleReference; updateExternalModuleReference(node: ExternalModuleReference, expression: Expression): ExternalModuleReference; createJSDocAllType(): JSDocAllType; @@ -11053,9 +11055,9 @@ declare namespace ts { /** @deprecated Use `factory.updateNamedImports` or the factory supplied by your transformation context instead. */ const updateNamedImports: (node: NamedImports, elements: readonly ImportSpecifier[]) => NamedImports; /** @deprecated Use `factory.createImportSpecifier` or the factory supplied by your transformation context instead. */ - const createImportSpecifier: (propertyName: Identifier | undefined, name: Identifier) => ImportSpecifier; + const createImportSpecifier: (isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) => ImportSpecifier; /** @deprecated Use `factory.updateImportSpecifier` or the factory supplied by your transformation context instead. */ - const updateImportSpecifier: (node: ImportSpecifier, propertyName: Identifier | undefined, name: Identifier) => ImportSpecifier; + const updateImportSpecifier: (node: ImportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) => ImportSpecifier; /** @deprecated Use `factory.createExportAssignment` or the factory supplied by your transformation context instead. */ const createExportAssignment: (decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isExportEquals: boolean | undefined, expression: Expression) => ExportAssignment; /** @deprecated Use `factory.updateExportAssignment` or the factory supplied by your transformation context instead. */ @@ -11065,9 +11067,9 @@ declare namespace ts { /** @deprecated Use `factory.updateNamedExports` or the factory supplied by your transformation context instead. */ const updateNamedExports: (node: NamedExports, elements: readonly ExportSpecifier[]) => NamedExports; /** @deprecated Use `factory.createExportSpecifier` or the factory supplied by your transformation context instead. */ - const createExportSpecifier: (propertyName: string | Identifier | undefined, name: string | Identifier) => ExportSpecifier; + const createExportSpecifier: (isTypeOnly: boolean, propertyName: string | Identifier | undefined, name: string | Identifier) => ExportSpecifier; /** @deprecated Use `factory.updateExportSpecifier` or the factory supplied by your transformation context instead. */ - const updateExportSpecifier: (node: ExportSpecifier, propertyName: Identifier | undefined, name: Identifier) => ExportSpecifier; + const updateExportSpecifier: (node: ExportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) => ExportSpecifier; /** @deprecated Use `factory.createExternalModuleReference` or the factory supplied by your transformation context instead. */ const createExternalModuleReference: (expression: Expression) => ExternalModuleReference; /** @deprecated Use `factory.updateExternalModuleReference` or the factory supplied by your transformation context instead. */ diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 6dc1325f91..dfd27fa2c9 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1698,10 +1698,12 @@ declare namespace ts { readonly parent: NamedImports; readonly propertyName?: Identifier; readonly name: Identifier; + readonly isTypeOnly: boolean; } export interface ExportSpecifier extends NamedDeclaration { readonly kind: SyntaxKind.ExportSpecifier; readonly parent: NamedExports; + readonly isTypeOnly: boolean; readonly propertyName?: Identifier; readonly name: Identifier; } @@ -3578,16 +3580,16 @@ declare namespace ts { updateNamespaceExport(node: NamespaceExport, name: Identifier): NamespaceExport; createNamedImports(elements: readonly ImportSpecifier[]): NamedImports; updateNamedImports(node: NamedImports, elements: readonly ImportSpecifier[]): NamedImports; - createImportSpecifier(propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; - updateImportSpecifier(node: ImportSpecifier, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; + createImportSpecifier(isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; + updateImportSpecifier(node: ImportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ImportSpecifier; createExportAssignment(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isExportEquals: boolean | undefined, expression: Expression): ExportAssignment; updateExportAssignment(node: ExportAssignment, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, expression: Expression): ExportAssignment; createExportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier?: Expression, assertClause?: AssertClause): ExportDeclaration; updateExportDeclaration(node: ExportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier: Expression | undefined, assertClause: AssertClause | undefined): ExportDeclaration; createNamedExports(elements: readonly ExportSpecifier[]): NamedExports; updateNamedExports(node: NamedExports, elements: readonly ExportSpecifier[]): NamedExports; - createExportSpecifier(propertyName: string | Identifier | undefined, name: string | Identifier): ExportSpecifier; - updateExportSpecifier(node: ExportSpecifier, propertyName: Identifier | undefined, name: Identifier): ExportSpecifier; + createExportSpecifier(isTypeOnly: boolean, propertyName: string | Identifier | undefined, name: string | Identifier): ExportSpecifier; + updateExportSpecifier(node: ExportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier): ExportSpecifier; createExternalModuleReference(expression: Expression): ExternalModuleReference; updateExternalModuleReference(node: ExternalModuleReference, expression: Expression): ExternalModuleReference; createJSDocAllType(): JSDocAllType; @@ -7252,9 +7254,9 @@ declare namespace ts { /** @deprecated Use `factory.updateNamedImports` or the factory supplied by your transformation context instead. */ const updateNamedImports: (node: NamedImports, elements: readonly ImportSpecifier[]) => NamedImports; /** @deprecated Use `factory.createImportSpecifier` or the factory supplied by your transformation context instead. */ - const createImportSpecifier: (propertyName: Identifier | undefined, name: Identifier) => ImportSpecifier; + const createImportSpecifier: (isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) => ImportSpecifier; /** @deprecated Use `factory.updateImportSpecifier` or the factory supplied by your transformation context instead. */ - const updateImportSpecifier: (node: ImportSpecifier, propertyName: Identifier | undefined, name: Identifier) => ImportSpecifier; + const updateImportSpecifier: (node: ImportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) => ImportSpecifier; /** @deprecated Use `factory.createExportAssignment` or the factory supplied by your transformation context instead. */ const createExportAssignment: (decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isExportEquals: boolean | undefined, expression: Expression) => ExportAssignment; /** @deprecated Use `factory.updateExportAssignment` or the factory supplied by your transformation context instead. */ @@ -7264,9 +7266,9 @@ declare namespace ts { /** @deprecated Use `factory.updateNamedExports` or the factory supplied by your transformation context instead. */ const updateNamedExports: (node: NamedExports, elements: readonly ExportSpecifier[]) => NamedExports; /** @deprecated Use `factory.createExportSpecifier` or the factory supplied by your transformation context instead. */ - const createExportSpecifier: (propertyName: string | Identifier | undefined, name: string | Identifier) => ExportSpecifier; + const createExportSpecifier: (isTypeOnly: boolean, propertyName: string | Identifier | undefined, name: string | Identifier) => ExportSpecifier; /** @deprecated Use `factory.updateExportSpecifier` or the factory supplied by your transformation context instead. */ - const updateExportSpecifier: (node: ExportSpecifier, propertyName: Identifier | undefined, name: Identifier) => ExportSpecifier; + const updateExportSpecifier: (node: ExportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) => ExportSpecifier; /** @deprecated Use `factory.createExternalModuleReference` or the factory supplied by your transformation context instead. */ const createExternalModuleReference: (expression: Expression) => ExternalModuleReference; /** @deprecated Use `factory.updateExternalModuleReference` or the factory supplied by your transformation context instead. */ diff --git a/tests/baselines/reference/exportSpecifiers.errors.txt b/tests/baselines/reference/exportSpecifiers.errors.txt new file mode 100644 index 0000000000..b771831f15 --- /dev/null +++ b/tests/baselines/reference/exportSpecifiers.errors.txt @@ -0,0 +1,40 @@ +/exports.ts(9,15): error TS2207: The 'type' modifier cannot be used on a named export when 'export type' is used on its export statement. +/imports.ts(3,1): error TS1362: 'as' cannot be used as a value because it was exported using 'export type'. +/imports.ts(4,1): error TS1362: 'something' cannot be used as a value because it was exported using 'export type'. +/imports.ts(5,1): error TS1362: 'foo' cannot be used as a value because it was exported using 'export type'. +/imports.ts(6,1): error TS1362: 'bar' cannot be used as a value because it was exported using 'export type'. + + +==== /imports.ts (4 errors) ==== + import { type, as, something, foo, bar } from "./exports.js"; + type; + as; // Error (used in emitting position) + ~~ +!!! error TS1362: 'as' cannot be used as a value because it was exported using 'export type'. +!!! related TS1377 /exports.ts:5:10: 'as' was exported here. + something; // Error (used in emitting position) + ~~~~~~~~~ +!!! error TS1362: 'something' cannot be used as a value because it was exported using 'export type'. +!!! related TS1377 /exports.ts:6:10: 'something' was exported here. + foo; // Error (used in emitting position) + ~~~ +!!! error TS1362: 'foo' cannot be used as a value because it was exported using 'export type'. +!!! related TS1377 /exports.ts:7:10: 'foo' was exported here. + bar; // Error (used in emitting position) + ~~~ +!!! error TS1362: 'bar' cannot be used as a value because it was exported using 'export type'. +!!! related TS1377 /exports.ts:8:10: 'bar' was exported here. + +==== /exports.ts (1 errors) ==== + const type = 0; + const as = 0; + const something = 0; + export { type }; + export { type as }; + export { type something }; + export { type type as foo }; + export { type as as bar }; + export type { type something as whatever }; // Error + ~~~~ +!!! error TS2207: The 'type' modifier cannot be used on a named export when 'export type' is used on its export statement. + \ No newline at end of file diff --git a/tests/baselines/reference/exportSpecifiers.js b/tests/baselines/reference/exportSpecifiers.js new file mode 100644 index 0000000000..1b4ea32845 --- /dev/null +++ b/tests/baselines/reference/exportSpecifiers.js @@ -0,0 +1,48 @@ +//// [tests/cases/conformance/externalModules/typeOnly/exportSpecifiers.ts] //// + +//// [imports.ts] +import { type, as, something, foo, bar } from "./exports.js"; +type; +as; // Error (used in emitting position) +something; // Error (used in emitting position) +foo; // Error (used in emitting position) +bar; // Error (used in emitting position) + +//// [exports.ts] +const type = 0; +const as = 0; +const something = 0; +export { type }; +export { type as }; +export { type something }; +export { type type as foo }; +export { type as as bar }; +export type { type something as whatever }; // Error + + +//// [exports.js] +var type = 0; +var as = 0; +var something = 0; +export { type }; +//// [imports.js] +import { type } from "./exports.js"; +type; +as; // Error (used in emitting position) +something; // Error (used in emitting position) +foo; // Error (used in emitting position) +bar; // Error (used in emitting position) + + +//// [exports.d.ts] +declare const type = 0; +declare const as = 0; +declare const something = 0; +export { type }; +export { type as }; +export { type something }; +export { type type as foo }; +export { type as as bar }; +export type { type something as whatever }; +//// [imports.d.ts] +export {}; diff --git a/tests/baselines/reference/exportSpecifiers.symbols b/tests/baselines/reference/exportSpecifiers.symbols new file mode 100644 index 0000000000..027f5db382 --- /dev/null +++ b/tests/baselines/reference/exportSpecifiers.symbols @@ -0,0 +1,54 @@ +=== /imports.ts === +import { type, as, something, foo, bar } from "./exports.js"; +>type : Symbol(type, Decl(imports.ts, 0, 8)) +>as : Symbol(as, Decl(imports.ts, 0, 14)) +>something : Symbol(something, Decl(imports.ts, 0, 18)) +>foo : Symbol(foo, Decl(imports.ts, 0, 29)) +>bar : Symbol(bar, Decl(imports.ts, 0, 34)) + +type; +>type : Symbol(type, Decl(imports.ts, 0, 8)) + +as; // Error (used in emitting position) +>as : Symbol(as, Decl(imports.ts, 0, 14)) + +something; // Error (used in emitting position) +>something : Symbol(something, Decl(imports.ts, 0, 18)) + +foo; // Error (used in emitting position) +>foo : Symbol(foo, Decl(imports.ts, 0, 29)) + +bar; // Error (used in emitting position) +>bar : Symbol(bar, Decl(imports.ts, 0, 34)) + +=== /exports.ts === +const type = 0; +>type : Symbol(type, Decl(exports.ts, 0, 5)) + +const as = 0; +>as : Symbol(as, Decl(exports.ts, 1, 5)) + +const something = 0; +>something : Symbol(something, Decl(exports.ts, 2, 5)) + +export { type }; +>type : Symbol(type, Decl(exports.ts, 3, 8)) + +export { type as }; +>as : Symbol(as, Decl(exports.ts, 4, 8)) + +export { type something }; +>something : Symbol(something, Decl(exports.ts, 5, 8)) + +export { type type as foo }; +>type : Symbol(type, Decl(exports.ts, 0, 5)) +>foo : Symbol(foo, Decl(exports.ts, 6, 8)) + +export { type as as bar }; +>as : Symbol(as, Decl(exports.ts, 1, 5)) +>bar : Symbol(bar, Decl(exports.ts, 7, 8)) + +export type { type something as whatever }; // Error +>something : Symbol(something, Decl(exports.ts, 2, 5)) +>whatever : Symbol(whatever, Decl(exports.ts, 8, 13)) + diff --git a/tests/baselines/reference/exportSpecifiers.types b/tests/baselines/reference/exportSpecifiers.types new file mode 100644 index 0000000000..16716d801f --- /dev/null +++ b/tests/baselines/reference/exportSpecifiers.types @@ -0,0 +1,57 @@ +=== /imports.ts === +import { type, as, something, foo, bar } from "./exports.js"; +>type : 0 +>as : 0 +>something : 0 +>foo : 0 +>bar : 0 + +type; +>type : 0 + +as; // Error (used in emitting position) +>as : 0 + +something; // Error (used in emitting position) +>something : 0 + +foo; // Error (used in emitting position) +>foo : 0 + +bar; // Error (used in emitting position) +>bar : 0 + +=== /exports.ts === +const type = 0; +>type : 0 +>0 : 0 + +const as = 0; +>as : 0 +>0 : 0 + +const something = 0; +>something : 0 +>0 : 0 + +export { type }; +>type : 0 + +export { type as }; +>as : 0 + +export { type something }; +>something : 0 + +export { type type as foo }; +>type : 0 +>foo : 0 + +export { type as as bar }; +>as : 0 +>bar : 0 + +export type { type something as whatever }; // Error +>something : 0 +>whatever : any + diff --git a/tests/baselines/reference/importSpecifiers1.errors.txt b/tests/baselines/reference/importSpecifiers1.errors.txt new file mode 100644 index 0000000000..db97058353 --- /dev/null +++ b/tests/baselines/reference/importSpecifiers1.errors.txt @@ -0,0 +1,108 @@ +/a.ts(4,1): error TS1361: 'as' cannot be used as a value because it was imported using 'import type'. +/b.ts(2,1): error TS2304: Cannot find name 'type'. +/c.ts(2,1): error TS2304: Cannot find name 'type'. +/c.ts(3,1): error TS1361: 'as' cannot be used as a value because it was imported using 'import type'. +/d.ts(1,21): error TS2300: Duplicate identifier 'as'. +/d.ts(1,24): error TS1005: ',' expected. +/d.ts(1,24): error TS2300: Duplicate identifier 'as'. +/e.ts(4,1): error TS1361: 'as' cannot be used as a value because it was imported using 'import type'. +/f.ts(1,15): error TS1003: Identifier expected. +/f.ts(1,15): error TS2305: Module '"./mod.js"' has no exported member 'import'. +/f.ts(2,18): error TS1003: Identifier expected. +/f.ts(2,18): error TS2300: Duplicate identifier 'export'. +/f.ts(3,21): error TS1003: Identifier expected. +/f.ts(3,21): error TS2300: Duplicate identifier 'export'. +/f.ts(6,1): error TS2304: Cannot find name 'type'. +/f.ts(7,1): error TS2304: Cannot find name 'as'. +/f.ts(8,1): error TS1361: 'something' cannot be used as a value because it was imported using 'import type'. +/f.ts(9,1): error TS1361: 's' cannot be used as a value because it was imported using 'import type'. +/g.ts(1,15): error TS2206: The 'type' modifier cannot be used on a named import when 'import type' is used on its import statement. + + +==== /mod.ts (0 errors) ==== + export const as = 0; + export const type = 0; + export const something = 0; + +==== /a.ts (1 errors) ==== + import { type } from "./mod.js"; + import { type as } from "./mod.js"; + type; + as; // Error (used in emitting position) + ~~ +!!! error TS1361: 'as' cannot be used as a value because it was imported using 'import type'. +!!! related TS1376 /a.ts:2:10: 'as' was imported here. + +==== /b.ts (1 errors) ==== + import { type as as } from "./mod.js"; + type; // Error (cannot resolve name) + ~~~~ +!!! error TS2304: Cannot find name 'type'. + as; + +==== /c.ts (2 errors) ==== + import { type as as as } from "./mod.js"; + type; // Error (cannot resolve name) + ~~~~ +!!! error TS2304: Cannot find name 'type'. + as; // Error (used in emitting position) + ~~ +!!! error TS1361: 'as' cannot be used as a value because it was imported using 'import type'. +!!! related TS1376 /c.ts:1:10: 'as' was imported here. + +==== /d.ts (3 errors) ==== + import { type as as as as } from "./mod.js"; // Error + ~~ +!!! error TS2300: Duplicate identifier 'as'. + ~~ +!!! error TS1005: ',' expected. + ~~ +!!! error TS2300: Duplicate identifier 'as'. + +==== /e.ts (1 errors) ==== + import { type type as as } from "./mod.js"; + import { type as type } from "./mod.js"; + type; + as; // Error (used in emitting position) + ~~ +!!! error TS1361: 'as' cannot be used as a value because it was imported using 'import type'. +!!! related TS1376 /e.ts:1:10: 'as' was imported here. + +==== /f.ts (10 errors) ==== + import { type import } from "./mod.js"; // Error + ~~~~~~ +!!! error TS1003: Identifier expected. + ~~~~~~ +!!! error TS2305: Module '"./mod.js"' has no exported member 'import'. + import { type as export } from "./mod.js"; // Error + ~~~~~~ +!!! error TS1003: Identifier expected. + ~~~~~~ +!!! error TS2300: Duplicate identifier 'export'. + import { type as as export } from "./mod.js"; // Error + ~~~~~~ +!!! error TS1003: Identifier expected. + ~~~~~~ +!!! error TS2300: Duplicate identifier 'export'. + import { type something } from "./mod.js"; + import { type something as s } from "./mod.js"; + type; // Error (cannot resolve name) + ~~~~ +!!! error TS2304: Cannot find name 'type'. + as; // Error (cannot resolve name) + ~~ +!!! error TS2304: Cannot find name 'as'. + something; // Error (used in emitting position) + ~~~~~~~~~ +!!! error TS1361: 'something' cannot be used as a value because it was imported using 'import type'. +!!! related TS1376 /f.ts:4:10: 'something' was imported here. + s; // Error (used in emitting position) + ~ +!!! error TS1361: 's' cannot be used as a value because it was imported using 'import type'. +!!! related TS1376 /f.ts:5:10: 's' was imported here. + +==== /g.ts (1 errors) ==== + import type { type something } from "./mod.js"; // Error + ~~~~ +!!! error TS2206: The 'type' modifier cannot be used on a named import when 'import type' is used on its import statement. + \ No newline at end of file diff --git a/tests/baselines/reference/importSpecifiers1.js b/tests/baselines/reference/importSpecifiers1.js new file mode 100644 index 0000000000..15f12e1f2e --- /dev/null +++ b/tests/baselines/reference/importSpecifiers1.js @@ -0,0 +1,97 @@ +//// [tests/cases/conformance/externalModules/typeOnly/importSpecifiers1.ts] //// + +//// [mod.ts] +export const as = 0; +export const type = 0; +export const something = 0; + +//// [a.ts] +import { type } from "./mod.js"; +import { type as } from "./mod.js"; +type; +as; // Error (used in emitting position) + +//// [b.ts] +import { type as as } from "./mod.js"; +type; // Error (cannot resolve name) +as; + +//// [c.ts] +import { type as as as } from "./mod.js"; +type; // Error (cannot resolve name) +as; // Error (used in emitting position) + +//// [d.ts] +import { type as as as as } from "./mod.js"; // Error + +//// [e.ts] +import { type type as as } from "./mod.js"; +import { type as type } from "./mod.js"; +type; +as; // Error (used in emitting position) + +//// [f.ts] +import { type import } from "./mod.js"; // Error +import { type as export } from "./mod.js"; // Error +import { type as as export } from "./mod.js"; // Error +import { type something } from "./mod.js"; +import { type something as s } from "./mod.js"; +type; // Error (cannot resolve name) +as; // Error (cannot resolve name) +something; // Error (used in emitting position) +s; // Error (used in emitting position) + +//// [g.ts] +import type { type something } from "./mod.js"; // Error + + +//// [mod.js] +export var as = 0; +export var type = 0; +export var something = 0; +//// [a.js] +import { type } from "./mod.js"; +type; +as; // Error (used in emitting position) +//// [b.js] +import { type as as } from "./mod.js"; +type; // Error (cannot resolve name) +as; +//// [c.js] +type; // Error (cannot resolve name) +as; // Error (used in emitting position) +export {}; +//// [d.js] +export {}; +//// [e.js] +import { type as type } from "./mod.js"; +type; +as; // Error (used in emitting position) +//// [f.js] +type; // Error (cannot resolve name) +as; // Error (cannot resolve name) +something; // Error (used in emitting position) +s; // Error (used in emitting position) +export {}; +//// [g.js] +export {}; + + +//// [mod.d.ts] +export declare const as = 0; +export declare const type = 0; +export declare const something = 0; +//// [a.d.ts] +export {}; +//// [b.d.ts] +export {}; +//// [c.d.ts] +export {}; +//// [d.d.ts] +export {}; +//// [e.d.ts] +export {}; +//// [f.d.ts] +export {}; +//// [g.d.ts] +export {}; diff --git a/tests/baselines/reference/importSpecifiers1.symbols b/tests/baselines/reference/importSpecifiers1.symbols new file mode 100644 index 0000000000..460baabd3f --- /dev/null +++ b/tests/baselines/reference/importSpecifiers1.symbols @@ -0,0 +1,93 @@ +=== /mod.ts === +export const as = 0; +>as : Symbol(as, Decl(mod.ts, 0, 12)) + +export const type = 0; +>type : Symbol(type, Decl(mod.ts, 1, 12)) + +export const something = 0; +>something : Symbol(something, Decl(mod.ts, 2, 12)) + +=== /a.ts === +import { type } from "./mod.js"; +>type : Symbol(type, Decl(a.ts, 0, 8)) + +import { type as } from "./mod.js"; +>as : Symbol(as, Decl(a.ts, 1, 8)) + +type; +>type : Symbol(type, Decl(a.ts, 0, 8)) + +as; // Error (used in emitting position) +>as : Symbol(as, Decl(a.ts, 1, 8)) + +=== /b.ts === +import { type as as } from "./mod.js"; +>type : Symbol(as, Decl(mod.ts, 1, 12)) +>as : Symbol(as, Decl(b.ts, 0, 8)) + +type; // Error (cannot resolve name) +as; +>as : Symbol(as, Decl(b.ts, 0, 8)) + +=== /c.ts === +import { type as as as } from "./mod.js"; +>as : Symbol(as, Decl(mod.ts, 0, 12)) +>as : Symbol(as, Decl(c.ts, 0, 8)) + +type; // Error (cannot resolve name) +as; // Error (used in emitting position) +>as : Symbol(as, Decl(c.ts, 0, 8)) + +=== /d.ts === +import { type as as as as } from "./mod.js"; // Error +>as : Symbol(as, Decl(mod.ts, 0, 12)) +>as : Symbol(as, Decl(d.ts, 0, 8)) +>as : Symbol(as, Decl(d.ts, 0, 22)) + +=== /e.ts === +import { type type as as } from "./mod.js"; +>type : Symbol(as, Decl(mod.ts, 1, 12)) +>as : Symbol(as, Decl(e.ts, 0, 8)) + +import { type as type } from "./mod.js"; +>type : Symbol(as, Decl(mod.ts, 1, 12)) +>type : Symbol(type, Decl(e.ts, 1, 8)) + +type; +>type : Symbol(type, Decl(e.ts, 1, 8)) + +as; // Error (used in emitting position) +>as : Symbol(as, Decl(e.ts, 0, 8)) + +=== /f.ts === +import { type import } from "./mod.js"; // Error +>import : Symbol(import, Decl(f.ts, 0, 8)) + +import { type as export } from "./mod.js"; // Error +>type : Symbol(export, Decl(mod.ts, 1, 12)) +>export : Symbol(export, Decl(f.ts, 1, 8)) + +import { type as as export } from "./mod.js"; // Error +>as : Symbol(as, Decl(mod.ts, 0, 12)) +>export : Symbol(export, Decl(f.ts, 2, 8)) + +import { type something } from "./mod.js"; +>something : Symbol(something, Decl(f.ts, 3, 8)) + +import { type something as s } from "./mod.js"; +>something : Symbol(something, Decl(mod.ts, 2, 12)) +>s : Symbol(s, Decl(f.ts, 4, 8)) + +type; // Error (cannot resolve name) +as; // Error (cannot resolve name) +something; // Error (used in emitting position) +>something : Symbol(something, Decl(f.ts, 3, 8)) + +s; // Error (used in emitting position) +>s : Symbol(s, Decl(f.ts, 4, 8)) + +=== /g.ts === +import type { type something } from "./mod.js"; // Error +>something : Symbol(something, Decl(g.ts, 0, 13)) + diff --git a/tests/baselines/reference/importSpecifiers1.types b/tests/baselines/reference/importSpecifiers1.types new file mode 100644 index 0000000000..2f04fe48f7 --- /dev/null +++ b/tests/baselines/reference/importSpecifiers1.types @@ -0,0 +1,104 @@ +=== /mod.ts === +export const as = 0; +>as : 0 +>0 : 0 + +export const type = 0; +>type : 0 +>0 : 0 + +export const something = 0; +>something : 0 +>0 : 0 + +=== /a.ts === +import { type } from "./mod.js"; +>type : 0 + +import { type as } from "./mod.js"; +>as : 0 + +type; +>type : 0 + +as; // Error (used in emitting position) +>as : 0 + +=== /b.ts === +import { type as as } from "./mod.js"; +>type : 0 +>as : 0 + +type; // Error (cannot resolve name) +>type : any + +as; +>as : 0 + +=== /c.ts === +import { type as as as } from "./mod.js"; +>as : 0 +>as : 0 + +type; // Error (cannot resolve name) +>type : any + +as; // Error (used in emitting position) +>as : 0 + +=== /d.ts === +import { type as as as as } from "./mod.js"; // Error +>as : 0 +>as : 0 +>as : 0 + +=== /e.ts === +import { type type as as } from "./mod.js"; +>type : 0 +>as : 0 + +import { type as type } from "./mod.js"; +>type : 0 +>type : 0 + +type; +>type : 0 + +as; // Error (used in emitting position) +>as : 0 + +=== /f.ts === +import { type import } from "./mod.js"; // Error +>import : any + +import { type as export } from "./mod.js"; // Error +>type : 0 +>export : 0 + +import { type as as export } from "./mod.js"; // Error +>as : 0 +>export : 0 + +import { type something } from "./mod.js"; +>something : 0 + +import { type something as s } from "./mod.js"; +>something : 0 +>s : 0 + +type; // Error (cannot resolve name) +>type : any + +as; // Error (cannot resolve name) +>as : any + +something; // Error (used in emitting position) +>something : 0 + +s; // Error (used in emitting position) +>s : 0 + +=== /g.ts === +import type { type something } from "./mod.js"; // Error +>something : any + diff --git a/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.js b/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.js new file mode 100644 index 0000000000..2c77dc75b2 --- /dev/null +++ b/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.js @@ -0,0 +1,20 @@ +//// [tests/cases/conformance/externalModules/typeOnly/preserveValueImports_importsNotUsedAsValues.ts] //// + +//// [mod.ts] +export type A = unknown; +export type B = never; +export type C = any; + +//// [index.ts] +import { type A, type B, type C } from "./mod.js"; + +//// [reexport.ts] +export { type A, type B, type C } from "./mod.js"; + + +//// [mod.js] +export {}; +//// [index.js] +import {} from "./mod.js"; +//// [reexport.js] +export {} from "./mod.js"; diff --git a/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.symbols b/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.symbols new file mode 100644 index 0000000000..a5e68feba6 --- /dev/null +++ b/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.symbols @@ -0,0 +1,22 @@ +=== /mod.ts === +export type A = unknown; +>A : Symbol(A, Decl(mod.ts, 0, 0)) + +export type B = never; +>B : Symbol(B, Decl(mod.ts, 0, 24)) + +export type C = any; +>C : Symbol(C, Decl(mod.ts, 1, 22)) + +=== /index.ts === +import { type A, type B, type C } from "./mod.js"; +>A : Symbol(A, Decl(index.ts, 0, 8)) +>B : Symbol(B, Decl(index.ts, 0, 16)) +>C : Symbol(C, Decl(index.ts, 0, 24)) + +=== /reexport.ts === +export { type A, type B, type C } from "./mod.js"; +>A : Symbol(A, Decl(reexport.ts, 0, 8)) +>B : Symbol(B, Decl(reexport.ts, 0, 16)) +>C : Symbol(C, Decl(reexport.ts, 0, 24)) + diff --git a/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.types b/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.types new file mode 100644 index 0000000000..fb4f950d50 --- /dev/null +++ b/tests/baselines/reference/preserveValueImports_importsNotUsedAsValues.types @@ -0,0 +1,22 @@ +=== /mod.ts === +export type A = unknown; +>A : unknown + +export type B = never; +>B : never + +export type C = any; +>C : any + +=== /index.ts === +import { type A, type B, type C } from "./mod.js"; +>A : any +>B : any +>C : any + +=== /reexport.ts === +export { type A, type B, type C } from "./mod.js"; +>A : any +>B : any +>C : any + diff --git a/tests/baselines/reference/preserveValueImports_mixedImports.errors.txt b/tests/baselines/reference/preserveValueImports_mixedImports.errors.txt new file mode 100644 index 0000000000..6ac4bec9fa --- /dev/null +++ b/tests/baselines/reference/preserveValueImports_mixedImports.errors.txt @@ -0,0 +1,15 @@ +/index.ts(1,21): error TS1444: 'ComponentProps' is a type and must be imported using a type-only import when 'preserveValueImports' and 'isolatedModules' are both enabled. + + +==== /exports.ts (0 errors) ==== + export function Component() {} + export interface ComponentProps {} + +==== /index.ts (1 errors) ==== + import { Component, ComponentProps } from "./exports.js"; + ~~~~~~~~~~~~~~ +!!! error TS1444: 'ComponentProps' is a type and must be imported using a type-only import when 'preserveValueImports' and 'isolatedModules' are both enabled. + +==== /index.fixed.ts (0 errors) ==== + import { Component, type ComponentProps } from "./exports.js"; + \ No newline at end of file diff --git a/tests/baselines/reference/preserveValueImports_mixedImports.js b/tests/baselines/reference/preserveValueImports_mixedImports.js new file mode 100644 index 0000000000..889ab0f80f --- /dev/null +++ b/tests/baselines/reference/preserveValueImports_mixedImports.js @@ -0,0 +1,19 @@ +//// [tests/cases/conformance/externalModules/typeOnly/preserveValueImports_mixedImports.ts] //// + +//// [exports.ts] +export function Component() {} +export interface ComponentProps {} + +//// [index.ts] +import { Component, ComponentProps } from "./exports.js"; + +//// [index.fixed.ts] +import { Component, type ComponentProps } from "./exports.js"; + + +//// [exports.js] +export function Component() { } +//// [index.js] +import { Component } from "./exports.js"; +//// [index.fixed.js] +import { Component } from "./exports.js"; diff --git a/tests/baselines/reference/preserveValueImports_mixedImports.symbols b/tests/baselines/reference/preserveValueImports_mixedImports.symbols new file mode 100644 index 0000000000..9f592eb2a0 --- /dev/null +++ b/tests/baselines/reference/preserveValueImports_mixedImports.symbols @@ -0,0 +1,17 @@ +=== /exports.ts === +export function Component() {} +>Component : Symbol(Component, Decl(exports.ts, 0, 0)) + +export interface ComponentProps {} +>ComponentProps : Symbol(ComponentProps, Decl(exports.ts, 0, 30)) + +=== /index.ts === +import { Component, ComponentProps } from "./exports.js"; +>Component : Symbol(Component, Decl(index.ts, 0, 8)) +>ComponentProps : Symbol(ComponentProps, Decl(index.ts, 0, 19)) + +=== /index.fixed.ts === +import { Component, type ComponentProps } from "./exports.js"; +>Component : Symbol(Component, Decl(index.fixed.ts, 0, 8)) +>ComponentProps : Symbol(ComponentProps, Decl(index.fixed.ts, 0, 19)) + diff --git a/tests/baselines/reference/preserveValueImports_mixedImports.types b/tests/baselines/reference/preserveValueImports_mixedImports.types new file mode 100644 index 0000000000..ec2682c94f --- /dev/null +++ b/tests/baselines/reference/preserveValueImports_mixedImports.types @@ -0,0 +1,16 @@ +=== /exports.ts === +export function Component() {} +>Component : () => void + +export interface ComponentProps {} + +=== /index.ts === +import { Component, ComponentProps } from "./exports.js"; +>Component : () => void +>ComponentProps : any + +=== /index.fixed.ts === +import { Component, type ComponentProps } from "./exports.js"; +>Component : () => void +>ComponentProps : any + diff --git a/tests/cases/conformance/externalModules/typeOnly/exportSpecifiers.ts b/tests/cases/conformance/externalModules/typeOnly/exportSpecifiers.ts new file mode 100644 index 0000000000..c8c584f66d --- /dev/null +++ b/tests/cases/conformance/externalModules/typeOnly/exportSpecifiers.ts @@ -0,0 +1,21 @@ +// @module: esnext +// @declaration: true + +// @Filename: /imports.ts +import { type, as, something, foo, bar } from "./exports.js"; +type; +as; // Error (used in emitting position) +something; // Error (used in emitting position) +foo; // Error (used in emitting position) +bar; // Error (used in emitting position) + +// @Filename: /exports.ts +const type = 0; +const as = 0; +const something = 0; +export { type }; +export { type as }; +export { type something }; +export { type type as foo }; +export { type as as bar }; +export type { type something as whatever }; // Error diff --git a/tests/cases/conformance/externalModules/typeOnly/importSpecifiers1.ts b/tests/cases/conformance/externalModules/typeOnly/importSpecifiers1.ts new file mode 100644 index 0000000000..348784befd --- /dev/null +++ b/tests/cases/conformance/externalModules/typeOnly/importSpecifiers1.ts @@ -0,0 +1,46 @@ +// @module: esnext +// @declaration: true + +// @Filename: /mod.ts +export const as = 0; +export const type = 0; +export const something = 0; + +// @Filename: /a.ts +import { type } from "./mod.js"; +import { type as } from "./mod.js"; +type; +as; // Error (used in emitting position) + +// @Filename: /b.ts +import { type as as } from "./mod.js"; +type; // Error (cannot resolve name) +as; + +// @Filename: /c.ts +import { type as as as } from "./mod.js"; +type; // Error (cannot resolve name) +as; // Error (used in emitting position) + +// @Filename: /d.ts +import { type as as as as } from "./mod.js"; // Error + +// @Filename: /e.ts +import { type type as as } from "./mod.js"; +import { type as type } from "./mod.js"; +type; +as; // Error (used in emitting position) + +// @Filename: /f.ts +import { type import } from "./mod.js"; // Error +import { type as export } from "./mod.js"; // Error +import { type as as export } from "./mod.js"; // Error +import { type something } from "./mod.js"; +import { type something as s } from "./mod.js"; +type; // Error (cannot resolve name) +as; // Error (cannot resolve name) +something; // Error (used in emitting position) +s; // Error (used in emitting position) + +// @Filename: /g.ts +import type { type something } from "./mod.js"; // Error diff --git a/tests/cases/conformance/externalModules/typeOnly/preserveValueImports_importsNotUsedAsValues.ts b/tests/cases/conformance/externalModules/typeOnly/preserveValueImports_importsNotUsedAsValues.ts new file mode 100644 index 0000000000..4797a30727 --- /dev/null +++ b/tests/cases/conformance/externalModules/typeOnly/preserveValueImports_importsNotUsedAsValues.ts @@ -0,0 +1,14 @@ +// @preserveValueImports: true +// @importsNotUsedAsValues: preserve +// @module: esnext + +// @Filename: /mod.ts +export type A = unknown; +export type B = never; +export type C = any; + +// @Filename: /index.ts +import { type A, type B, type C } from "./mod.js"; + +// @Filename: /reexport.ts +export { type A, type B, type C } from "./mod.js"; diff --git a/tests/cases/conformance/externalModules/typeOnly/preserveValueImports_mixedImports.ts b/tests/cases/conformance/externalModules/typeOnly/preserveValueImports_mixedImports.ts new file mode 100644 index 0000000000..85cbae30f7 --- /dev/null +++ b/tests/cases/conformance/externalModules/typeOnly/preserveValueImports_mixedImports.ts @@ -0,0 +1,13 @@ +// @preserveValueImports: true +// @isolatedModules: true +// @module: es2015 + +// @Filename: /exports.ts +export function Component() {} +export interface ComponentProps {} + +// @Filename: /index.ts +import { Component, ComponentProps } from "./exports.js"; + +// @Filename: /index.fixed.ts +import { Component, type ComponentProps } from "./exports.js"; diff --git a/tests/cases/fourslash/completionInNamedImportLocation.ts b/tests/cases/fourslash/completionInNamedImportLocation.ts index 85db087f11..057259855d 100644 --- a/tests/cases/fourslash/completionInNamedImportLocation.ts +++ b/tests/cases/fourslash/completionInNamedImportLocation.ts @@ -13,6 +13,18 @@ goTo.file("a.ts"); verify.completions( - { marker: "1", exact: [{ name: "x", text: "var x: number" }, { name: "y", text: "var y: number" }] }, - { marker: "2", exact: [{ name: "y", text: "var y: number" }] }, + { + marker: "1", + exact: [ + { name: "x", text: "var x: number" }, + { name: "y", text: "var y: number" }, + { name: "type", sortText: completion.SortText.GlobalsOrKeywords } + ] + }, + { + marker: "2", + exact: [ + { name: "y", text: "var y: number" }, + { name: "type", sortText: completion.SortText.GlobalsOrKeywords } + ] }, ); diff --git a/tests/cases/fourslash/completionListForExportEquals.ts b/tests/cases/fourslash/completionListForExportEquals.ts index 54161ea840..a03b3ddb5b 100644 --- a/tests/cases/fourslash/completionListForExportEquals.ts +++ b/tests/cases/fourslash/completionListForExportEquals.ts @@ -13,4 +13,4 @@ // @Filename: /a.ts ////import { /**/ } from "foo"; -verify.completions({ marker: "", exact: ["Static", "foo"] }); +verify.completions({ marker: "", exact: ["Static", "foo", { name: "type", sortText: completion.SortText.GlobalsOrKeywords }] }); diff --git a/tests/cases/fourslash/completionListForExportEquals2.ts b/tests/cases/fourslash/completionListForExportEquals2.ts index cf890b47ab..061f10e450 100644 --- a/tests/cases/fourslash/completionListForExportEquals2.ts +++ b/tests/cases/fourslash/completionListForExportEquals2.ts @@ -11,4 +11,4 @@ // @Filename: /a.ts ////import { /**/ } from "foo"; -verify.completions({ marker: "", exact: "Static" }); +verify.completions({ marker: "", exact: ["Static", { name: "type", sortText: completion.SortText.GlobalsOrKeywords }] }); diff --git a/tests/cases/fourslash/completionListInExportClause01.ts b/tests/cases/fourslash/completionListInExportClause01.ts index d11d395b5b..427d344930 100644 --- a/tests/cases/fourslash/completionListInExportClause01.ts +++ b/tests/cases/fourslash/completionListInExportClause01.ts @@ -12,10 +12,12 @@ ////export {bar as /*5*/, /*6*/ from "./m1" ////export {foo, bar, baz as b,/*7*/} from "./m1" +const type = { name: "type", sortText: completion.SortText.GlobalsOrKeywords }; + verify.completions( - { marker: ["1", "2", "3"], exact: ["bar", "baz", "foo"] }, - { marker: "4", exact: ["bar", "baz"] }, + { marker: ["1", "2", "3"], exact: ["bar", "baz", "foo", type] }, + { marker: "4", exact: ["bar", "baz", type] }, { marker: "5", exact: undefined, isNewIdentifierLocation: true }, - { marker: "6", exact: ["baz", "foo"] }, + { marker: "6", exact: ["baz", "foo", type] }, { marker: "7", exact: undefined }, ); diff --git a/tests/cases/fourslash/completionListInExportClause02.ts b/tests/cases/fourslash/completionListInExportClause02.ts index 7ded96ef0a..38ed71352f 100644 --- a/tests/cases/fourslash/completionListInExportClause02.ts +++ b/tests/cases/fourslash/completionListInExportClause02.ts @@ -8,4 +8,4 @@ //// export { /**/ } from "M1" ////} -verify.completions({ marker: "", exact: "V" }); +verify.completions({ marker: "", exact: ["V", { name: "type", sortText: completion.SortText.GlobalsOrKeywords }] }); diff --git a/tests/cases/fourslash/completionListInExportClause03.ts b/tests/cases/fourslash/completionListInExportClause03.ts index f731833ac0..d7b2581bf9 100644 --- a/tests/cases/fourslash/completionListInExportClause03.ts +++ b/tests/cases/fourslash/completionListInExportClause03.ts @@ -10,4 +10,4 @@ ////} // Ensure we don't filter out the current item. -verify.completions({ marker: "", exact: ["abc", "def"] }); +verify.completions({ marker: "", exact: ["abc", "def", { name: "type", sortText: completion.SortText.GlobalsOrKeywords }] }); diff --git a/tests/cases/fourslash/completionListInImportClause01.ts b/tests/cases/fourslash/completionListInImportClause01.ts index 026d436063..48ed52ae51 100644 --- a/tests/cases/fourslash/completionListInImportClause01.ts +++ b/tests/cases/fourslash/completionListInImportClause01.ts @@ -1,4 +1,5 @@ /// + // @ModuleResolution: classic // @Filename: m1.ts @@ -12,11 +13,16 @@ ////import {foo,/*4*/ from "m1" ////import {bar as /*5*/, /*6*/ from "m1" ////import {foo, bar, baz as b,/*7*/} from "m1" +////import { type /*8*/ } from "m1"; +////import { type b/*9*/ } from "m1"; + +const type = { name: "type", sortText: completion.SortText.GlobalsOrKeywords }; verify.completions( - { marker: ["1", "2", "3"], exact: ["bar", "baz", "foo"] }, - { marker: "4", exact: ["bar", "baz"] }, - { marker: "5", exact: undefined, isNewIdentifierLocation: true }, - { marker: "6", exact: ["baz", "foo"] }, - { marker: "7", exact: undefined }, + // { marker: ["1", "2", "3"], exact: ["bar", "baz", "foo", type] }, + // { marker: "4", exact: ["bar", "baz", type] }, + // { marker: "5", exact: undefined, isNewIdentifierLocation: true }, + // { marker: "6", exact: ["baz", "foo", type] }, + // { marker: "7", exact: undefined }, + { marker: ["8", "9"], exact: ["bar", "baz", "foo"] }, // No 'type' ); diff --git a/tests/cases/fourslash/completionListInImportClause02.ts b/tests/cases/fourslash/completionListInImportClause02.ts index 0bc840eed5..ce2370f8b5 100644 --- a/tests/cases/fourslash/completionListInImportClause02.ts +++ b/tests/cases/fourslash/completionListInImportClause02.ts @@ -8,4 +8,4 @@ //// import { /**/ } from "M1" ////} -verify.completions({ marker: "", exact: "V" }); +verify.completions({ marker: "", exact: ["V", { name: "type", sortText: completion.SortText.GlobalsOrKeywords }] }); diff --git a/tests/cases/fourslash/completionListInImportClause03.ts b/tests/cases/fourslash/completionListInImportClause03.ts index db1d7f84a5..48babf7492 100644 --- a/tests/cases/fourslash/completionListInImportClause03.ts +++ b/tests/cases/fourslash/completionListInImportClause03.ts @@ -10,4 +10,4 @@ ////} // Ensure we don't filter out the current item. -verify.completions({ marker: "", exact: ["abc", "def"] }); +verify.completions({ marker: "", exact: ["abc", "def", { name: "type", sortText: completion.SortText.GlobalsOrKeywords }] }); diff --git a/tests/cases/fourslash/completionListInImportClause04.ts b/tests/cases/fourslash/completionListInImportClause04.ts index 321a38d854..3d2ccdb8ad 100644 --- a/tests/cases/fourslash/completionListInImportClause04.ts +++ b/tests/cases/fourslash/completionListInImportClause04.ts @@ -11,7 +11,7 @@ // @Filename: app.ts ////import {/*1*/} from './foo'; -verify.completions({ marker: "1", exact: ["prototype", "prop1", "prop2"] }); +verify.completions({ marker: "1", exact: ["prototype", "prop1", "prop2", { name: "type", sortText: completion.SortText.GlobalsOrKeywords }] }); verify.noErrors(); goTo.marker('2'); verify.noErrors(); diff --git a/tests/cases/fourslash/completionListIsGlobalCompletion.ts b/tests/cases/fourslash/completionListIsGlobalCompletion.ts index 7184e50499..46b83e1a9a 100644 --- a/tests/cases/fourslash/completionListIsGlobalCompletion.ts +++ b/tests/cases/fourslash/completionListIsGlobalCompletion.ts @@ -7,7 +7,7 @@ ////} // @Filename: a.ts -////import { /*1*/ } from "./file.ts"; // no globals in imports - export found +////import { /*1*/ } from "./file.ts"; // no globals in imports - export not found //@Filename: file.tsx /////// // no globals in reference paths @@ -38,7 +38,8 @@ const x = ["test", "A", "B", "C", "y", "z", "x", "user"]; const globals: ReadonlyArray = [...x, ...completion.globals] verify.completions( - { marker: ["1", "3", "6", "8", "12", "14"], exact: undefined, isGlobalCompletion: false }, + { marker: ["1", "3"], exact: [{ name: "type", sortText: completion.SortText.GlobalsOrKeywords }], isNewIdentifierLocation: true, isGlobalCompletion: false }, + { marker: ["6", "8", "12", "14"], exact: undefined, isGlobalCompletion: false }, { marker: "2", exact: ["a.ts", "file.ts"], isGlobalCompletion: false, isNewIdentifierLocation: true }, { marker: ["4", "19"], exact: [], isGlobalCompletion: false }, { marker: ["5", "11"], exact: globals, isGlobalCompletion: true }, diff --git a/tests/cases/fourslash/completionsInExport.ts b/tests/cases/fourslash/completionsInExport.ts index 0edbe66c6e..94c3d2745a 100644 --- a/tests/cases/fourslash/completionsInExport.ts +++ b/tests/cases/fourslash/completionsInExport.ts @@ -4,16 +4,18 @@ ////type T = number; ////export { /**/ }; +const type = { name: "type", sortText: completion.SortText.GlobalsOrKeywords } + verify.completions({ marker: "", - exact: ["a", "T"] + exact: ["a", "T", type] }); // Deprioritize 'a' since it has been exported already. // (Keep it in the list because you can still do 'a as b'.) edit.insert("a, "); verify.completions({ - exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T"] + exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T", type] }); // No completions for new name @@ -25,7 +27,7 @@ verify.completions({ // 'T' still hasn't been exported by name edit.insert("U, "); verify.completions({ - exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T"] + exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T", type] }); // 'a' and 'T' are back to the same priority @@ -33,6 +35,7 @@ edit.insert("T, "); verify.completions({ exact: [ { name: "a", sortText: completion.SortText.OptionalMember }, - { name: "T", sortText: completion.SortText.OptionalMember } + { name: "T", sortText: completion.SortText.OptionalMember }, + type, ] }); diff --git a/tests/cases/fourslash/completionsInExport_invalid.ts b/tests/cases/fourslash/completionsInExport_invalid.ts index 0e632e7125..6318d98a37 100644 --- a/tests/cases/fourslash/completionsInExport_invalid.ts +++ b/tests/cases/fourslash/completionsInExport_invalid.ts @@ -8,5 +8,5 @@ verify.completions({ marker: "", - exact: ["topLevel"] + exact: ["topLevel", { name: "type", sortText: completion.SortText.GlobalsOrKeywords }] }); diff --git a/tests/cases/fourslash/completionsInExport_moduleBlock.ts b/tests/cases/fourslash/completionsInExport_moduleBlock.ts index 7c9b950017..51021a9512 100644 --- a/tests/cases/fourslash/completionsInExport_moduleBlock.ts +++ b/tests/cases/fourslash/completionsInExport_moduleBlock.ts @@ -8,16 +8,18 @@ //// export { /**/ }; ////} +const type = { name: "type", sortText: completion.SortText.GlobalsOrKeywords }; + verify.completions({ marker: "", - exact: ["a", "T"] + exact: ["a", "T", type] }); // Deprioritize 'a' since it has been exported already. // (Keep it in the list because you can still do 'a as b'.) edit.insert("a, "); verify.completions({ - exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T"] + exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T", type] }); // No completions for new name @@ -29,7 +31,7 @@ verify.completions({ // 'T' still hasn't been exported by name edit.insert("U, "); verify.completions({ - exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T"] + exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T", type] }); // 'a' and 'T' are back to the same priority @@ -37,6 +39,7 @@ edit.insert("T, "); verify.completions({ exact: [ { name: "a", sortText: completion.SortText.OptionalMember }, - { name: "T", sortText: completion.SortText.OptionalMember } + { name: "T", sortText: completion.SortText.OptionalMember }, + type, ] }); diff --git a/tests/cases/fourslash/importNameCodeFix_importType1.ts b/tests/cases/fourslash/importNameCodeFix_importType1.ts new file mode 100644 index 0000000000..cd9dbfd319 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_importType1.ts @@ -0,0 +1,28 @@ +/// + +// @preserveValueImports: true +// @isolatedModules: true +// @module: es2015 + +// @Filename: /exports.ts +//// export default someValue = 0; +//// export function Component() {} +//// export interface ComponentProps {} + +// @Filename: /a.ts +//// import { Component } from "./exports.js"; +//// interface MoreProps extends /*a*/ComponentProps {} + +// @Filename: /b.ts +//// import someValue from "./exports.js"; +//// interface MoreProps extends /*b*/ComponentProps {} + +goTo.marker("a"); +verify.importFixAtPosition([ +`import { Component, type ComponentProps } from "./exports.js"; +interface MoreProps extends ComponentProps {}`]); + +goTo.marker("b"); +verify.importFixAtPosition([ +`import someValue, { type ComponentProps } from "./exports.js"; +interface MoreProps extends ComponentProps {}`]); diff --git a/tests/cases/fourslash/importNameCodeFix_importType2.ts b/tests/cases/fourslash/importNameCodeFix_importType2.ts new file mode 100644 index 0000000000..f336b3e507 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_importType2.ts @@ -0,0 +1,51 @@ +/// + +// @importsNotUsedAsValues: error +// @preserveValueImports: true +// @isolatedModules: true +// @module: es2015 + +// @Filename: /exports1.ts +//// export default interface SomeType {} +//// export interface OtherType {} +//// export interface OtherOtherType {} +//// export const someValue = 0; + +// @Filename: /a.ts +//// import type SomeType from "./exports1.js"; +//// someValue/*a*/ + +// @Filename: /b.ts +//// import { someValue } from "./exports1.js"; +//// const b: SomeType/*b*/ = someValue; + +// @Filename: /c.ts +//// import type SomeType from "./exports1.js"; +//// const x: OtherType/*c*/ + +// @Filename: /d.ts +//// import type { OtherType } from "./exports1.js"; +//// const x: OtherOtherType/*d*/ + +goTo.marker("a"); +verify.importFixAtPosition([ +`import type SomeType from "./exports1.js"; +import { someValue } from "./exports1.js"; +someValue`]); + +goTo.marker("b"); +verify.importFixAtPosition([ +`import type SomeType from "./exports1.js"; +import { someValue } from "./exports1.js"; +const b: SomeType = someValue;`]); + +goTo.marker("c"); +verify.importFixAtPosition([ +`import type { OtherType } from "./exports1.js"; +import type SomeType from "./exports1.js"; +const x: OtherType`]); + +goTo.marker("d"); +verify.importFixAtPosition([ +`import type { OtherOtherType, OtherType } from "./exports1.js"; +const x: OtherOtherType`]); diff --git a/tests/cases/fourslash/importNameCodeFix_importType3.ts b/tests/cases/fourslash/importNameCodeFix_importType3.ts new file mode 100644 index 0000000000..6c530b3475 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_importType3.ts @@ -0,0 +1,18 @@ +/// + +// @preserveValueImports: true +// @isolatedModules: true +// @module: es2015 + +// @Filename: /exports.ts +//// class SomeClass {} +//// export type { SomeClass }; + +// @Filename: /a.ts +//// import {} from "./exports.js"; +//// function takeSomeClass(c: SomeClass/**/) + +goTo.marker(""); +verify.importFixAtPosition([ +`import { type SomeClass } from "./exports.js"; +function takeSomeClass(c: SomeClass)`]); diff --git a/tests/cases/fourslash/importNameCodeFix_importType4.ts b/tests/cases/fourslash/importNameCodeFix_importType4.ts new file mode 100644 index 0000000000..b14a2f8439 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_importType4.ts @@ -0,0 +1,18 @@ +/// + +// @preserveValueImports: true +// @isolatedModules: true +// @module: es2015 + +// @Filename: /exports.ts +//// export interface SomeInterface {} +//// export class SomePig {} + +// @Filename: /a.ts +//// import type { SomeInterface } from "./exports.js"; +//// new SomePig/**/ + +goTo.marker(""); +verify.importFixAtPosition([ +`import { SomePig, type SomeInterface } from "./exports.js"; +new SomePig`]); diff --git a/tests/cases/fourslash/importStatementCompletions1.ts b/tests/cases/fourslash/importStatementCompletions1.ts index de1a2c3eb3..89bc68189f 100644 --- a/tests/cases/fourslash/importStatementCompletions1.ts +++ b/tests/cases/fourslash/importStatementCompletions1.ts @@ -22,32 +22,37 @@ // @Filename: /index5.ts //// import f/*5*/ from ""; -[0, 1, 2, 3, 4, 5].forEach(marker => { - verify.completions({ - isNewIdentifierLocation: true, - marker: "" + marker, - exact: [{ - name: "foo", - source: "./mod", - insertText: `import { foo$1 } from "./mod";`, - isSnippet: true, - replacementSpan: test.ranges()[marker], - sourceDisplay: "./mod", - }, { - name: "Foo", - source: "./mod", - insertText: `import { Foo$1 } from "./mod";`, - isSnippet: true, - replacementSpan: test.ranges()[marker], - sourceDisplay: "./mod", - }], - preferences: { - includeCompletionsForImportStatements: true, - includeInsertTextCompletions: true, - includeCompletionsWithSnippetText: true, - } - }); -}); +// ([[0, true], [1, true], [2, false], [3, true], [4, true], [5, true]] as const).forEach(([marker, typeKeywordValid]) => { +// verify.completions({ +// isNewIdentifierLocation: true, +// marker: "" + marker, +// exact: [{ +// name: "foo", +// source: "./mod", +// insertText: `import { foo$1 } from "./mod";`, +// isSnippet: true, +// replacementSpan: test.ranges()[marker], +// sourceDisplay: "./mod", +// }, +// { +// name: "Foo", +// source: "./mod", +// insertText: `import { Foo$1 } from "./mod";`, +// isSnippet: true, +// replacementSpan: test.ranges()[marker], +// sourceDisplay: "./mod", +// }, +// ...typeKeywordValid ? [{ +// name: "type", +// sortText: completion.SortText.GlobalsOrKeywords, +// }] : []], +// preferences: { +// includeCompletionsForImportStatements: true, +// includeInsertTextCompletions: true, +// includeCompletionsWithSnippetText: true, +// } +// }); +// }); // @Filename: /index6.ts //// import f/*6*/ from "nope"; @@ -76,7 +81,7 @@ verify.completions({ isNewIdentifierLocation: true, marker: "" + marker, - exact: [], + exact: [{ name: "type", sortText: completion.SortText.GlobalsOrKeywords }], preferences: { includeCompletionsForImportStatements: true, includeInsertTextCompletions: true, diff --git a/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts b/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts index 4d41ee5702..80c65f9eb6 100644 --- a/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts +++ b/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts @@ -19,6 +19,9 @@ verify.completions({ isSnippet: true, replacementSpan: test.ranges()[0], sourceDisplay: "./mod", + }, { + name: "type", + sortText: completion.SortText.GlobalsOrKeywords, }], preferences: { includeCompletionsForImportStatements: true, diff --git a/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts b/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts index 6d0d6ecd4d..531e69f881 100644 --- a/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts +++ b/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts @@ -19,6 +19,9 @@ verify.completions({ isSnippet: true, replacementSpan: test.ranges()[0], sourceDisplay: "./mod", + }, { + name: "type", + sortText: completion.SortText.GlobalsOrKeywords, }], preferences: { includeCompletionsForImportStatements: true, diff --git a/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts b/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts index f92eecec49..edcea01c1b 100644 --- a/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts +++ b/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts @@ -12,7 +12,10 @@ verify.completions({ isNewIdentifierLocation: true, marker: "", - exact: [], + exact: [{ + name: "type", + sortText: completion.SortText.GlobalsOrKeywords, + }], preferences: { includeCompletionsForImportStatements: true, includeInsertTextCompletions: true, diff --git a/tests/cases/fourslash/importStatementCompletions_noSnippet.ts b/tests/cases/fourslash/importStatementCompletions_noSnippet.ts index 7545caf97a..4a923a78d5 100644 --- a/tests/cases/fourslash/importStatementCompletions_noSnippet.ts +++ b/tests/cases/fourslash/importStatementCompletions_noSnippet.ts @@ -16,6 +16,9 @@ verify.completions({ isSnippet: undefined, // <-- undefined replacementSpan: test.ranges()[0], sourceDisplay: "./mod", + }, { + name: "type", + sortText: completion.SortText.GlobalsOrKeywords, }], preferences: { includeCompletionsForImportStatements: true, diff --git a/tests/cases/fourslash/importStatementCompletions_quotes.ts b/tests/cases/fourslash/importStatementCompletions_quotes.ts index e39b96a530..fad1af70b9 100644 --- a/tests/cases/fourslash/importStatementCompletions_quotes.ts +++ b/tests/cases/fourslash/importStatementCompletions_quotes.ts @@ -17,6 +17,9 @@ verify.completions({ isSnippet: true, replacementSpan: test.ranges()[0], sourceDisplay: "./mod", + }, { + name: "type", + sortText: completion.SortText.GlobalsOrKeywords, }], preferences: { includeCompletionsForImportStatements: true, diff --git a/tests/cases/fourslash/importStatementCompletions_semicolons.ts b/tests/cases/fourslash/importStatementCompletions_semicolons.ts index 6674caee82..03b2b2d93a 100644 --- a/tests/cases/fourslash/importStatementCompletions_semicolons.ts +++ b/tests/cases/fourslash/importStatementCompletions_semicolons.ts @@ -17,6 +17,9 @@ verify.completions({ isSnippet: true, replacementSpan: test.ranges()[0], sourceDisplay: "./mod", + }, { + name: "type", + sortText: completion.SortText.GlobalsOrKeywords, }], preferences: { includeCompletionsForImportStatements: true, diff --git a/tests/cases/fourslash/importTypeCompletions7.ts b/tests/cases/fourslash/importTypeCompletions7.ts index 85825b4ef5..22fa2076bc 100644 --- a/tests/cases/fourslash/importTypeCompletions7.ts +++ b/tests/cases/fourslash/importTypeCompletions7.ts @@ -19,6 +19,9 @@ verify.completions({ source: "./foo", insertText: "import * as Foo from \"./foo\";", replacementSpan: test.ranges()[0] + }, { + name: "type", + sortText: completion.SortText.GlobalsOrKeywords, }], isNewIdentifierLocation: true, preferences: { diff --git a/tests/cases/fourslash/importTypeCompletions8.ts b/tests/cases/fourslash/importTypeCompletions8.ts new file mode 100644 index 0000000000..a6043131e3 --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions8.ts @@ -0,0 +1,26 @@ +/// +// @target: esnext + +// @filename: /foo.ts +////export interface Foo {} + +// @filename: /bar.ts +////[|import { type F/**/ }|] + +goTo.file("/bar.ts") +verify.completions({ + marker: "", + exact: [{ + name: "Foo", + sourceDisplay: "./foo", + source: "./foo", + insertText: "import { type Foo } from \"./foo\";", + replacementSpan: test.ranges()[0] + }], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +}); diff --git a/tests/cases/fourslash/importTypeCompletions9.ts b/tests/cases/fourslash/importTypeCompletions9.ts new file mode 100644 index 0000000000..96059ab978 --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions9.ts @@ -0,0 +1,26 @@ +/// +// @target: esnext + +// @filename: /foo.ts +////export interface Foo {} + +// @filename: /bar.ts +////[|import { type /**/ }|] + +goTo.file("/bar.ts") +verify.completions({ + marker: "", + exact: [{ + name: "Foo", + sourceDisplay: "./foo", + source: "./foo", + insertText: "import { type Foo } from \"./foo\";", + replacementSpan: test.ranges()[0] + }], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +}); diff --git a/tests/cases/fourslash/server/importStatementCompletions_pnpm1.ts b/tests/cases/fourslash/server/importStatementCompletions_pnpm1.ts index 9e6d16ced2..c2ffa75730 100644 --- a/tests/cases/fourslash/server/importStatementCompletions_pnpm1.ts +++ b/tests/cases/fourslash/server/importStatementCompletions_pnpm1.ts @@ -22,6 +22,9 @@ verify.completions({ isSnippet: true, replacementSpan: test.ranges()[0], sourceDisplay: "react", + }, { + name: "type", + sortText: completion.SortText.GlobalsOrKeywords, }], preferences: { includeCompletionsForImportStatements: true, diff --git a/tests/cases/fourslash/server/importStatementCompletions_pnpmTransitive.ts b/tests/cases/fourslash/server/importStatementCompletions_pnpmTransitive.ts index 5649c24476..d0159d4fc8 100644 --- a/tests/cases/fourslash/server/importStatementCompletions_pnpmTransitive.ts +++ b/tests/cases/fourslash/server/importStatementCompletions_pnpmTransitive.ts @@ -20,7 +20,7 @@ goTo.marker(""); verify.completions({ isNewIdentifierLocation: true, marker: "", - exact: [], + exact: [{ name: "type", sortText: completion.SortText.GlobalsOrKeywords }], preferences: { includeCompletionsForImportStatements: true, includeCompletionsWithInsertText: true,