perf(compiler-cli): avoid module resolution in cycle analysis (#40948)

The compiler performs cycle analysis for the used directives and pipes
of a component's template to avoid introducing a cyclic import into the
generated output. The used directives and pipes are represented by their
output expression which would typically be an `ExternalExpr`; those are
responsible for the generation of an `import` statement. Cycle analysis
needs to determine the `ts.SourceFile` that would end up being imported
by these `ExternalExpr`s, as the `ts.SourceFile` is then checked against
the program's `ImportGraph` to determine if the import is allowed, i.e.
does not introduce a cycle. To accomplish this, the `ExternalExpr` was
dissected and ran through module resolution to obtain the imported
`ts.SourceFile`.

This module resolution step is relatively expensive, as it typically
needs to hit the filesystem. Even in the presence of a module resolution
cache would these module resolution requests generally see cache misses,
as the generated import originates from a file for which the cache has
not previously seen the imported module specifier.

This commit removes the need for the module resolution by wrapping the
generated `Expression` in an `EmittedReference` struct. This allows the
reference emitter mechanism that is responsible for generating the
`Expression` to also communicate from which `ts.SourceFile` the
generated `Expression` would be imported, precluding the need for module
resolution down the road.

PR Close #40948
This commit is contained in:
JoostK 2021-02-15 22:28:19 +01:00 committed by Andrew Kushnir
parent 9cec94a008
commit 87bca2a5c6
10 changed files with 162 additions and 83 deletions

View File

@ -12,7 +12,7 @@ import * as ts from 'typescript';
import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../cycles'; import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../cycles';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
import {absoluteFrom, relative} from '../../file_system'; import {absoluteFrom, relative} from '../../file_system';
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {DefaultImportRecorder, ImportedFile, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {DependencyTracker} from '../../incremental/api'; import {DependencyTracker} from '../../incremental/api';
import {extractSemanticTypeParameters, isArrayEqual, isReferenceEqual, SemanticDepGraphUpdater, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph'; import {extractSemanticTypeParameters, isArrayEqual, isReferenceEqual, SemanticDepGraphUpdater, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph';
import {IndexingContext} from '../../indexer'; import {IndexingContext} from '../../indexer';
@ -628,11 +628,14 @@ export class ComponentDecoratorHandler implements
const bound = binder.bind({template: metadata.template.nodes}); const bound = binder.bind({template: metadata.template.nodes});
// The BoundTarget knows which directives and pipes matched the template. // The BoundTarget knows which directives and pipes matched the template.
type UsedDirective = R3UsedDirectiveMetadata&{ref: Reference<ClassDeclaration>}; type UsedDirective =
R3UsedDirectiveMetadata&{ref: Reference<ClassDeclaration>, importedFile: ImportedFile};
const usedDirectives: UsedDirective[] = bound.getUsedDirectives().map(directive => { const usedDirectives: UsedDirective[] = bound.getUsedDirectives().map(directive => {
const type = this.refEmitter.emit(directive.ref, context);
return { return {
ref: directive.ref, ref: directive.ref,
type: this.refEmitter.emit(directive.ref, context), type: type.expression,
importedFile: type.importedFile,
selector: directive.selector, selector: directive.selector,
inputs: directive.inputs.propertyNames, inputs: directive.inputs.propertyNames,
outputs: directive.outputs.propertyNames, outputs: directive.outputs.propertyNames,
@ -640,17 +643,25 @@ export class ComponentDecoratorHandler implements
isComponent: directive.isComponent, isComponent: directive.isComponent,
}; };
}); });
type UsedPipe = {ref: Reference<ClassDeclaration>, pipeName: string, expression: Expression};
type UsedPipe = {
ref: Reference<ClassDeclaration>,
pipeName: string,
expression: Expression,
importedFile: ImportedFile,
};
const usedPipes: UsedPipe[] = []; const usedPipes: UsedPipe[] = [];
for (const pipeName of bound.getUsedPipes()) { for (const pipeName of bound.getUsedPipes()) {
if (!pipes.has(pipeName)) { if (!pipes.has(pipeName)) {
continue; continue;
} }
const pipe = pipes.get(pipeName)!; const pipe = pipes.get(pipeName)!;
const type = this.refEmitter.emit(pipe, context);
usedPipes.push({ usedPipes.push({
ref: pipe, ref: pipe,
pipeName, pipeName,
expression: this.refEmitter.emit(pipe, context), expression: type.expression,
importedFile: type.importedFile,
}); });
} }
if (this.semanticDepGraphUpdater !== null) { if (this.semanticDepGraphUpdater !== null) {
@ -665,14 +676,16 @@ export class ComponentDecoratorHandler implements
// import which needs to be generated would create a cycle. // import which needs to be generated would create a cycle.
const cyclesFromDirectives = new Map<UsedDirective, Cycle>(); const cyclesFromDirectives = new Map<UsedDirective, Cycle>();
for (const usedDirective of usedDirectives) { for (const usedDirective of usedDirectives) {
const cycle = this._checkForCyclicImport(usedDirective.ref, usedDirective.type, context); const cycle =
this._checkForCyclicImport(usedDirective.importedFile, usedDirective.type, context);
if (cycle !== null) { if (cycle !== null) {
cyclesFromDirectives.set(usedDirective, cycle); cyclesFromDirectives.set(usedDirective, cycle);
} }
} }
const cyclesFromPipes = new Map<UsedPipe, Cycle>(); const cyclesFromPipes = new Map<UsedPipe, Cycle>();
for (const usedPipe of usedPipes) { for (const usedPipe of usedPipes) {
const cycle = this._checkForCyclicImport(usedPipe.ref, usedPipe.expression, context); const cycle =
this._checkForCyclicImport(usedPipe.importedFile, usedPipe.expression, context);
if (cycle !== null) { if (cycle !== null) {
cyclesFromPipes.set(usedPipe, cycle); cyclesFromPipes.set(usedPipe, cycle);
} }
@ -682,11 +695,11 @@ export class ComponentDecoratorHandler implements
if (!cycleDetected) { if (!cycleDetected) {
// No cycle was detected. Record the imports that need to be created in the cycle detector // No cycle was detected. Record the imports that need to be created in the cycle detector
// so that future cyclic import checks consider their production. // so that future cyclic import checks consider their production.
for (const {type} of usedDirectives) { for (const {type, importedFile} of usedDirectives) {
this._recordSyntheticImport(type, context); this._recordSyntheticImport(importedFile, type, context);
} }
for (const {expression} of usedPipes) { for (const {expression, importedFile} of usedPipes) {
this._recordSyntheticImport(expression, context); this._recordSyntheticImport(importedFile, expression, context);
} }
// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures. // Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
@ -1189,7 +1202,17 @@ export class ComponentDecoratorHandler implements
} }
} }
private _expressionToImportedFile(expr: Expression, origin: ts.SourceFile): ts.SourceFile|null { private _resolveImportedFile(importedFile: ImportedFile, expr: Expression, origin: ts.SourceFile):
ts.SourceFile|null {
// If `importedFile` is not 'unknown' then it accurately reflects the source file that is
// being imported.
if (importedFile !== 'unknown') {
return importedFile;
}
// Otherwise `expr` has to be inspected to determine the file that is being imported. If `expr`
// is not an `ExternalExpr` then it does not correspond with an import, so return null in that
// case.
if (!(expr instanceof ExternalExpr)) { if (!(expr instanceof ExternalExpr)) {
return null; return null;
} }
@ -1204,18 +1227,19 @@ export class ComponentDecoratorHandler implements
* *
* @returns a `Cycle` object if a cycle would be created, otherwise `null`. * @returns a `Cycle` object if a cycle would be created, otherwise `null`.
*/ */
private _checkForCyclicImport(ref: Reference, expr: Expression, origin: ts.SourceFile): Cycle private _checkForCyclicImport(
|null { importedFile: ImportedFile, expr: Expression, origin: ts.SourceFile): Cycle|null {
const importedFile = this._expressionToImportedFile(expr, origin); const imported = this._resolveImportedFile(importedFile, expr, origin);
if (importedFile === null) { if (imported === null) {
return null; return null;
} }
// Check whether the import is legal. // Check whether the import is legal.
return this.cycleAnalyzer.wouldCreateCycle(origin, importedFile); return this.cycleAnalyzer.wouldCreateCycle(origin, imported);
} }
private _recordSyntheticImport(expr: Expression, origin: ts.SourceFile): void { private _recordSyntheticImport(
const imported = this._expressionToImportedFile(expr, origin); importedFile: ImportedFile, expr: Expression, origin: ts.SourceFile): void {
const imported = this._resolveImportedFile(importedFile, expr, origin);
if (imported === null) { if (imported === null) {
return; return;
} }

View File

@ -418,7 +418,7 @@ export class NgModuleDecoratorHandler implements
const context = getSourceFile(node); const context = getSourceFile(node);
for (const exportRef of analysis.exports) { for (const exportRef of analysis.exports) {
if (isNgModule(exportRef.node, scope.compilation)) { if (isNgModule(exportRef.node, scope.compilation)) {
data.injectorImports.push(this.refEmitter.emit(exportRef, context)); data.injectorImports.push(this.refEmitter.emit(exportRef, context).expression);
} }
} }
@ -466,12 +466,12 @@ export class NgModuleDecoratorHandler implements
for (const decl of analysis.declarations) { for (const decl of analysis.declarations) {
const remoteScope = this.scopeRegistry.getRemoteScope(decl.node); const remoteScope = this.scopeRegistry.getRemoteScope(decl.node);
if (remoteScope !== null) { if (remoteScope !== null) {
const directives = const directives = remoteScope.directives.map(
remoteScope.directives.map(directive => this.refEmitter.emit(directive, context)); directive => this.refEmitter.emit(directive, context).expression);
const pipes = remoteScope.pipes.map(pipe => this.refEmitter.emit(pipe, context)); const pipes = remoteScope.pipes.map(pipe => this.refEmitter.emit(pipe, context).expression);
const directiveArray = new LiteralArrayExpr(directives); const directiveArray = new LiteralArrayExpr(directives);
const pipesArray = new LiteralArrayExpr(pipes); const pipesArray = new LiteralArrayExpr(pipes);
const declExpr = this.refEmitter.emit(decl, context)!; const declExpr = this.refEmitter.emit(decl, context).expression;
const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope); const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope);
const callExpr = const callExpr =
new InvokeFunctionExpr(setComponentScope, [declExpr, directiveArray, pipesArray]); new InvokeFunctionExpr(setComponentScope, [declExpr, directiveArray, pipesArray]);

View File

@ -267,13 +267,12 @@ function createUnsuitableInjectionTokenError(
export function toR3Reference( export function toR3Reference(
valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile, valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile,
typeContext: ts.SourceFile, refEmitter: ReferenceEmitter): R3Reference { typeContext: ts.SourceFile, refEmitter: ReferenceEmitter): R3Reference {
const value = refEmitter.emit(valueRef, valueContext); return {
const type = refEmitter.emit( value: refEmitter.emit(valueRef, valueContext).expression,
typeRef, typeContext, ImportFlags.ForceNewImport | ImportFlags.AllowTypeImports); type: refEmitter
if (value === null || type === null) { .emit(typeRef, typeContext, ImportFlags.ForceNewImport | ImportFlags.AllowTypeImports)
throw new Error(`Could not refer to ${ts.SyntaxKind[valueRef.node.kind]}`); .expression,
} };
return {value, type};
} }
export function isAngularCore(decorator: Decorator): decorator is Decorator&{import: Import} { export function isAngularCore(decorator: Decorator): decorator is Decorator&{import: Import} {

View File

@ -9,7 +9,7 @@
export {AliasingHost, AliasStrategy, PrivateExportAliasingHost, UnifiedModulesAliasingHost} from './src/alias'; export {AliasingHost, AliasStrategy, PrivateExportAliasingHost, UnifiedModulesAliasingHost} from './src/alias';
export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core'; export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core';
export {DefaultImportRecorder, DefaultImportTracker, NOOP_DEFAULT_IMPORT_RECORDER} from './src/default'; export {DefaultImportRecorder, DefaultImportTracker, NOOP_DEFAULT_IMPORT_RECORDER} from './src/default';
export {AbsoluteModuleStrategy, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesStrategy} from './src/emitter'; export {AbsoluteModuleStrategy, EmittedReference, ImportedFile, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesStrategy} from './src/emitter';
export {Reexport} from './src/reexport'; export {Reexport} from './src/reexport';
export {OwningModule, Reference} from './src/references'; export {OwningModule, Reference} from './src/references';
export {ModuleResolver} from './src/resolver'; export {ModuleResolver} from './src/resolver';

View File

@ -10,13 +10,12 @@ import {Expression, ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api'; import {UnifiedModulesHost} from '../../core/api';
import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection'; import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {ImportFlags, ReferenceEmitStrategy} from './emitter'; import {EmittedReference, ImportFlags, ReferenceEmitStrategy} from './emitter';
import {Reference} from './references'; import {Reference} from './references';
// Escape anything that isn't alphanumeric, '/' or '_'. // Escape anything that isn't alphanumeric, '/' or '_'.
const CHARS_TO_ESCAPE = /[^a-zA-Z0-9/_]/g; const CHARS_TO_ESCAPE = /[^a-zA-Z0-9/_]/g;
@ -213,11 +212,11 @@ export class PrivateExportAliasingHost implements AliasingHost {
* directive or pipe, if it exists. * directive or pipe, if it exists.
*/ */
export class AliasStrategy implements ReferenceEmitStrategy { export class AliasStrategy implements ReferenceEmitStrategy {
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importMode: ImportFlags): Expression|null { emit(ref: Reference, context: ts.SourceFile, importMode: ImportFlags): EmittedReference|null {
if (importMode & ImportFlags.NoAliasing) { if (importMode & ImportFlags.NoAliasing || ref.alias === null) {
return null; return null;
} }
return ref.alias; return {expression: ref.alias, importedFile: 'unknown'};
} }
} }

View File

@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api'; import {UnifiedModulesHost} from '../../core/api';
import {absoluteFromSourceFile, dirname, LogicalFileSystem, LogicalProjectPath, relative, toRelativeImport} from '../../file_system'; import {absoluteFromSourceFile, dirname, LogicalFileSystem, LogicalProjectPath, relative, toRelativeImport} from '../../file_system';
import {stripExtension} from '../../file_system/src/util'; import {stripExtension} from '../../file_system/src/util';
import {DeclarationNode, isConcreteDeclaration, ReflectionHost} from '../../reflection'; import {DeclarationNode, ReflectionHost} from '../../reflection';
import {getSourceFile, isDeclaration, isTypeDeclaration, nodeNameForError} from '../../util/src/typescript'; import {getSourceFile, isDeclaration, isTypeDeclaration, nodeNameForError} from '../../util/src/typescript';
import {findExportedNameOfNode} from './find_export'; import {findExportedNameOfNode} from './find_export';
@ -51,6 +51,36 @@ export enum ImportFlags {
AllowTypeImports = 0x04, AllowTypeImports = 0x04,
} }
/**
* An emitter strategy has the ability to indicate which `ts.SourceFile` is being imported by the
* expression that it has generated. This information is useful for consumers of the emitted
* reference that would otherwise have to perform a relatively expensive module resolution step,
* e.g. for cyclic import analysis. In cases the emitter is unable to definitively determine the
* imported source file or a computation would be required to actually determine the imported
* source file, then `'unknown'` should be returned. If the generated expression does not represent
* an import then `null` should be used.
*/
export type ImportedFile = ts.SourceFile|'unknown'|null;
/**
* Represents the emitted expression of a `Reference` that is valid in the source file it was
* emitted from.
*/
export interface EmittedReference {
/**
* The expression that refers to `Reference`.
*/
expression: Expression;
/**
* The `ts.SourceFile` that is imported by `expression`. This is not necessarily the source file
* of the `Reference`'s declaration node, as the reference may have been rewritten through an
* alias export. It could also be `null` if `expression` is a local identifier, or `'unknown'` if
* the exact source file that is being imported is not known to the emitter.
*/
importedFile: ImportedFile;
}
/** /**
* A particular strategy for generating an expression which refers to a `Reference`. * A particular strategy for generating an expression which refers to a `Reference`.
* *
@ -71,9 +101,10 @@ export interface ReferenceEmitStrategy {
* @param ref the `Reference` for which to generate an expression * @param ref the `Reference` for which to generate an expression
* @param context the source file in which the `Expression` must be valid * @param context the source file in which the `Expression` must be valid
* @param importFlags a flag which controls whether imports should be generated or not * @param importFlags a flag which controls whether imports should be generated or not
* @returns an `Expression` which refers to the `Reference`, or `null` if none can be generated * @returns an `EmittedReference` which refers to the `Reference`, or `null` if none can be
* generated
*/ */
emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): Expression|null; emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): EmittedReference|null;
} }
/** /**
@ -85,8 +116,8 @@ export interface ReferenceEmitStrategy {
export class ReferenceEmitter { export class ReferenceEmitter {
constructor(private strategies: ReferenceEmitStrategy[]) {} constructor(private strategies: ReferenceEmitStrategy[]) {}
emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags = ImportFlags.None): emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags = ImportFlags.None):
Expression { EmittedReference {
for (const strategy of this.strategies) { for (const strategy of this.strategies) {
const emitted = strategy.emit(ref, context, importFlags); const emitted = strategy.emit(ref, context, importFlags);
if (emitted !== null) { if (emitted !== null) {
@ -103,7 +134,7 @@ export class ReferenceEmitter {
* such identifiers are available. * such identifiers are available.
*/ */
export class LocalIdentifierStrategy implements ReferenceEmitStrategy { export class LocalIdentifierStrategy implements ReferenceEmitStrategy {
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importFlags: ImportFlags): Expression|null { emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): EmittedReference|null {
const refSf = getSourceFile(ref.node); const refSf = getSourceFile(ref.node);
// If the emitter has specified ForceNewImport, then LocalIdentifierStrategy should not use a // If the emitter has specified ForceNewImport, then LocalIdentifierStrategy should not use a
@ -118,20 +149,41 @@ export class LocalIdentifierStrategy implements ReferenceEmitStrategy {
// such a case, the reference's `identities` property would be `[foo]`, which would result in an // such a case, the reference's `identities` property would be `[foo]`, which would result in an
// invalid emission of a free-standing `foo` identifier, rather than `exports.foo`. // invalid emission of a free-standing `foo` identifier, rather than `exports.foo`.
if (!isDeclaration(ref.node) && refSf === context) { if (!isDeclaration(ref.node) && refSf === context) {
return new WrappedNodeExpr(ref.node); return {
expression: new WrappedNodeExpr(ref.node),
importedFile: null,
};
} }
// A Reference can have multiple identities in different files, so it may already have an // A Reference can have multiple identities in different files, so it may already have an
// Identifier in the requested context file. // Identifier in the requested context file.
const identifier = ref.getIdentityIn(context); const identifier = ref.getIdentityIn(context);
if (identifier !== null) { if (identifier !== null) {
return new WrappedNodeExpr(identifier); return {
expression: new WrappedNodeExpr(identifier),
importedFile: null,
};
} else { } else {
return null; return null;
} }
} }
} }
/**
* Represents the exported declarations from a module source file.
*/
interface ModuleExports {
/**
* The source file of the module.
*/
module: ts.SourceFile;
/**
* The map of declarations to their exported name.
*/
exportMap: Map<DeclarationNode, string>;
}
/** /**
* A `ReferenceEmitStrategy` which will refer to declarations that come from `node_modules` using * A `ReferenceEmitStrategy` which will refer to declarations that come from `node_modules` using
* an absolute import. * an absolute import.
@ -146,13 +198,13 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
* A cache of the exports of specific modules, because resolving a module to its exports is a * A cache of the exports of specific modules, because resolving a module to its exports is a
* costly operation. * costly operation.
*/ */
private moduleExportsCache = new Map<string, Map<DeclarationNode, string>|null>(); private moduleExportsCache = new Map<string, ModuleExports|null>();
constructor( constructor(
protected program: ts.Program, protected checker: ts.TypeChecker, protected program: ts.Program, protected checker: ts.TypeChecker,
protected moduleResolver: ModuleResolver, private reflectionHost: ReflectionHost) {} protected moduleResolver: ModuleResolver, private reflectionHost: ReflectionHost) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importFlags: ImportFlags): Expression|null { emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): EmittedReference|null {
if (ref.bestGuessOwningModule === null) { if (ref.bestGuessOwningModule === null) {
// There is no module name available for this Reference, meaning it was arrived at via a // There is no module name available for this Reference, meaning it was arrived at via a
// relative path. // relative path.
@ -168,38 +220,30 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
// Try to find the exported name of the declaration, if one is available. // Try to find the exported name of the declaration, if one is available.
const {specifier, resolutionContext} = ref.bestGuessOwningModule; const {specifier, resolutionContext} = ref.bestGuessOwningModule;
const symbolName = this.resolveImportName(specifier, ref.node, resolutionContext); const exports = this.getExportsOfModule(specifier, resolutionContext);
if (symbolName === null) { if (exports === null || !exports.exportMap.has(ref.node)) {
// TODO(alxhub): make this error a ts.Diagnostic pointing at whatever caused this import to be // TODO(alxhub): make this error a ts.Diagnostic pointing at whatever caused this import to be
// triggered. // triggered.
throw new Error(`Symbol ${ref.debugName} declared in ${ throw new Error(`Symbol ${ref.debugName} declared in ${
getSourceFile(ref.node).fileName} is not exported from ${specifier} (import into ${ getSourceFile(ref.node).fileName} is not exported from ${specifier} (import into ${
context.fileName})`); context.fileName})`);
} }
const symbolName = exports.exportMap.get(ref.node)!;
return new ExternalExpr(new ExternalReference(specifier, symbolName)); return {
expression: new ExternalExpr(new ExternalReference(specifier, symbolName)),
importedFile: exports.module,
};
} }
private resolveImportName(moduleName: string, target: DeclarationNode, fromFile: string): string private getExportsOfModule(moduleName: string, fromFile: string): ModuleExports|null {
|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<DeclarationNode, string>|null {
if (!this.moduleExportsCache.has(moduleName)) { if (!this.moduleExportsCache.has(moduleName)) {
this.moduleExportsCache.set(moduleName, this.enumerateExportsOfModule(moduleName, fromFile)); this.moduleExportsCache.set(moduleName, this.enumerateExportsOfModule(moduleName, fromFile));
} }
return this.moduleExportsCache.get(moduleName)!; return this.moduleExportsCache.get(moduleName)!;
} }
protected enumerateExportsOfModule(specifier: string, fromFile: string): protected enumerateExportsOfModule(specifier: string, fromFile: string): ModuleExports|null {
Map<DeclarationNode, string>|null {
// First, resolve the module specifier to its entry point, and get the ts.Symbol for it. // First, resolve the module specifier to its entry point, and get the ts.Symbol for it.
const entryPointFile = this.moduleResolver.resolveModule(specifier, fromFile); const entryPointFile = this.moduleResolver.resolveModule(specifier, fromFile);
if (entryPointFile === null) { if (entryPointFile === null) {
@ -214,7 +258,7 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
exports.forEach((declaration, name) => { exports.forEach((declaration, name) => {
exportMap.set(declaration.node, name); exportMap.set(declaration.node, name);
}); });
return exportMap; return {module: entryPointFile, exportMap};
} }
} }
@ -229,7 +273,7 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
export class LogicalProjectStrategy implements ReferenceEmitStrategy { export class LogicalProjectStrategy implements ReferenceEmitStrategy {
constructor(private reflector: ReflectionHost, private logicalFs: LogicalFileSystem) {} constructor(private reflector: ReflectionHost, private logicalFs: LogicalFileSystem) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null { emit(ref: Reference, context: ts.SourceFile): EmittedReference|null {
const destSf = getSourceFile(ref.node); const destSf = getSourceFile(ref.node);
// Compute the relative path from the importing file to the file being imported. This is done // Compute the relative path from the importing file to the file being imported. This is done
@ -260,7 +304,10 @@ export class LogicalProjectStrategy implements ReferenceEmitStrategy {
// With both files expressed as LogicalProjectPaths, getting the module specifier as a relative // With both files expressed as LogicalProjectPaths, getting the module specifier as a relative
// path is now straightforward. // path is now straightforward.
const moduleName = LogicalProjectPath.relativePathBetween(originPath, destPath); const moduleName = LogicalProjectPath.relativePathBetween(originPath, destPath);
return new ExternalExpr({moduleName, name}); return {
expression: new ExternalExpr({moduleName, name}),
importedFile: destSf,
};
} }
} }
@ -273,14 +320,14 @@ export class LogicalProjectStrategy implements ReferenceEmitStrategy {
export class RelativePathStrategy implements ReferenceEmitStrategy { export class RelativePathStrategy implements ReferenceEmitStrategy {
constructor(private reflector: ReflectionHost) {} constructor(private reflector: ReflectionHost) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null { emit(ref: Reference, context: ts.SourceFile): EmittedReference|null {
const destSf = getSourceFile(ref.node); const destSf = getSourceFile(ref.node);
const relativePath = const relativePath =
relative(dirname(absoluteFromSourceFile(context)), absoluteFromSourceFile(destSf)); relative(dirname(absoluteFromSourceFile(context)), absoluteFromSourceFile(destSf));
const moduleName = toRelativeImport(stripExtension(relativePath)); const moduleName = toRelativeImport(stripExtension(relativePath));
const name = findExportedNameOfNode(ref.node, destSf, this.reflector); const name = findExportedNameOfNode(ref.node, destSf, this.reflector);
return new ExternalExpr({moduleName, name}); return {expression: new ExternalExpr({moduleName, name}), importedFile: destSf};
} }
} }
@ -291,7 +338,7 @@ export class RelativePathStrategy implements ReferenceEmitStrategy {
export class UnifiedModulesStrategy implements ReferenceEmitStrategy { export class UnifiedModulesStrategy implements ReferenceEmitStrategy {
constructor(private reflector: ReflectionHost, private unifiedModulesHost: UnifiedModulesHost) {} constructor(private reflector: ReflectionHost, private unifiedModulesHost: UnifiedModulesHost) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null { emit(ref: Reference, context: ts.SourceFile): EmittedReference|null {
const destSf = getSourceFile(ref.node); const destSf = getSourceFile(ref.node);
const name = findExportedNameOfNode(ref.node, destSf, this.reflector); const name = findExportedNameOfNode(ref.node, destSf, this.reflector);
if (name === null) { if (name === null) {
@ -301,6 +348,9 @@ export class UnifiedModulesStrategy implements ReferenceEmitStrategy {
const moduleName = const moduleName =
this.unifiedModulesHost.fileNameToModuleName(destSf.fileName, context.fileName); this.unifiedModulesHost.fileNameToModuleName(destSf.fileName, context.fileName);
return new ExternalExpr({moduleName, name}); return {
expression: new ExternalExpr({moduleName, name}),
importedFile: destSf,
};
} }
} }

View File

@ -72,11 +72,14 @@ runInEachFileSystem(() => {
resolutionContext: context.fileName, resolutionContext: context.fileName,
}); });
const emitted = strategy.emit(reference, context, ImportFlags.None); const emitted = strategy.emit(reference, context, ImportFlags.None);
if (!(emitted instanceof ExternalExpr)) { if (emitted === null) {
return fail('Reference should be emitted');
}
if (!(emitted.expression instanceof ExternalExpr)) {
return fail('Reference should be emitted as ExternalExpr'); return fail('Reference should be emitted as ExternalExpr');
} }
expect(emitted.value.name).toEqual('Bar'); expect(emitted.expression.value.name).toEqual('Bar');
expect(emitted.value.moduleName).toEqual('external'); expect(emitted.expression.value.moduleName).toEqual('external');
}); });
it('should throw when generating an import to a type-only declaration when not allowed', () => { it('should throw when generating an import to a type-only declaration when not allowed', () => {
@ -121,11 +124,14 @@ runInEachFileSystem(() => {
const reference = const reference =
new Reference(decl, {specifier: 'external', resolutionContext: context.fileName}); new Reference(decl, {specifier: 'external', resolutionContext: context.fileName});
const emitted = strategy.emit(reference, context, ImportFlags.AllowTypeImports); const emitted = strategy.emit(reference, context, ImportFlags.AllowTypeImports);
if (!(emitted instanceof ExternalExpr)) { if (emitted === null) {
return fail('Reference should be emitted');
}
if (!(emitted.expression instanceof ExternalExpr)) {
return fail('Reference should be emitted as ExternalExpr'); return fail('Reference should be emitted as ExternalExpr');
} }
expect(emitted.value.name).toEqual('Foo'); expect(emitted.expression.value.name).toEqual('Foo');
expect(emitted.value.moduleName).toEqual('external'); expect(emitted.expression.value.moduleName).toEqual('external');
}); });
}); });
@ -165,7 +171,7 @@ runInEachFileSystem(() => {
expect(ref).not.toBeNull(); expect(ref).not.toBeNull();
// Expect the prefixed name from the TestHost. // Expect the prefixed name from the TestHost.
expect((ref! as ExternalExpr).value.name).toEqual('testFoo'); expect((ref!.expression as ExternalExpr).value.name).toEqual('testFoo');
}); });
}); });
}); });

