feat(ivy): use fileNameToModuleName to emit imports when it's available (#28523)

The ultimate goal of this commit is to make use of fileNameToModuleName to
get the module specifier to use when generating an import, when that API is
available in the CompilerHost that ngtsc is created with.

As part of getting there, the way in which ngtsc tracks references and
generates import module specifiers is refactored considerably. References
are tracked with the Reference class, and previously ngtsc had several
different kinds of Reference. An AbsoluteReference represented a declaration
which needed to be imported via an absolute module specifier tracked in the
AbsoluteReference, and a RelativeReference represented a declaration from
the local program, imported via relative path or referred to directly by
identifier if possible. Thus, how to refer to a particular declaration was
encoded into the Reference type _at the time of creation of the Reference_.

This commit refactors that logic and reduces Reference to a single class
with no subclasses. A Reference represents a node being referenced, plus
context about how the node was located. This context includes a
"bestGuessOwningModule", the compiler's best guess at which absolute
module specifier has defined this reference. For example, if the compiler
arrives at the declaration of CommonModule via an import to @angular/common,
then any references obtained from CommonModule (e.g. NgIf) will also be
considered to be owned by @angular/common.

A ReferenceEmitter class and accompanying ReferenceEmitStrategy interface
are introduced. To produce an Expression referring to a given Reference'd
node, the ReferenceEmitter consults a sequence of ReferenceEmitStrategy
implementations.

Several different strategies are defined:

- LocalIdentifierStrategy: use local ts.Identifiers if available.
- AbsoluteModuleStrategy: if the Reference has a bestGuessOwningModule,
  import the node via an absolute import from that module specifier.
- LogicalProjectStrategy: if the Reference is in the logical project
  (is under the project rootDirs), import the node via a relative import.
- FileToModuleStrategy: use a FileToModuleHost to generate the module
  specifier by which to import the node.

Depending on the availability of fileNameToModuleName in the CompilerHost,
then, a different collection of these strategies is used for compilation.

PR Close #28523
This commit is contained in:
Alex Rickabaugh
2019-02-01 17:24:21 -08:00
committed by Misko Hevery
parent a529f53031
commit 423b39e216
40 changed files with 709 additions and 417 deletions

View File

