Skip to content

Commit

Permalink
Type-only import specifiers (#45998)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
  • Loading branch information
andrewbranch and DanielRosenwasser authored Sep 27, 2021
1 parent 26aef89 commit e160bc8
Show file tree
Hide file tree
Showing 80 changed files with 1,841 additions and 348 deletions.
63 changes: 41 additions & 22 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ namespace ts {
getLocalTypeParametersOfClassOrInterfaceOrTypeAlias,
isDeclarationVisible,
isPropertyAccessible,
getTypeOnlyAliasDeclaration,
};

function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): Signature | undefined {
Expand Down Expand Up @@ -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);
Expand All @@ -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));
}

Expand Down Expand Up @@ -2590,17 +2590,15 @@ 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;
const relatedMessage = isExport
? 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));
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
)])
)
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
)])),
Expand Down Expand Up @@ -7424,6 +7423,7 @@ namespace ts {
/*importClause*/ undefined,
factory.createNamedImports([
factory.createImportSpecifier(
/*isTypeOnly*/ false,
localName !== verbatimTargetName ? factory.createIdentifier(verbatimTargetName) : undefined,
factory.createIdentifier(localName)
)
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<ImportSpecifier | ExportSpecifier, boolean>(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);
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3441,6 +3441,10 @@ namespace ts {
}

function emitImportOrExportSpecifier(node: ImportOrExportSpecifier) {
if (node.isTypeOnly) {
writeKeyword("type");
writeSpace();
}
if (node.propertyName) {
emit(node.propertyName);
writeSpace();
Expand Down
22 changes: 13 additions & 9 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImportSpecifier>(SyntaxKind.ImportSpecifier);
node.isTypeOnly = isTypeOnly;
node.propertyName = propertyName;
node.name = name;
node.transformFlags |=
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<ExportSpecifier>(SyntaxKind.ExportSpecifier);
node.isTypeOnly = isTypeOnly;
node.propertyName = asName(propertyName);
node.name = asName(name);
node.transformFlags |=
Expand All @@ -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;
}

Expand Down Expand Up @@ -5488,7 +5492,7 @@ namespace ts {
/*modifiers*/ undefined,
/*isTypeOnly*/ false,
createNamedExports([
createExportSpecifier(/*propertyName*/ undefined, exportName)
createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, exportName)
])
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/factory/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit e160bc8

Please sign in to comment.