View File

@ -101,7 +101,7 @@ export class ModuleWithProvidersScanner {
const ngModuleExpr = const ngModuleExpr =
this.emitter.emit(ngModule, decl.getSourceFile(), ImportFlags.ForceNewImport); this.emitter.emit(ngModule, decl.getSourceFile(), ImportFlags.ForceNewImport);
const ngModuleType = new ExpressionType(ngModuleExpr); const ngModuleType = new ExpressionType(ngModuleExpr.expression);
const mwpNgType = new ExpressionType( const mwpNgType = new ExpressionType(
new ExternalExpr(Identifiers.ModuleWithProviders), [/* modifiers */], [ngModuleType]); new ExternalExpr(Identifiers.ModuleWithProviders), [/* modifiers */], [ngModuleType]);

View File

@ -511,7 +511,8 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
asAlias: exportName, asAlias: exportName,
}); });
} else { } else {
const expr = this.refEmitter.emit(exportRef.cloneWithNoIdentifiers(), sourceFile); const expr =
this.refEmitter.emit(exportRef.cloneWithNoIdentifiers(), sourceFile).expression;
if (!(expr instanceof ExternalExpr) || expr.value.moduleName === null || if (!(expr instanceof ExternalExpr) || expr.value.moduleName === null ||
expr.value.name === null) { expr.value.name === null) {
throw new Error('Expected ExternalExpr'); throw new Error('Expected ExternalExpr');

View File

@ -123,7 +123,7 @@ export class Environment {
const ngExpr = this.refEmitter.emit(ref, this.contextFile, ImportFlags.NoAliasing); const ngExpr = this.refEmitter.emit(ref, this.contextFile, ImportFlags.NoAliasing);
// Use `translateExpression` to convert the `Expression` into a `ts.Expression`. // Use `translateExpression` to convert the `Expression` into a `ts.Expression`.
return translateExpression(ngExpr, this.importManager); return translateExpression(ngExpr.expression, this.importManager);
} }
/** /**
@ -137,7 +137,7 @@ export class Environment {
// Create an `ExpressionType` from the `Expression` and translate it via `translateType`. // Create an `ExpressionType` from the `Expression` and translate it via `translateType`.
// TODO(alxhub): support references to types with generic arguments in a clean way. // TODO(alxhub): support references to types with generic arguments in a clean way.
return translateType(new ExpressionType(ngExpr), this.importManager); return translateType(new ExpressionType(ngExpr.expression), this.importManager);
} }
private emitTypeParameters(declaration: ClassDeclaration<ts.ClassDeclaration>): private emitTypeParameters(declaration: ClassDeclaration<ts.ClassDeclaration>):