feat(ivy): reference external classes by their exported name (#27743)

Previously, ngtsc would assume that a given directive/pipe being imported
from an external package was importable using the same name by which it
was declared. This isn't always true; sometimes a package will export a
directive under a different name. For example, Angular frequently prefixes
directive names with the 'ɵ' character to indicate that they're part of
the package's private API, and not for public consumption.

This commit introduces the TsReferenceResolver class which, given a
declaration to import and a module name to import it from, can determine
the exported name of the declared class within the module. This allows
ngtsc to pick the correct name by which to import the class instead of
making assumptions about how it was exported.

This resolver is used to select a correct symbol name when creating an
AbsoluteReference.

FW-517 #resolve
FW-536 #resolve

PR Close #27743
This commit is contained in:
Alex Rickabaugh
2018-12-18 11:09:21 -08:00
committed by Kara Erickson
parent 0b9094ec63
commit 1c39ad38d3
14 changed files with 332 additions and 128 deletions

View File

@ -12,6 +12,7 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/util",
"@ngdeps//@types/node",
"@ngdeps//typescript",
],

View File

@ -7,3 +7,4 @@
*/
export {AbsoluteReference, ImportMode, NodeReference, Reference, ResolvedReference} from './src/references';
export {ReferenceResolver, TsReferenceResolver} from './src/resolver';

View File

@ -0,0 +1,116 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {isFromDtsFile} from '../../util/src/typescript';
import {AbsoluteReference, Reference, ResolvedReference} from './references';
export interface ReferenceResolver {
resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string):
Reference<ts.Declaration>;
}
export class TsReferenceResolver implements ReferenceResolver {
private moduleExportsCache = new Map<string, Map<ts.Declaration, string>|null>();
constructor(
private program: ts.Program, private checker: ts.TypeChecker,
private options: ts.CompilerOptions, private host: ts.CompilerHost) {}
resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string):
Reference<ts.Declaration> {
const id = identifierOfDeclaration(decl);
if (id === undefined) {
throw new Error(`Internal error: don't know how to refer to ${ts.SyntaxKind[decl.kind]}`);
}
if (!isFromDtsFile(decl) || importFromHint === null) {
return new ResolvedReference(decl, id);
} else {
const publicName = this.resolveImportName(importFromHint, decl, fromFile);
if (publicName !== null) {
return new AbsoluteReference(decl, id, importFromHint, publicName);
} else {
throw new Error(`Internal error: Symbol ${id.text} is not exported from ${importFromHint}`);
}
}
}
private resolveImportName(moduleName: string, target: ts.Declaration, fromFile: string): string
|null {
const exports = this.getExportsOfModule(moduleName, fromFile);
if (exports !== null && exports.has(target)) {
return exports.get(target) !;
} else {
return null;
}
}
private getExportsOfModule(moduleName: string, fromFile: string):
Map<ts.Declaration, string>|null {
if (!this.moduleExportsCache.has(moduleName)) {
this.moduleExportsCache.set(moduleName, this.enumerateExportsOfModule(moduleName, fromFile));
}
return this.moduleExportsCache.get(moduleName) !;
}
private enumerateExportsOfModule(moduleName: string, fromFile: string):
Map<ts.Declaration, string>|null {
const resolved = ts.resolveModuleName(moduleName, fromFile, this.options, this.host);
if (resolved.resolvedModule === undefined) {
return null;
}
const indexFile = this.program.getSourceFile(resolved.resolvedModule.resolvedFileName);
if (indexFile === undefined) {
return null;
}
const indexSymbol = this.checker.getSymbolAtLocation(indexFile);
if (indexSymbol === undefined) {
return null;
}
const exportMap = new Map<ts.Declaration, string>();
const exports = this.checker.getExportsOfModule(indexSymbol);
for (const expSymbol of exports) {
const declSymbol = expSymbol.flags & ts.SymbolFlags.Alias ?
this.checker.getAliasedSymbol(expSymbol) :
expSymbol;
const decl = declSymbol.valueDeclaration;
if (decl === undefined) {
continue;
}
if (declSymbol.name === expSymbol.name || !exportMap.has(decl)) {
exportMap.set(decl, expSymbol.name);
}
}
return exportMap;
}
}
function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined {
if (ts.isClassDeclaration(decl)) {
return decl.name;
} else if (ts.isEnumDeclaration(decl)) {
return decl.name;
} else if (ts.isFunctionDeclaration(decl)) {
return decl.name;
} else if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) {
return decl.name;
} else if (ts.isShorthandPropertyAssignment(decl)) {
return decl.name;
} else {
return undefined;
}
}