@ -12,7 +12,7 @@ import * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ModuleResolver, ResolvedReference} from '../../imports';
import {ModuleResolver, Reference} from '../../imports';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
@ -225,7 +225,7 @@ export class ComponentDecoratorHandler implements
// If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this component appears in an `@NgModule` scope, its selector can be determined.
if (metadata.selector !== null) {
const ref = new ResolvedReference(node, node.name !);
const ref = new Reference(node);
this.scopeRegistry.registerDirective(node, {
ref,
name: node.name !.text,

View File

@ -10,7 +10,7 @@ import {ConstantPool, Expression, ParseError, R3DirectiveMetadata, R3QueryMetada
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Reference, ResolvedReference} from '../../imports';
import {Reference} from '../../imports';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
@ -57,7 +57,7 @@ export class DirectiveDecoratorHandler implements
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this directive appears in an `@NgModule` scope, its selector can be determined.
if (analysis && analysis.selector !== null) {
let ref = new ResolvedReference(node, node.name !);
const ref = new Reference(node);
this.scopeRegistry.registerDirective(node, {
ref,
directive: ref,

View File

@ -10,11 +10,12 @@ import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, R3Identi
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Reference, ResolvedReference} from '../../imports';
import {Reference, ReferenceEmitter} from '../../imports';
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
import {NgModuleRouteAnalyzer} from '../../routing';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {getSourceFile} from '../../util/src/typescript';
import {generateSetClassMetadataCall} from './metadata';
import {ReferencesRegistry} from './references_registry';
@ -37,7 +38,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private scopeRegistry: SelectorScopeRegistry, private referencesRegistry: ReferencesRegistry,
private isCore: boolean, private routeAnalyzer: NgModuleRouteAnalyzer|null) {}
private isCore: boolean, private routeAnalyzer: NgModuleRouteAnalyzer|null,
private refEmitter: ReferenceEmitter) {}
readonly precedence = HandlerPrecedence.PRIMARY;
@ -177,10 +179,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
if (analysis.metadataStmt !== null) {
ngModuleStatements.push(analysis.metadataStmt);
}
let context = node.getSourceFile();
if (context === undefined) {
context = ts.getOriginalNode(node).getSourceFile();
}
const context = getSourceFile(node);
for (const decl of analysis.declarations) {
if (this.scopeRegistry.requiresRemoteScope(decl.node)) {
const scope = this.scopeRegistry.lookupCompilationScopeAsRefs(decl.node);
@ -190,11 +189,11 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
const directives: Expression[] = [];
const pipes: Expression[] = [];
scope.directives.forEach(
(directive, _) => { directives.push(directive.ref.toExpression(context) !); });
scope.pipes.forEach(pipe => pipes.push(pipe.toExpression(context) !));
(directive, _) => { directives.push(this.refEmitter.emit(directive.ref, context) !); });
scope.pipes.forEach(pipe => pipes.push(this.refEmitter.emit(pipe, context) !));
const directiveArray = new LiteralArrayExpr(directives);
const pipesArray = new LiteralArrayExpr(pipes);
const declExpr = decl.toExpression(context) !;
const declExpr = this.refEmitter.emit(decl, context) !;
const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope);
const callExpr =
new InvokeFunctionExpr(setComponentScope, [declExpr, directiveArray, pipesArray]);
@ -221,15 +220,15 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
private _toR3Reference(
valueRef: Reference<ts.Declaration>, valueContext: ts.SourceFile,
typeContext: ts.SourceFile): R3Reference {
if (!(valueRef instanceof ResolvedReference)) {
return toR3Reference(valueRef, valueRef, valueContext, valueContext);
if (valueRef.hasOwningModuleGuess) {
return toR3Reference(valueRef, valueRef, valueContext, valueContext, this.refEmitter);
} else {
let typeRef = valueRef;
let typeNode = this.reflector.getDtsDeclaration(typeRef.node);
if (typeNode !== null && ts.isClassDeclaration(typeNode)) {
typeRef = new ResolvedReference(typeNode, typeNode.name !);
typeRef = new Reference(typeNode);
}
return toR3Reference(valueRef, typeRef, valueContext, typeContext);
return toR3Reference(valueRef, typeRef, valueContext, typeContext, this.refEmitter);
}
}
@ -328,10 +327,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
// Recurse into nested arrays.
refList.push(...this.resolveTypeList(expr, entry, name));
} else if (isDeclarationReference(entry)) {
if (!entry.expressable) {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `One entry in ${name} is not a type`);
} else if (!this.reflector.isClass(entry.node)) {
if (!this.reflector.isClass(entry.node)) {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, entry.node,
`Entry is not a type, but is used as such in ${name} array`);

View File

@ -9,7 +9,7 @@
import {Expression, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteReference, Reference, ReferenceResolver, ResolvedReference} from '../../imports';
import {Reference, ReferenceEmitter} from '../../imports';
import {ReflectionHost, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectTypeEntityToDeclaration} from '../../reflection';
import {TypeCheckableDirectiveMeta} from '../../typecheck';
@ -96,7 +96,7 @@ export class SelectorScopeRegistry {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private resolver: ReferenceResolver) {}
private refEmitter: ReferenceEmitter) {}
/**
* Register a module's metadata with the registry.
@ -224,7 +224,7 @@ export class SelectorScopeRegistry {
*/
lookupCompilationScope(node: ts.Declaration): CompilationScope<Expression>|null {
const scope = this.lookupCompilationScopeAsRefs(node);
return scope !== null ? convertScopeToExpressions(scope, node) : null;
return scope !== null ? convertScopeToExpressions(scope, node, this.refEmitter) : null;
}
private lookupScopesOrDie(
@ -273,20 +273,20 @@ export class SelectorScopeRegistry {
// Expand imports to the exported scope of those imports.
...flatten(data.imports.map(
ref =>
this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref), context)
this.lookupScopesOrDie(ref.node as ts.Declaration, ref.ownedByModuleGuess, context)
.exported)),
// And include the compilation scope of exported modules.
...flatten(
data.exports
.map(
ref => this.lookupScopes(
ref.node as ts.Declaration, absoluteModuleName(ref), context))
ref.node as ts.Declaration, ref.ownedByModuleGuess, context))
.filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null)
.map(scope => scope.exported))
],
exported: flatten(data.exports.map(ref => {
const scope =
this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref), context);
this.lookupScopes(ref.node as ts.Declaration, ref.ownedByModuleGuess, context);
if (scope !== null) {
return scope.exported;
} else {
@ -438,11 +438,11 @@ export class SelectorScopeRegistry {
const type = element.exprName;
if (ngModuleImportedFrom !== null) {
const {node, from} = reflectTypeEntityToDeclaration(type, this.checker);
const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
return this.resolver.resolve(node, moduleName, resolutionContext);
const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
return new Reference(node, {specifier, resolutionContext});
} else {
const {node} = reflectTypeEntityToDeclaration(type, this.checker);
return this.resolver.resolve(node, null, resolutionContext);
return new Reference(node);
}
});
}
@ -455,17 +455,11 @@ function flatten<T>(array: T[][]): T[] {
}, [] as T[]);
}
function absoluteModuleName(ref: Reference): string|null {
if (!(ref instanceof AbsoluteReference)) {
return null;
}
return ref.moduleName;
}
function convertDirectiveReferenceList(
input: ScopeDirective<Reference>[], context: ts.SourceFile): ScopeDirective<Expression>[] {
input: ScopeDirective<Reference>[], context: ts.SourceFile,
refEmitter: ReferenceEmitter): ScopeDirective<Expression>[] {
return input.map(meta => {
const directive = meta.directive.toExpression(context);
const directive = refEmitter.emit(meta.directive, context);
if (directive === null) {
throw new Error(`Could not write expression to reference ${meta.directive.node}`);
}
@ -474,10 +468,11 @@ function convertDirectiveReferenceList(
}
function convertPipeReferenceMap(
map: Map<string, Reference>, context: ts.SourceFile): Map<string, Expression> {
map: Map<string, Reference>, context: ts.SourceFile,
refEmitter: ReferenceEmitter): Map<string, Expression> {
const newMap = new Map<string, Expression>();
map.forEach((meta, selector) => {
const pipe = meta.toExpression(context);
const pipe = refEmitter.emit(meta, context);
if (pipe === null) {
throw new Error(`Could not write expression to reference ${meta.node}`);
}
@ -487,10 +482,11 @@ function convertPipeReferenceMap(
}
function convertScopeToExpressions(
scope: CompilationScope<Reference>, context: ts.Declaration): CompilationScope<Expression> {
scope: CompilationScope<Reference>, context: ts.Declaration,
refEmitter: ReferenceEmitter): CompilationScope<Expression> {
const sourceContext = ts.getOriginalNode(context).getSourceFile();
const directives = convertDirectiveReferenceList(scope.directives, sourceContext);
const pipes = convertPipeReferenceMap(scope.pipes, sourceContext);
const directives = convertDirectiveReferenceList(scope.directives, sourceContext, refEmitter);
const pipes = convertPipeReferenceMap(scope.pipes, sourceContext, refEmitter);
const declPointer = maybeUnwrapNameOfDeclaration(context);
let containsForwardDecls = false;
directives.forEach(meta => {

View File

@ -10,7 +10,7 @@ import {R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNode
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {AbsoluteReference, ImportMode, Reference} from '../../imports';
import {ImportMode, Reference, ReferenceEmitter} from '../../imports';
import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost} from '../../reflection';
export enum ConstructorDepErrorKind {
@ -119,9 +119,9 @@ export function validateConstructorDependencies(
export function toR3Reference(
valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile,
typeContext: ts.SourceFile): R3Reference {
const value = valueRef.toExpression(valueContext, ImportMode.UseExistingImport);
const type = typeRef.toExpression(typeContext, ImportMode.ForceNewImport);
typeContext: ts.SourceFile, refEmitter: ReferenceEmitter): R3Reference {
const value = refEmitter.emit(valueRef, valueContext, ImportMode.UseExistingImport);
const type = refEmitter.emit(typeRef, typeContext, ImportMode.ForceNewImport);
if (value === null || type === null) {
throw new Error(`Could not refer to ${ts.SyntaxKind[valueRef.node.kind]}`);
}
@ -133,8 +133,7 @@ export function isAngularCore(decorator: Decorator): boolean {
}
export function isAngularCoreReference(reference: Reference, symbolName: string) {
return reference instanceof AbsoluteReference && reference.moduleName === '@angular/core' &&
reference.symbolName === symbolName;
return reference.ownedByModuleGuess === '@angular/core' && reference.debugName === symbolName;
}
/**
@ -209,8 +208,7 @@ export function unwrapForwardRef(node: ts.Expression, reflector: ReflectionHost)
export function forwardRefResolver(
ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration>,
args: ts.Expression[]): ts.Expression|null {
if (!(ref instanceof AbsoluteReference) || ref.moduleName !== '@angular/core' ||
ref.symbolName !== 'forwardRef' || args.length !== 1) {
if (!isAngularCoreReference(ref, 'forwardRef') || args.length !== 1) {
return null;
}
return expandForwardRef(args[0]);

View File

@ -16,9 +16,11 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/util",
"@ngdeps//typescript",
],
)

View File

@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {CycleAnalyzer, ImportGraph} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ModuleResolver, TsReferenceResolver} from '../../imports';
import {ModuleResolver, ReferenceEmitter} from '../../imports';
import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
@ -44,15 +44,15 @@ describe('ComponentDecoratorHandler', () => {
]);
const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker);
const resolver = new TsReferenceResolver(program, checker, options, host);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const moduleResolver = new ModuleResolver(program, options, host);
const importGraph = new ImportGraph(moduleResolver);
const cycleAnalyzer = new CycleAnalyzer(importGraph);
const handler = new ComponentDecoratorHandler(
reflectionHost, evaluator, new SelectorScopeRegistry(checker, reflectionHost, resolver),
false, new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer);
reflectionHost, evaluator,
new SelectorScopeRegistry(checker, reflectionHost, new ReferenceEmitter([])), false,
new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer);
const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {

View File

@ -8,9 +8,11 @@
import * as ts from 'typescript';
import {AbsoluteReference, ResolvedReference, TsReferenceResolver} from '../../imports';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports';
import {LogicalFileSystem} from '../../path';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getRootDirs} from '../../util/src/typescript';
import {SelectorScopeRegistry} from '../src/selector_scope';
describe('SelectorScopeRegistry', () => {
@ -63,18 +65,19 @@ describe('SelectorScopeRegistry', () => {
expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined();
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
const resolver = new TsReferenceResolver(program, checker, options, host);
const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver);
const ProgramCmpRef = new Reference(ProgramCmp);
const refEmitter = makeReferenceEmitter(program, checker, options, host);
const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter);
registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],
declarations: [new Reference(ProgramCmp)],
exports: [],
imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')],
imports: [new Reference(
SomeModule,
{specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})],
});
const ref = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
const ref = new Reference(ProgramCmp);
registry.registerDirective(ProgramCmp, {
name: 'ProgramCmp',
ref: ProgramCmpRef,
@ -136,14 +139,15 @@ describe('SelectorScopeRegistry', () => {
expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined();
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
const resolver = new TsReferenceResolver(program, checker, options, host);
const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver);
const ProgramCmpRef = new Reference(ProgramCmp);
const refEmitter = makeReferenceEmitter(program, checker, options, host);
const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter);
registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],
exports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')],
declarations: [new Reference(ProgramCmp)],
exports: [new Reference(
SomeModule,
{specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})],
imports: [],
});
@ -166,4 +170,15 @@ describe('SelectorScopeRegistry', () => {
expect(scope.directives).toBeDefined();
expect(scope.directives.length).toBe(2);
});
});
});
function makeReferenceEmitter(
program: ts.Program, checker: ts.TypeChecker, options: ts.CompilerOptions,
host: ts.CompilerHost): ReferenceEmitter {
const rootDirs = getRootDirs(host, options);
return new ReferenceEmitter([
new LocalIdentifierStrategy(),
new AbsoluteModuleStrategy(program, checker, options, host),
new LogicalProjectStrategy(checker, new LogicalFileSystem(rootDirs)),
]);
}