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:
parent
9cec94a008
commit
87bca2a5c6
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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} {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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'};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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>):
|
||||||
|
|
Loading…
Reference in New Issue