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

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ConstantPool} from '@angular/compiler'; import {ConstantPool} from '@angular/compiler';
import {TsReferenceResolver} from '@angular/compiler-cli/src/ngtsc/imports';
import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator'; import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
import * as path from 'canonical-path'; import * as path from 'canonical-path';
import * as fs from 'fs'; import * as fs from 'fs';
@ -59,8 +60,9 @@ export class FileResourceLoader implements ResourceLoader {
*/ */
export class DecorationAnalyzer { export class DecorationAnalyzer {
resourceLoader = new FileResourceLoader(); resourceLoader = new FileResourceLoader();
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost); resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host);
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker); scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver);
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver);
handlers: DecoratorHandler<any, any>[] = [ handlers: DecoratorHandler<any, any>[] = [
new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator), new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator),
new ComponentDecoratorHandler( new ComponentDecoratorHandler(

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../../ngtsc/imports'; import {Reference, TsReferenceResolver} from '../../../ngtsc/imports';
import {PartialEvaluator} from '../../../ngtsc/partial_evaluator'; import {PartialEvaluator} from '../../../ngtsc/partial_evaluator';
import {TypeScriptReflectionHost} from '../../../ngtsc/reflection'; import {TypeScriptReflectionHost} from '../../../ngtsc/reflection';
import {getDeclaration, makeProgram} from '../../../ngtsc/testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../../ngtsc/testing/in_memory_typescript';
@ -16,7 +16,7 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr
describe('NgccReferencesRegistry', () => { describe('NgccReferencesRegistry', () => {
it('should return a mapping from resolved reference identifiers to their declarations', () => { it('should return a mapping from resolved reference identifiers to their declarations', () => {
const {program} = makeProgram([{ const {program, options, host} = makeProgram([{
name: 'index.ts', name: 'index.ts',
contents: ` contents: `
export class SomeClass {} export class SomeClass {}
@ -38,9 +38,10 @@ describe('NgccReferencesRegistry', () => {
getDeclaration(program, 'index.ts', 'someVariable', ts.isVariableDeclaration); getDeclaration(program, 'index.ts', 'someVariable', ts.isVariableDeclaration);
const testArrayExpression = testArrayDeclaration.initializer !; const testArrayExpression = testArrayDeclaration.initializer !;
const host = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(host, checker); const resolver = new TsReferenceResolver(program, checker, options, host);
const registry = new NgccReferencesRegistry(host); const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const registry = new NgccReferencesRegistry(reflectionHost);
const references = evaluator.evaluate(testArrayExpression) as Reference<ts.Declaration>[]; const references = evaluator.evaluate(testArrayExpression) as Reference<ts.Declaration>[];
registry.add(null !, ...references); registry.add(null !, ...references);

View File

@ -9,7 +9,7 @@
import {Expression, WrappedNodeExpr} from '@angular/compiler'; import {Expression, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteReference, Reference, ResolvedReference} from '../../imports'; import {AbsoluteReference, Reference, ReferenceResolver, ResolvedReference} from '../../imports';
import {ReflectionHost, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectTypeEntityToDeclaration} from '../../reflection'; import {ReflectionHost, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectTypeEntityToDeclaration} from '../../reflection';
import {TypeCheckableDirectiveMeta} from '../../typecheck'; import {TypeCheckableDirectiveMeta} from '../../typecheck';
@ -89,7 +89,9 @@ export class SelectorScopeRegistry {
*/ */
private _declararedTypeToModule = new Map<ts.Declaration, ts.Declaration>(); private _declararedTypeToModule = new Map<ts.Declaration, ts.Declaration>();
constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {} constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private resolver: ReferenceResolver) {}
/** /**
* Register a module's metadata with the registry. * Register a module's metadata with the registry.
@ -161,7 +163,9 @@ export class SelectorScopeRegistry {
// Process the declaration scope of the module, and lookup the selector of every declared type. // Process the declaration scope of the module, and lookup the selector of every declared type.
// The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule // The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule
// was not imported from a .d.ts source. // was not imported from a .d.ts source.
for (const ref of this.lookupScopesOrDie(module !, /* ngModuleImportedFrom */ null) for (const ref of this
.lookupScopesOrDie(
module !, /* ngModuleImportedFrom */ null, node.getSourceFile().fileName)
.compilation) { .compilation) {
const node = ts.getOriginalNode(ref.node) as ts.Declaration; const node = ts.getOriginalNode(ref.node) as ts.Declaration;
@ -203,9 +207,10 @@ export class SelectorScopeRegistry {
return scope !== null ? convertScopeToExpressions(scope, node) : null; return scope !== null ? convertScopeToExpressions(scope, node) : null;
} }
private lookupScopesOrDie(node: ts.Declaration, ngModuleImportedFrom: string|null): private lookupScopesOrDie(
SelectorScopes { node: ts.Declaration, ngModuleImportedFrom: string|null,
const result = this.lookupScopes(node, ngModuleImportedFrom); resolutionContext: string): SelectorScopes {
const result = this.lookupScopes(node, ngModuleImportedFrom, resolutionContext);
if (result === null) { if (result === null) {
throw new Error(`Module not found: ${reflectNameOfDeclaration(node)}`); throw new Error(`Module not found: ${reflectNameOfDeclaration(node)}`);
} }
@ -219,8 +224,9 @@ export class SelectorScopeRegistry {
* (`ngModuleImportedFrom`) then all of its declarations are exported at that same path, as well * (`ngModuleImportedFrom`) then all of its declarations are exported at that same path, as well
* as imports and exports from other modules that are relatively imported. * as imports and exports from other modules that are relatively imported.
*/ */
private lookupScopes(node: ts.Declaration, ngModuleImportedFrom: string|null): SelectorScopes private lookupScopes(
|null { node: ts.Declaration, ngModuleImportedFrom: string|null,
resolutionContext: string): SelectorScopes|null {
let data: ModuleData|null = null; let data: ModuleData|null = null;
// Either this module was analyzed directly, or has a precompiled ngModuleDef. // Either this module was analyzed directly, or has a precompiled ngModuleDef.
@ -230,7 +236,7 @@ export class SelectorScopeRegistry {
} else { } else {
// The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type // The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type
// annotation that specifies the needed metadata. // annotation that specifies the needed metadata.
data = this._readModuleDataFromCompiledClass(node, ngModuleImportedFrom); data = this._readModuleDataFromCompiledClass(node, ngModuleImportedFrom, resolutionContext);
// Note that data here could still be null, if the class didn't have a precompiled // Note that data here could still be null, if the class didn't have a precompiled
// ngModuleDef. // ngModuleDef.
} }
@ -239,22 +245,28 @@ export class SelectorScopeRegistry {
return null; return null;
} }
const context = node.getSourceFile().fileName;
return { return {
compilation: [ compilation: [
...data.declarations, ...data.declarations,
// Expand imports to the exported scope of those imports. // Expand imports to the exported scope of those imports.
...flatten(data.imports.map( ...flatten(data.imports.map(
ref => this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref)) ref =>
this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref), context)
.exported)), .exported)),
// And include the compilation scope of exported modules. // And include the compilation scope of exported modules.
...flatten( ...flatten(
data.exports data.exports
.map(ref => this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref))) .map(
ref => this.lookupScopes(
ref.node as ts.Declaration, absoluteModuleName(ref), context))
.filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null) .filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null)
.map(scope => scope.exported)) .map(scope => scope.exported))
], ],
exported: flatten(data.exports.map(ref => { exported: flatten(data.exports.map(ref => {
const scope = this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref)); const scope =
this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref), context);
if (scope !== null) { if (scope !== null) {
return scope.exported; return scope.exported;
} else { } else {
@ -297,7 +309,8 @@ export class SelectorScopeRegistry {
* stemming from this module. * stemming from this module.
*/ */
private _readModuleDataFromCompiledClass( private _readModuleDataFromCompiledClass(
clazz: ts.Declaration, ngModuleImportedFrom: string|null): ModuleData|null { clazz: ts.Declaration, ngModuleImportedFrom: string|null,
resolutionContext: string): ModuleData|null {
// This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`. // This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`.
// TODO(alxhub): investigate caching of .d.ts module metadata. // TODO(alxhub): investigate caching of .d.ts module metadata.
const ngModuleDef = this.reflector.getMembersOfClass(clazz).find( const ngModuleDef = this.reflector.getMembersOfClass(clazz).find(
@ -315,9 +328,12 @@ export class SelectorScopeRegistry {
// Read the ModuleData out of the type arguments. // Read the ModuleData out of the type arguments.
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments; const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
return { return {
declarations: this._extractReferencesFromType(declarationMetadata, ngModuleImportedFrom), declarations: this._extractReferencesFromType(
exports: this._extractReferencesFromType(exportMetadata, ngModuleImportedFrom), declarationMetadata, ngModuleImportedFrom, resolutionContext),
imports: this._extractReferencesFromType(importMetadata, ngModuleImportedFrom), exports:
this._extractReferencesFromType(exportMetadata, ngModuleImportedFrom, resolutionContext),
imports:
this._extractReferencesFromType(importMetadata, ngModuleImportedFrom, resolutionContext),
}; };
} }
@ -389,8 +405,9 @@ export class SelectorScopeRegistry {
* This operation assumes that these types should be imported from `ngModuleImportedFrom` unless * This operation assumes that these types should be imported from `ngModuleImportedFrom` unless
* they themselves were imported from another absolute path. * they themselves were imported from another absolute path.
*/ */
private _extractReferencesFromType(def: ts.TypeNode, ngModuleImportedFrom: string|null): private _extractReferencesFromType(
Reference<ts.Declaration>[] { def: ts.TypeNode, ngModuleImportedFrom: string|null,
resolutionContext: string): Reference<ts.Declaration>[] {
if (!ts.isTupleTypeNode(def)) { if (!ts.isTupleTypeNode(def)) {
return []; return [];
} }
@ -402,12 +419,10 @@ export class SelectorScopeRegistry {
if (ngModuleImportedFrom !== null) { if (ngModuleImportedFrom !== null) {
const {node, from} = reflectTypeEntityToDeclaration(type, this.checker); const {node, from} = reflectTypeEntityToDeclaration(type, this.checker);
const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
const id = reflectIdentifierOfDeclaration(node); return this.resolver.resolve(node, moduleName, resolutionContext);
return new AbsoluteReference(node, id !, moduleName, id !.text);
} else { } else {
const {node} = reflectTypeEntityToDeclaration(type, this.checker); const {node} = reflectTypeEntityToDeclaration(type, this.checker);
const id = reflectIdentifierOfDeclaration(node); return this.resolver.resolve(node, null, resolutionContext);
return new ResolvedReference(node, id !);
} }
}); });
} }

View File

@ -9,9 +9,9 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {TsReferenceResolver} from '../../imports';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost} from '../../reflection'; import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {ResourceLoader} from '../src/api'; import {ResourceLoader} from '../src/api';
import {ComponentDecoratorHandler} from '../src/component'; import {ComponentDecoratorHandler} from '../src/component';
@ -23,7 +23,7 @@ export class NoopResourceLoader implements ResourceLoader {
describe('ComponentDecoratorHandler', () => { describe('ComponentDecoratorHandler', () => {
it('should produce a diagnostic when @Component has non-literal argument', () => { it('should produce a diagnostic when @Component has non-literal argument', () => {
const {program} = makeProgram([ const {program, options, host} = makeProgram([
{ {
name: 'node_modules/@angular/core/index.d.ts', name: 'node_modules/@angular/core/index.d.ts',
contents: 'export const Component: any;', contents: 'export const Component: any;',
@ -39,13 +39,14 @@ describe('ComponentDecoratorHandler', () => {
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(host, checker); const resolver = new TsReferenceResolver(program, checker, options, host);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const handler = new ComponentDecoratorHandler( const handler = new ComponentDecoratorHandler(
host, evaluator, new SelectorScopeRegistry(checker, host), false, new NoopResourceLoader(), reflectionHost, evaluator, new SelectorScopeRegistry(checker, reflectionHost, resolver),
[''], false, true); false, new NoopResourceLoader(), [''], false, true);
const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration); const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration);
const detected = handler.detect(TestCmp, host.getDecoratorsOfDeclaration(TestCmp)); const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) { if (detected === undefined) {
return fail('Failed to recognize @Component'); return fail('Failed to recognize @Component');
} }

View File

@ -8,15 +8,14 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteReference, ResolvedReference} from '../../imports'; import {AbsoluteReference, ResolvedReference, TsReferenceResolver} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection'; import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {SelectorScopeRegistry} from '../src/selector_scope'; import {SelectorScopeRegistry} from '../src/selector_scope';
describe('SelectorScopeRegistry', () => { describe('SelectorScopeRegistry', () => {
it('absolute imports work', () => { it('absolute imports work', () => {
const {program} = makeProgram([ const {program, options, host} = makeProgram([
{ {
name: 'node_modules/@angular/core/index.d.ts', name: 'node_modules/@angular/core/index.d.ts',
contents: ` contents: `
@ -29,6 +28,7 @@ describe('SelectorScopeRegistry', () => {
contents: ` contents: `
import {NgModuleDef} from '@angular/core'; import {NgModuleDef} from '@angular/core';
import * as i0 from './component'; import * as i0 from './component';
export {SomeCmp} from './component';
export declare class SomeModule { export declare class SomeModule {
static ngModuleDef: NgModuleDef<SomeModule, [typeof i0.SomeCmp], never, [typeof i0.SomeCmp]>; static ngModuleDef: NgModuleDef<SomeModule, [typeof i0.SomeCmp], never, [typeof i0.SomeCmp]>;
@ -54,7 +54,7 @@ describe('SelectorScopeRegistry', () => {
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const ProgramModule = const ProgramModule =
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration); getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration); const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration);
@ -65,7 +65,8 @@ describe('SelectorScopeRegistry', () => {
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
const registry = new SelectorScopeRegistry(checker, host); const resolver = new TsReferenceResolver(program, checker, options, host);
const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver);
registry.registerModule(ProgramModule, { registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],
@ -95,7 +96,7 @@ describe('SelectorScopeRegistry', () => {
}); });
it('exports of third-party libs work', () => { it('exports of third-party libs work', () => {
const {program} = makeProgram([ const {program, options, host} = makeProgram([
{ {
name: 'node_modules/@angular/core/index.d.ts', name: 'node_modules/@angular/core/index.d.ts',
contents: ` contents: `
@ -126,7 +127,7 @@ describe('SelectorScopeRegistry', () => {
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const ProgramModule = const ProgramModule =
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration); getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration); const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration);
@ -137,7 +138,8 @@ describe('SelectorScopeRegistry', () => {
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
const registry = new SelectorScopeRegistry(checker, host); const resolver = new TsReferenceResolver(program, checker, options, host);
const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver);
registry.registerModule(ProgramModule, { registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],

View File

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

View File

@ -7,3 +7,4 @@
*/ */
export {AbsoluteReference, ImportMode, NodeReference, Reference, ResolvedReference} from './src/references'; 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;
}
}

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference, ReferenceResolver} from '../../imports';
import {ReflectionHost} from '../../reflection'; import {ReflectionHost} from '../../reflection';
import {StaticInterpreter} from './interpreter'; import {StaticInterpreter} from './interpreter';
@ -19,12 +19,15 @@ export type ForeignFunctionResolver =
ts.Expression | null; ts.Expression | null;
export class PartialEvaluator { export class PartialEvaluator {
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {} constructor(
private host: ReflectionHost, private checker: ts.TypeChecker,
private refResolver: ReferenceResolver) {}
evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue { evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue {
const interpreter = new StaticInterpreter(this.host, this.checker); const interpreter = new StaticInterpreter(this.host, this.checker, this.refResolver);
return interpreter.visit(expr, { return interpreter.visit(expr, {
absoluteModuleName: null, absoluteModuleName: null,
resolutionContext: expr.getSourceFile().fileName,
scope: new Map<ts.ParameterDeclaration, ResolvedValue>(), foreignFunctionResolver, scope: new Map<ts.ParameterDeclaration, ResolvedValue>(), foreignFunctionResolver,
}); });
} }

View File

@ -8,8 +8,8 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteReference, NodeReference, Reference, ResolvedReference} from '../../imports'; import {AbsoluteReference, NodeReference, Reference, ReferenceResolver, ResolvedReference} from '../../imports';
import {ReflectionHost} from '../../reflection'; import {Declaration, ReflectionHost} from '../../reflection';
import {ArraySliceBuiltinFn} from './builtin'; import {ArraySliceBuiltinFn} from './builtin';
import {BuiltinFn, DYNAMIC_VALUE, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap, isDynamicValue} from './result'; import {BuiltinFn, DYNAMIC_VALUE, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap, isDynamicValue} from './result';
@ -61,7 +61,16 @@ const UNARY_OPERATORS = new Map<ts.SyntaxKind, (a: any) => any>([
]); ]);
interface Context { interface Context {
/**
* The module name (if any) which was used to reach the currently resolving symbols.
*/
absoluteModuleName: string|null; absoluteModuleName: string|null;
/**
* A file name representing the context in which the current `absoluteModuleName`, if any, was
* resolved.
*/
resolutionContext: string;
scope: Scope; scope: Scope;
foreignFunctionResolver? foreignFunctionResolver?
(ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>, (ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>,
@ -69,7 +78,9 @@ interface Context {
} }
export class StaticInterpreter { export class StaticInterpreter {
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {} constructor(
private host: ReflectionHost, private checker: ts.TypeChecker,
private refResolver: ReferenceResolver) {}
visit(node: ts.Expression, context: Context): ResolvedValue { visit(node: ts.Expression, context: Context): ResolvedValue {
return this.visitExpression(node, context); return this.visitExpression(node, context);
@ -203,8 +214,8 @@ export class StaticInterpreter {
if (decl === null) { if (decl === null) {
return DYNAMIC_VALUE; return DYNAMIC_VALUE;
} }
const result = this.visitDeclaration( const result =
decl.node, {...context, absoluteModuleName: decl.viaModule || context.absoluteModuleName}); this.visitDeclaration(decl.node, {...context, ...joinModuleContext(context, node, decl)});
if (result instanceof Reference) { if (result instanceof Reference) {
result.addIdentifier(node); result.addIdentifier(node);
} }
@ -292,9 +303,9 @@ export class StaticInterpreter {
} }
const map = new Map<string, ResolvedValue>(); const map = new Map<string, ResolvedValue>();
declarations.forEach((decl, name) => { declarations.forEach((decl, name) => {
const value = this.visitDeclaration(decl.node, { const value = this.visitDeclaration(
...context, decl.node, {
absoluteModuleName: decl.viaModule || context.absoluteModuleName, ...context, ...joinModuleContext(context, node, decl),
}); });
map.set(name, value); map.set(name, value);
}); });
@ -381,12 +392,16 @@ export class StaticInterpreter {
// If the function is declared in a different file, resolve the foreign function expression // If the function is declared in a different file, resolve the foreign function expression
// using the absolute module name of that file (if any). // using the absolute module name of that file (if any).
let absoluteModuleName: string|null = context.absoluteModuleName; if ((lhs instanceof NodeReference || lhs instanceof AbsoluteReference) &&
if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) { lhs.moduleName !== null) {
absoluteModuleName = lhs.moduleName || absoluteModuleName; context = {
...context,
absoluteModuleName: lhs.moduleName,
resolutionContext: node.getSourceFile().fileName,
};
} }
return this.visitExpression(expr, {...context, absoluteModuleName}); return this.visitExpression(expr, context);
} }
const body = fn.body; const body = fn.body;
@ -473,17 +488,7 @@ export class StaticInterpreter {
} }
private getReference(node: ts.Declaration, context: Context): Reference { private getReference(node: ts.Declaration, context: Context): Reference {
const id = identifierOfDeclaration(node); return this.refResolver.resolve(node, context.absoluteModuleName, context.resolutionContext);
if (id === undefined) {
throw new Error(`Don't know how to refer to ${ts.SyntaxKind[node.kind]}`);
}
if (context.absoluteModuleName !== null) {
// TODO(alxhub): investigate whether this can get symbol names wrong in the event of
// re-exports under different names.
return new AbsoluteReference(node, id, context.absoluteModuleName, id.text);
} else {
return new ResolvedReference(node, id);
}
} }
} }
@ -504,22 +509,6 @@ function literal(value: ResolvedValue): any {
throw new Error(`Value ${value} is not literal and cannot be used in this context.`); throw new Error(`Value ${value} is not literal and cannot be used in this context.`);
} }
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;
}
}
function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean { function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean {
if (node.parent === undefined || !ts.isVariableDeclarationList(node.parent)) { if (node.parent === undefined || !ts.isVariableDeclarationList(node.parent)) {
return false; return false;
@ -532,3 +521,19 @@ function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean {
return varStmt.modifiers !== undefined && return varStmt.modifiers !== undefined &&
varStmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword); varStmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword);
} }
const EMPTY = {};
function joinModuleContext(existing: Context, node: ts.Node, decl: Declaration): {
absoluteModuleName?: string,
resolutionContext?: string,
} {
if (decl.viaModule !== null && decl.viaModule !== existing.absoluteModuleName) {
return {
absoluteModuleName: decl.viaModule,
resolutionContext: node.getSourceFile().fileName,
};
} else {
return EMPTY;
}
}

View File

@ -9,7 +9,7 @@
import {WrappedNodeExpr} from '@angular/compiler'; import {WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteReference, Reference} from '../../imports'; import {AbsoluteReference, Reference, TsReferenceResolver} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection'; import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {PartialEvaluator} from '../src/interface'; import {PartialEvaluator} from '../src/interface';
@ -42,9 +42,10 @@ function makeExpression(
function evaluate<T extends ResolvedValue>( function evaluate<T extends ResolvedValue>(
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): T { code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): T {
const {expression, checker} = makeExpression(code, expr, supportingFiles); const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles);
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker); const resolver = new TsReferenceResolver(program, checker, options, host);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
return evaluator.evaluate(expression) as T; return evaluator.evaluate(expression) as T;
} }
@ -135,7 +136,7 @@ describe('ngtsc metadata', () => {
}); });
it('imports work', () => { it('imports work', () => {
const {program} = makeProgram([ const {program, options, host} = makeProgram([
{name: 'second.ts', contents: 'export function foo(bar) { return bar; }'}, {name: 'second.ts', contents: 'export function foo(bar) { return bar; }'},
{ {
name: 'entry.ts', name: 'entry.ts',
@ -149,7 +150,8 @@ describe('ngtsc metadata', () => {
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
const evaluator = new PartialEvaluator(reflectionHost, checker); const resolver = new TsReferenceResolver(program, checker, options, host);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const resolved = evaluator.evaluate(expr); const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Reference)) { if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference'); return fail('Expected expression to resolve to a reference');
@ -167,7 +169,7 @@ describe('ngtsc metadata', () => {
}); });
it('absolute imports work', () => { it('absolute imports work', () => {
const {program} = makeProgram([ const {program, options, host} = makeProgram([
{name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'}, {name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'},
{ {
name: 'entry.ts', name: 'entry.ts',
@ -181,7 +183,8 @@ describe('ngtsc metadata', () => {
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
const evaluator = new PartialEvaluator(reflectionHost, checker); const resolver = new TsReferenceResolver(program, checker, options, host);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const resolved = evaluator.evaluate(expr); const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof AbsoluteReference)) { if (!(resolved instanceof AbsoluteReference)) {
return fail('Expected expression to resolve to an absolute reference'); return fail('Expected expression to resolve to an absolute reference');

View File

@ -16,7 +16,7 @@ import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecorato
import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {ErrorCode, ngErrorCode} from './diagnostics'; import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {Reference} from './imports'; import {Reference, TsReferenceResolver} from './imports';
import {PartialEvaluator} from './partial_evaluator'; import {PartialEvaluator} from './partial_evaluator';
import {TypeScriptReflectionHost} from './reflection'; import {TypeScriptReflectionHost} from './reflection';
import {FileResourceLoader, HostResourceLoader} from './resource_loader'; import {FileResourceLoader, HostResourceLoader} from './resource_loader';
@ -274,8 +274,9 @@ export class NgtscProgram implements api.Program {
private makeCompilation(): IvyCompilation { private makeCompilation(): IvyCompilation {
const checker = this.tsProgram.getTypeChecker(); const checker = this.tsProgram.getTypeChecker();
const evaluator = new PartialEvaluator(this.reflector, checker); const refResolver = new TsReferenceResolver(this.tsProgram, checker, this.options, this.host);
const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector); const evaluator = new PartialEvaluator(this.reflector, checker, refResolver);
const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, refResolver);
// If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in // If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in
// order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there // order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there

View File

@ -9,6 +9,8 @@
const TS = /\.tsx?$/i; const TS = /\.tsx?$/i;
const D_TS = /\.d\.ts$/i; const D_TS = /\.d\.ts$/i;
import * as ts from 'typescript';
export function isDtsPath(filePath: string): boolean { export function isDtsPath(filePath: string): boolean {
return D_TS.test(filePath); return D_TS.test(filePath);
} }
@ -16,3 +18,11 @@ export function isDtsPath(filePath: string): boolean {
export function isNonDeclarationTsPath(filePath: string): boolean { export function isNonDeclarationTsPath(filePath: string): boolean {
return TS.test(filePath) && !D_TS.test(filePath); return TS.test(filePath) && !D_TS.test(filePath);
} }
export function isFromDtsFile(node: ts.Node): boolean {
let sf: ts.SourceFile|undefined = node.getSourceFile();
if (sf === undefined) {
sf = ts.getOriginalNode(node).getSourceFile();
}
return sf !== undefined && D_TS.test(sf.fileName);
}

View File

@ -420,10 +420,12 @@ describe('ngtsc behavioral tests', () => {
env.write('node_modules/router/index.d.ts', ` env.write('node_modules/router/index.d.ts', `
import {ModuleWithProviders} from '@angular/core'; import {ModuleWithProviders} from '@angular/core';
import * as internal from './internal'; import * as internal from './internal';
export {InternalRouterModule} from './internal';
declare class RouterModule { declare export class RouterModule {
static forRoot(): ModuleWithProviders<internal.InternalRouterModule>; static forRoot(): ModuleWithProviders<internal.InternalRouterModule>;
} }
`); `);
env.write('node_modules/router/internal.d.ts', ` env.write('node_modules/router/internal.d.ts', `
@ -1195,6 +1197,7 @@ describe('ngtsc behavioral tests', () => {
// Success is enough to indicate that this passes. // Success is enough to indicate that this passes.
}); });
describe('when processing external directives', () => {
it('should not emit multiple references to the same directive', () => { it('should not emit multiple references to the same directive', () => {
env.tsconfig(); env.tsconfig();
env.write('node_modules/external/index.d.ts', ` env.write('node_modules/external/index.d.ts', `
@ -1231,6 +1234,46 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents).toMatch(/directives: \[i1\.ExternalDir\]/); expect(jsContents).toMatch(/directives: \[i1\.ExternalDir\]/);
}); });
it('should import directives by their external name', () => {
env.tsconfig();
env.write('node_modules/external/index.d.ts', `
import {ɵDirectiveDefWithMeta, ɵNgModuleDefWithMeta} from '@angular/core';
import {InternalDir} from './internal';
export {InternalDir as ExternalDir} from './internal';
export declare class ExternalModule {
static ngModuleDef: ɵNgModuleDefWithMeta<ExternalModule, [typeof InternalDir], never, [typeof InternalDir]>;
}
`);
env.write('node_modules/external/internal.d.ts', `
export declare class InternalDir {
static ngDirectiveDef: ɵDirectiveDefWithMeta<InternalDir, '[test]', never, never, never, never>;
}
`);
env.write('test.ts', `
import {Component, Directive, NgModule} from '@angular/core';
import {ExternalModule} from 'external';
@Component({
template: '<div test></div>',
})
class Cmp {}
@NgModule({
declarations: [Cmp],
imports: [ExternalModule],
})
class Module {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toMatch(/directives: \[i1\.ExternalDir\]/);
});
});
describe('flat module indices', () => { describe('flat module indices', () => {
it('should generate a basic flat module index', () => { it('should generate a basic flat module index', () => {
env.tsconfig({ env.tsconfig({