Use import types to refer to declarations in declaration emit (#24071)

* Stand up a simple implementation using import types for exports of modules which are otherwise inaccessible

* Ensure references exist to link to modules containing utilized ambient modules

* Accept baselines with new import type usage

* Fix lint
This commit is contained in:
Wesley Wigham
2018-05-17 13:08:22 -07:00
committed by GitHub
parent 09b9ec43e3
commit 3fc727b256
77 changed files with 1154 additions and 1637 deletions

View File

@@ -2794,6 +2794,14 @@ namespace ts {
}
return hasAccessibleDeclarations;
}
else {
if (some(symbol.declarations, hasExternalModuleSymbol)) {
// Any meaning of a module symbol is always accessible via an `import` type
return {
accessibility: SymbolAccessibility.Accessible
};
}
}
// If we haven't got the accessible symbol, it doesn't mean the symbol is actually inaccessible.
// It could be a qualified symbol and hence verify the path
@@ -3164,9 +3172,9 @@ namespace ts {
return createTypeReferenceNode(name, /*typeArguments*/ undefined);
}
if (!inTypeAlias && type.aliasSymbol && (context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope || isTypeSymbolAccessible(type.aliasSymbol, context.enclosingDeclaration))) {
const name = symbolToTypeReferenceName(type.aliasSymbol);
const typeArgumentNodes = mapToTypeNodes(type.aliasTypeArguments, context);
return createTypeReferenceNode(name, typeArgumentNodes);
if (isReservedMemberName(type.aliasSymbol.escapedName) && !(type.aliasSymbol.flags & SymbolFlags.Class)) return createTypeReferenceNode(createIdentifier(""), typeArgumentNodes);
return symbolToTypeNode(type.aliasSymbol, context, SymbolFlags.Type, typeArgumentNodes);
}
if (type.flags & (TypeFlags.Union | TypeFlags.Intersection)) {
const types = type.flags & TypeFlags.Union ? formatUnionTypes((<UnionType>type).types) : (<IntersectionType>type).types;
@@ -3329,12 +3337,6 @@ namespace ts {
return setEmitFlags(typeLiteralNode, (context.flags & NodeBuilderFlags.MultilineObjectLiterals) ? 0 : EmitFlags.SingleLine);
}
function symbolToTypeReferenceName(symbol: Symbol) {
// Unnamed function expressions and arrow functions have reserved names that we don't want to display
const entityName = symbol.flags & SymbolFlags.Class || !isReservedMemberName(symbol.escapedName) ? symbolToName(symbol, context, SymbolFlags.Type, /*expectsIdentifier*/ false) : createIdentifier("");
return entityName;
}
function typeReferenceToTypeNode(type: TypeReference) {
const typeArguments: Type[] = type.typeArguments || emptyArray;
if (type.target === globalArrayType) {
@@ -3369,7 +3371,7 @@ namespace ts {
else {
const outerTypeParameters = type.target.outerTypeParameters;
let i = 0;
let qualifiedName: QualifiedName | undefined;
let resultType: TypeReferenceNode | ImportTypeNode;
if (outerTypeParameters) {
const length = outerTypeParameters.length;
while (i < length) {
@@ -3383,64 +3385,66 @@ namespace ts {
// the default outer type arguments), we don't show the group.
if (!rangeEquals(outerTypeParameters, typeArguments, start, i)) {
const typeArgumentSlice = mapToTypeNodes(typeArguments.slice(start, i), context);
const typeArgumentNodes = typeArgumentSlice && createNodeArray(typeArgumentSlice);
const namePart = symbolToTypeReferenceName(parent);
(namePart.kind === SyntaxKind.Identifier ? namePart : namePart.right).typeArguments = typeArgumentNodes;
if (qualifiedName) {
Debug.assert(!qualifiedName.right);
qualifiedName = addToQualifiedNameMissingRightIdentifier(qualifiedName, namePart);
qualifiedName = createQualifiedName(qualifiedName, /*right*/ undefined);
}
else {
qualifiedName = createQualifiedName(namePart, /*right*/ undefined);
}
const flags = context.flags;
context.flags |= NodeBuilderFlags.ForbidIndexedAccessSymbolReferences;
const ref = symbolToTypeNode(parent, context, SymbolFlags.Type, typeArgumentSlice) as TypeReferenceNode | ImportTypeNode;
context.flags = flags;
resultType = !resultType ? ref : appendReferenceToType(resultType, ref as TypeReferenceNode);
}
}
}
let entityName: EntityName;
const nameIdentifier = symbolToTypeReferenceName(type.symbol);
if (qualifiedName) {
Debug.assert(!qualifiedName.right);
qualifiedName = addToQualifiedNameMissingRightIdentifier(qualifiedName, nameIdentifier);
entityName = qualifiedName;
}
else {
entityName = nameIdentifier;
}
let typeArgumentNodes: ReadonlyArray<TypeNode> | undefined;
if (typeArguments.length > 0) {
const typeParameterCount = (type.target.typeParameters || emptyArray).length;
typeArgumentNodes = mapToTypeNodes(typeArguments.slice(i, typeParameterCount), context);
}
if (typeArgumentNodes) {
const lastIdentifier = entityName.kind === SyntaxKind.Identifier ? entityName : entityName.right;
lastIdentifier.typeArguments = undefined;
}
return createTypeReferenceNode(entityName, typeArgumentNodes);
const flags = context.flags;
context.flags |= NodeBuilderFlags.ForbidIndexedAccessSymbolReferences;
const finalRef = symbolToTypeNode(type.symbol, context, SymbolFlags.Type, typeArgumentNodes);
context.flags = flags;
return !resultType ? finalRef : appendReferenceToType(resultType, finalRef as TypeReferenceNode);
}
}
function addToQualifiedNameMissingRightIdentifier(left: QualifiedName, right: Identifier | QualifiedName) {
Debug.assert(left.right === undefined);
if (right.kind === SyntaxKind.Identifier) {
left.right = right;
return left;
function appendReferenceToType(root: TypeReferenceNode | ImportTypeNode, ref: TypeReferenceNode): TypeReferenceNode | ImportTypeNode {
if (isImportTypeNode(root)) {
// first shift type arguments
const innerParams = root.typeArguments;
if (root.qualifier) {
(isIdentifier(root.qualifier) ? root.qualifier : root.qualifier.right).typeArguments = innerParams;
}
root.typeArguments = ref.typeArguments;
// then move qualifiers
const ids = getAccessStack(ref);
for (const id of ids) {
root.qualifier = root.qualifier ? createQualifiedName(root.qualifier, id) : id;
}
return root;
}
let rightPart = right;
while (rightPart.left.kind !== SyntaxKind.Identifier) {
rightPart = rightPart.left;
else {
// first shift type arguments
const innerParams = root.typeArguments;
(isIdentifier(root.typeName) ? root.typeName : root.typeName.right).typeArguments = innerParams;
root.typeArguments = ref.typeArguments;
// then move qualifiers
const ids = getAccessStack(ref);
for (const id of ids) {
root.typeName = createQualifiedName(root.typeName, id);
}
return root;
}
}
left.right = rightPart.left;
rightPart.left = left;
return right;
function getAccessStack(ref: TypeReferenceNode): Identifier[] {
let state = ref.typeName;
const ids = [];
while (!isIdentifier(state)) {
ids.unshift(state.right);
state = state.left;
}
ids.unshift(state);
return ids;
}
function createTypeNodesFromResolvedType(resolvedType: ResolvedType): TypeElement[] {
@@ -3674,7 +3678,7 @@ namespace ts {
}
}
function lookupSymbolChain(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags) {
function lookupSymbolChain(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, yieldModuleSymbol?: boolean) {
context.tracker.trackSymbol(symbol, context.enclosingDeclaration, meaning);
// Try to get qualified name if the symbol is not a type parameter and there is an enclosing declaration.
let chain: Symbol[];
@@ -3714,7 +3718,7 @@ namespace ts {
// If this is the last part of outputting the symbol, always output. The cases apply only to parent symbols.
endOfChain ||
// If a parent symbol is an external module, don't write it. (We prefer just `x` vs `"foo/bar".x`.)
!(!parentSymbol && forEach(symbol.declarations, hasExternalModuleSymbol)) &&
(yieldModuleSymbol || !(!parentSymbol && forEach(symbol.declarations, hasExternalModuleSymbol))) &&
// If a parent symbol is an anonymous type, don't write it.
!(symbol.flags & (SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral))) {
@@ -3762,8 +3766,8 @@ namespace ts {
return top;
}
function symbolToTypeNode(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags): TypeNode {
const chain = lookupSymbolChain(symbol, context, meaning);
function symbolToTypeNode(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, overrideTypeArguments?: ReadonlyArray<TypeNode>): TypeNode {
const chain = lookupSymbolChain(symbol, context, meaning, !(context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope)); // If we're using aliases outside the current scope, dont bother with the module
context.flags |= NodeBuilderFlags.InInitialEntityName;
const rootName = getNameOfSymbolAsWritten(chain[0], context);
@@ -3773,9 +3777,13 @@ namespace ts {
if (ambientModuleSymbolRegex.test(rootName)) {
// module is root, must use `ImportTypeNode`
const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined;
const typeParameterNodes = lookupTypeParameterNodes(chain, 0, context);
const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context);
const lit = createLiteralTypeNode(createLiteral(rootName.substring(1, rootName.length - 1)));
if (!nonRootParts || isEntityName(nonRootParts)) {
if (nonRootParts) {
const lastId = isIdentifier(nonRootParts) ? nonRootParts : (nonRootParts as QualifiedName).right;
lastId.typeArguments = undefined;
}
return createImportTypeNode(lit, nonRootParts as EntityName, typeParameterNodes as ReadonlyArray<TypeNode>, isTypeOf);
}
else {
@@ -3789,10 +3797,18 @@ namespace ts {
if (isIndexedAccessTypeNode(entityName)) {
return entityName; // Indexed accesses can never be `typeof`
}
return isTypeOf ? createTypeQueryNode(entityName) : createTypeReferenceNode(entityName, /*typeArguments*/ undefined);
if (isTypeOf) {
return createTypeQueryNode(entityName);
}
else {
const lastId = isIdentifier(entityName) ? entityName : entityName.right;
const lastTypeArgs = lastId.typeArguments;
lastId.typeArguments = undefined;
return createTypeReferenceNode(entityName, lastTypeArgs as NodeArray<TypeNode>);
}
function createAccessFromSymbolChain(chain: Symbol[], index: number, stopper: number): EntityName | IndexedAccessTypeNode {
const typeParameterNodes = lookupTypeParameterNodes(chain, index, context);
const typeParameterNodes = index === (chain.length - 1) ? overrideTypeArguments : lookupTypeParameterNodes(chain, index, context);
const symbol = chain[index];
if (index === 0) {
@@ -3804,7 +3820,7 @@ namespace ts {
}
const parent = chain[index - 1];
if (parent && getMembersOfSymbol(parent) && getMembersOfSymbol(parent).get(symbol.escapedName) === symbol) {
if (!(context.flags & NodeBuilderFlags.ForbidIndexedAccessSymbolReferences) && parent && getMembersOfSymbol(parent) && getMembersOfSymbol(parent).get(symbol.escapedName) === symbol) {
// Should use an indexed access
const LHS = createAccessFromSymbolChain(chain, index - 1, stopper);
if (isIndexedAccessTypeNode(LHS)) {
@@ -4007,6 +4023,23 @@ namespace ts {
return "default";
}
if (symbol.declarations && symbol.declarations.length) {
if (some(symbol.declarations, hasExternalModuleSymbol) && context.enclosingDeclaration) {
const file = getDeclarationOfKind<SourceFile>(symbol, SyntaxKind.SourceFile);
if (!file || !context.tracker.moduleResolverHost) {
if (context.tracker.trackReferencedAmbientModule) {
const ambientDecls = filter(symbol.declarations, isAmbientModule);
if (length(ambientDecls)) {
for (const decl of ambientDecls) {
context.tracker.trackReferencedAmbientModule(decl);
}
}
}
// ambient module, just use declaration/symbol name (fallthrough)
}
else {
return `"${getResolvedExternalModuleName(context.tracker.moduleResolverHost, file, getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration)))}"`;
}
}
const declaration = symbol.declarations[0];
const name = getNameOfDeclaration(declaration);
if (name) {

View File

@@ -37,20 +37,23 @@ namespace ts {
let lateStatementReplacementMap: Map<VisitResult<LateVisibilityPaintedStatement>>;
let suppressNewDiagnosticContexts: boolean;
const host = context.getEmitHost();
const symbolTracker: SymbolTracker = {
trackSymbol,
reportInaccessibleThisError,
reportInaccessibleUniqueSymbolError,
reportPrivateInBaseOfClassExpression
reportPrivateInBaseOfClassExpression,
moduleResolverHost: host,
trackReferencedAmbientModule,
};
let errorNameNode: DeclarationName | undefined;
let currentSourceFile: SourceFile;
let refs: Map<SourceFile>;
const resolver = context.getEmitResolver();
const options = context.getCompilerOptions();
const newLine = getNewLineCharacter(options);
const { noResolve, stripInternal } = options;
const host = context.getEmitHost();
return transformRoot;
function recordTypeReferenceDirectivesIfNecessary(typeReferenceDirectives: string[]): void {
@@ -63,6 +66,11 @@ namespace ts {
}
}
function trackReferencedAmbientModule(node: ModuleDeclaration) {
const container = getSourceFileOfNode(node);
refs.set("" + getOriginalNodeId(container), container);
}
function handleSymbolAccessibilityError(symbolAccessibilityResult: SymbolAccessibilityResult) {
if (symbolAccessibilityResult.accessibility === SymbolAccessibility.Accessible) {
// Add aliases back onto the possible imports list if they're not there so we can try them again with updated visibility info
@@ -197,13 +205,13 @@ namespace ts {
lateMarkedStatements = undefined;
lateStatementReplacementMap = createMap();
necessaryTypeRefernces = undefined;
const refs = collectReferences(currentSourceFile, createMap());
refs = collectReferences(currentSourceFile, createMap());
const references: FileReference[] = [];
const outputFilePath = getDirectoryPath(normalizeSlashes(getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath));
const referenceVisitor = mapReferencesIntoArray(references, outputFilePath);
refs.forEach(referenceVisitor);
const statements = visitNodes(node.statements, visitDeclarationStatements);
let combinedStatements = setTextRange(createNodeArray(transformAndReplaceLatePaintedStatements(statements)), node.statements);
refs.forEach(referenceVisitor);
const emittedImports = filter(combinedStatements, isAnyImportSyntax);
if (isExternalModule(node) && (!resultHasExternalModuleIndicator || (needsScopeFixMarker && !resultHasScopeMarker))) {
combinedStatements = setTextRange(createNodeArray([...combinedStatements, createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createNamedExports([]), /*moduleSpecifier*/ undefined)]), combinedStatements);

View File

@@ -3093,7 +3093,7 @@ namespace ts {
WriteArrayAsGenericType = 1 << 1, // Write Array<T> instead T[]
GenerateNamesForShadowedTypeParams = 1 << 2, // When a type parameter T is shadowing another T, generate a name for it so it can still be referenced
UseStructuralFallback = 1 << 3, // When an alias cannot be named by its symbol, rather than report an error, fallback to a structural printout if possible
// empty space
ForbidIndexedAccessSymbolReferences = 1 << 4, // Forbid references like `I["a"]["b"]` - print `typeof I.a<x>.b<y>` instead
WriteTypeArgumentsOfSignature = 1 << 5, // Write the type arguments instead of type parameters of the signature
UseFullyQualifiedType = 1 << 6, // Write out the fully qualified type name (eg. Module.Type, instead of Type)
UseOnlyExternalAliasing = 1 << 7, // Only use external aliases for a symbol
@@ -5258,6 +5258,13 @@ namespace ts {
isAtStartOfLine(): boolean;
}
/* @internal */
export interface ModuleNameResolverHost {
getCanonicalFileName(f: string): string;
getCommonSourceDirectory(): string;
getCurrentDirectory(): string;
}
/** @deprecated See comment on SymbolWriter */
// Note: this has non-deprecated internal uses.
export interface SymbolTracker {
@@ -5268,6 +5275,10 @@ namespace ts {
reportInaccessibleThisError?(): void;
reportPrivateInBaseOfClassExpression?(propertyName: string): void;
reportInaccessibleUniqueSymbolError?(): void;
/* @internal */
moduleResolverHost?: ModuleNameResolverHost;
/* @internal */
trackReferencedAmbientModule?(decl: ModuleDeclaration): void;
}
export interface TextSpan {

View File

@@ -2878,11 +2878,11 @@ namespace ts {
};
}
export function getResolvedExternalModuleName(host: EmitHost, file: SourceFile): string {
return file.moduleName || getExternalModuleNameFromPath(host, file.fileName);
export function getResolvedExternalModuleName(host: ModuleNameResolverHost, file: SourceFile, referenceFile?: SourceFile): string {
return file.moduleName || getExternalModuleNameFromPath(host, file.fileName, referenceFile && referenceFile.fileName);
}
export function getExternalModuleNameFromDeclaration(host: EmitHost, resolver: EmitResolver, declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode): string {
export function getExternalModuleNameFromDeclaration(host: ModuleNameResolverHost, resolver: EmitResolver, declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode): string {
const file = resolver.getExternalModuleFileFromDeclaration(declaration);
if (!file || file.isDeclarationFile) {
return undefined;
@@ -2893,12 +2893,13 @@ namespace ts {
/**
* Resolves a local path to a path which is absolute to the base of the emit
*/
export function getExternalModuleNameFromPath(host: EmitHost, fileName: string): string {
export function getExternalModuleNameFromPath(host: ModuleNameResolverHost, fileName: string, referencePath?: string): string {
const getCanonicalFileName = (f: string) => host.getCanonicalFileName(f);
const dir = toPath(host.getCommonSourceDirectory(), host.getCurrentDirectory(), getCanonicalFileName);
const dir = toPath(referencePath ? getDirectoryPath(referencePath) : host.getCommonSourceDirectory(), host.getCurrentDirectory(), getCanonicalFileName);
const filePath = getNormalizedAbsolutePath(fileName, host.getCurrentDirectory());
const relativePath = getRelativePathToDirectoryOrUrl(dir, filePath, dir, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
return removeFileExtension(relativePath);
const extensionless = removeFileExtension(relativePath);
return referencePath ? ensurePathIsNonModuleName(extensionless) : extensionless;
}
export function getOwnEmitOutputFilePath(sourceFile: SourceFile, host: EmitHost, extension: string) {