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

@ -254,6 +254,7 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs):
"createExternalSymbolFactoryReexports": (not _is_bazel()),
# FIXME: wrong place to de-dupe
"expectedOut": depset([o.path for o in expected_outs]).to_list(),
"_useHostForImportGeneration": (not _is_bazel()),
}
if _should_produce_flat_module_outs(ctx):

View File

@ -29,6 +29,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/entry_point",
"//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/routing",
"//packages/compiler-cli/src/ngtsc/shims",

View File

@ -15,6 +15,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/cycles",
"//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/transform",
"//packages/compiler-cli/src/ngtsc/translator",

View File

@ -12,8 +12,9 @@ import * as ts from 'typescript';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations';
import {CycleAnalyzer, ImportGraph} from '../../../ngtsc/cycles';
import {ModuleResolver, TsReferenceResolver} from '../../../ngtsc/imports';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, ReferenceEmitter} from '../../../ngtsc/imports';
import {PartialEvaluator} from '../../../ngtsc/partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from '../../../ngtsc/path';
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../ngtsc/transform';
import {DecoratedClass} from '../host/decorated_class';
import {NgccReflectionHost} from '../host/ngcc_host';
@ -62,9 +63,16 @@ class NgccResourceLoader implements ResourceLoader {
*/
export class DecorationAnalyzer {
resourceManager = new NgccResourceLoader();
resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host);
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver);
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver);
refEmitter = new ReferenceEmitter([
new LocalIdentifierStrategy(),
new AbsoluteModuleStrategy(this.program, this.typeChecker, this.options, this.host),
// TODO(alxhub): there's no reason why ngcc needs the "logical file system" logic here, as ngcc
// projects only ever have one rootDir. Instead, ngcc should just switch its emitted imort based
// on whether a bestGuessOwningModule is present in the Reference.
new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)),
]);
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.refEmitter);
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker);
moduleResolver = new ModuleResolver(this.program, this.options, this.host);
importGraph = new ImportGraph(this.moduleResolver);
cycleAnalyzer = new CycleAnalyzer(this.importGraph);
@ -79,7 +87,7 @@ export class DecorationAnalyzer {
new InjectableDecoratorHandler(this.reflectionHost, this.isCore, /* strictCtorDeps */ false),
new NgModuleDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry,
this.isCore, /* routeAnalyzer */ null),
this.isCore, /* routeAnalyzer */ null, this.refEmitter),
new PipeDecoratorHandler(this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore),
];
@ -87,7 +95,7 @@ export class DecorationAnalyzer {
private program: ts.Program, private options: ts.CompilerOptions,
private host: ts.CompilerHost, private typeChecker: ts.TypeChecker,
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
private rootDirs: string[], private isCore: boolean) {}
private rootDirs: AbsoluteFsPath[], private isCore: boolean) {}
/**
* Analyze a program to find all the decorated files should be transformed.

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ReferencesRegistry} from '../../../ngtsc/annotations';
import {ResolvedReference} from '../../../ngtsc/imports';
import {Reference} from '../../../ngtsc/imports';
import {Declaration} from '../../../ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
@ -62,8 +62,9 @@ export class ModuleWithProvidersAnalyzer {
`The referenced NgModule in ${fn.declaration.getText()} is not a class declaration in the typings program; instead we get ${dtsNgModule.getText()}`);
}
// Record the usage of the internal module as it needs to become an exported symbol
this.referencesRegistry.add(
ngModule.node, new ResolvedReference(ngModule.node, fn.ngModule));
const reference = new Reference(ngModule.node);
reference.addIdentifier(fn.ngModule);
this.referencesRegistry.add(ngModule.node, reference);
ngModule = {node: dtsNgModule, viaModule: null};
}

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ReferencesRegistry} from '../../../ngtsc/annotations';
import {Reference, ResolvedReference} from '../../../ngtsc/imports';
import {Reference} from '../../../ngtsc/imports';
import {Declaration, ReflectionHost} from '../../../ngtsc/reflection';
import {hasNameIdentifier} from '../utils';
@ -31,8 +31,8 @@ export class NgccReferencesRegistry implements ReferencesRegistry {
*/
add(source: ts.Declaration, ...references: Reference<ts.Declaration>[]): void {
references.forEach(ref => {
// Only store resolved references. We are not interested in literals.
if (ref instanceof ResolvedReference && hasNameIdentifier(ref.node)) {
// Only store relative references. We are not interested in literals.
if (ref.bestGuessOwningModule === null && hasNameIdentifier(ref.node)) {
const declaration = this.host.getDeclarationOfIdentifier(ref.node.name);
if (declaration && hasNameIdentifier(declaration.node)) {
this.map.set(declaration.node.name, declaration);

View File

@ -7,10 +7,13 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {BundleProgram, makeBundleProgram} from './bundle_program';
import {EntryPoint, EntryPointFormat} from './entry_point';
/**
* A bundle of files and paths (and TS programs) that correspond to a particular
* format of a package entry-point.
@ -18,7 +21,7 @@ import {EntryPoint, EntryPointFormat} from './entry_point';
export interface EntryPointBundle {
format: EntryPointFormat;
isFlat: boolean;
rootDirs: string[];
rootDirs: AbsoluteFsPath[];
src: BundleProgram;
dts: BundleProgram|null;
}
@ -45,7 +48,7 @@ export function makeEntryPointBundle(
rootDir: entryPoint.path,
};
const host = ts.createCompilerHost(options);
const rootDirs = [entryPoint.path];
const rootDirs = [AbsoluteFsPath.from(entryPoint.path)];
// Create the bundle programs, as necessary.
const src = makeBundleProgram(isCore, path, 'r3_symbols.js', options, host);

View File

@ -12,6 +12,7 @@ ts_library(
"//packages/compiler-cli/src/ngcc",
"//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/transform",

View File

@ -7,6 +7,7 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {Decorator} from '../../../ngtsc/reflection';
import {DecoratorHandler, DetectResult} from '../../../ngtsc/transform';
import {DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
@ -99,7 +100,7 @@ describe('DecorationAnalyzer', () => {
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const analyzer = new DecorationAnalyzer(
program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry,
[''], false);
[AbsoluteFsPath.fromUnchecked('/')], false);
testHandler = createTestHandler();
analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram();
@ -143,7 +144,7 @@ describe('DecorationAnalyzer', () => {
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const analyzer = new DecorationAnalyzer(
program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry,
[''], false);
[AbsoluteFsPath.fromUnchecked('/')], false);
const testHandler = createTestHandler();
analyzer.handlers = [testHandler];
const result = analyzer.analyzeProgram();
@ -161,7 +162,7 @@ describe('DecorationAnalyzer', () => {
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const analyzer = new DecorationAnalyzer(
program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry,
[''], false);
[AbsoluteFsPath.fromUnchecked('/')], false);
const testHandler = createTestHandler();
analyzer.handlers = [testHandler];
const result = analyzer.analyzeProgram();

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ResolvedReference} from '../../../ngtsc/imports';
import {Reference} from '../../../ngtsc/imports';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
@ -136,19 +136,13 @@ describe('PrivateDeclarationsAnalyzer', () => {
// decoration handlers in the `DecorationAnalyzer`.
const publicComponentDeclaration =
getDeclaration(program, '/src/a.js', 'PublicComponent', ts.isClassDeclaration);
referencesRegistry.add(
null !,
new ResolvedReference(publicComponentDeclaration, publicComponentDeclaration.name !));
referencesRegistry.add(null !, new Reference(publicComponentDeclaration));
const privateComponentDeclaration =
getDeclaration(program, '/src/b.js', 'PrivateComponent', ts.isClassDeclaration);
referencesRegistry.add(
null !, new ResolvedReference(
privateComponentDeclaration, privateComponentDeclaration.name !));
referencesRegistry.add(null !, new Reference(privateComponentDeclaration));
const internalComponentDeclaration =
getDeclaration(program, '/src/c.js', 'InternalComponent', ts.isClassDeclaration);
referencesRegistry.add(
null !, new ResolvedReference(
internalComponentDeclaration, internalComponentDeclaration.name !));
referencesRegistry.add(null !, new Reference(internalComponentDeclaration));
const analyses = analyzer.analyzeProgram(program);
expect(analyses.length).toEqual(2);

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {Reference, TsReferenceResolver} from '../../../ngtsc/imports';
import {Reference} from '../../../ngtsc/imports';
import {PartialEvaluator} from '../../../ngtsc/partial_evaluator';
import {TypeScriptReflectionHost} from '../../../ngtsc/reflection';
import {getDeclaration, makeProgram} from '../../../ngtsc/testing/in_memory_typescript';
@ -39,11 +39,11 @@ describe('NgccReferencesRegistry', () => {
const testArrayExpression = testArrayDeclaration.initializer !;
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 registry = new NgccReferencesRegistry(reflectionHost);
const references = evaluator.evaluate(testArrayExpression) as Reference<ts.Declaration>[];
const references = (evaluator.evaluate(testArrayExpression) as any[])
.filter(ref => ref instanceof Reference) as Reference<ts.Declaration>[];
registry.add(null !, ...references);
const map = registry.getDeclarationMap();

View File

@ -7,6 +7,7 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {makeProgram} from '../../../ngtsc/testing/in_memory_typescript';
import {BundleProgram} from '../../src/packages/bundle_program';
import {EntryPointFormat} from '../../src/packages/entry_point';
@ -15,6 +16,7 @@ import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
export {getDeclaration} from '../../../ngtsc/testing/in_memory_typescript';
/**
*
* @param format The format of the bundle.
@ -27,7 +29,7 @@ export function makeTestEntryPointBundle(
const src = makeTestBundleProgram(files);
const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null;
const isFlat = src.r3SymbolsFile === null;
return {format, rootDirs: ['/'], src, dts, isFlat};
return {format, rootDirs: [AbsoluteFsPath.fromUnchecked('/')], src, dts, isFlat};
}
/**

View File

@ -8,6 +8,7 @@
import {dirname} from 'canonical-path';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
@ -21,10 +22,11 @@ function setup(file: {name: string, contents: string}) {
const typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm2015ReflectionHost(false, typeChecker);
const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = new DecorationAnalyzer(
bundle.src.program, bundle.src.options, bundle.src.host,
typeChecker, host, referencesRegistry, [''], false)
.analyzeProgram();
const decorationAnalyses =
new DecorationAnalyzer(
bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host,
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new EsmRenderer(host, false, bundle, dir, dir);
return {

View File

@ -8,6 +8,7 @@
import {dirname} from 'canonical-path';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
@ -21,10 +22,11 @@ function setup(file: {name: string, contents: string}) {
const typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm5ReflectionHost(false, typeChecker);
const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = new DecorationAnalyzer(
bundle.src.program, bundle.src.options, bundle.src.host,
typeChecker, host, referencesRegistry, [''], false)
.analyzeProgram();
const decorationAnalyses =
new DecorationAnalyzer(
bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host,
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new Esm5Renderer(host, false, bundle, dir, dir);
return {

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)),
]);
}

View File

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

View File

@ -7,5 +7,6 @@
*/
export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core';
export {AbsoluteReference, ImportMode, NodeReference, Reference, ResolvedReference} from './src/references';
export {ModuleResolver, ReferenceResolver, TsReferenceResolver} from './src/resolver';
export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter} from './src/emitter';
export {ImportMode, OwningModule, Reference} from './src/references';
export {ModuleResolver} from './src/resolver';

View File

@ -0,0 +1,271 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Expression, ExternalExpr, WrappedNodeExpr} from '@angular/compiler';
import {ExternalReference} from '@angular/compiler/src/compiler';
import * as ts from 'typescript';
import {LogicalFileSystem, LogicalProjectPath} from '../../path';
import {getSourceFile, isDeclaration, nodeNameForError} from '../../util/src/typescript';
import {findExportedNameOfNode} from './find_export';
import {ImportMode, Reference} from './references';
/**
* A host which supports an operation to convert a file name into a module name.
*
* This operation is typically implemented as part of the compiler host passed to ngtsc when running
* under a build tool like Bazel or Blaze.
*/
export interface FileToModuleHost {
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string;
}
/**
* A particular strategy for generating an expression which refers to a `Reference`.
*
* There are many potential ways a given `Reference` could be referred to in the context of a given
* file. A local declaration could be available, the `Reference` could be importable via a relative
* import within the project, or an absolute import into `node_modules` might be necessary.
*
* Different `ReferenceEmitStrategy` implementations implement specific logic for generating such
* references. A single strategy (such as using a local declaration) may not always be able to
* generate an expression for every `Reference` (for example, if no local identifier is available),
* and may return `null` in such a case.
*/
export interface ReferenceEmitStrategy {
/**
* Emit an `Expression` which refers to the given `Reference` in the context of a particular
* source file, if possible.
*
* @param ref the `Reference` for which to generate an expression
* @param context the source file in which the `Expression` must be valid
* @param importMode 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
*/
emit(ref: Reference, context: ts.SourceFile, importMode: ImportMode): Expression|null;
}
/**
* Generates `Expression`s which refer to `Reference`s in a given context.
*
* A `ReferenceEmitter` uses one or more `ReferenceEmitStrategy` implementations to produce a
* an `Expression` which refers to a `Reference` in the context of a particular file.
*/
export class ReferenceEmitter {
constructor(private strategies: ReferenceEmitStrategy[]) {}
emit(
ref: Reference, context: ts.SourceFile,
importMode: ImportMode = ImportMode.UseExistingImport): Expression {
for (const strategy of this.strategies) {
const emitted = strategy.emit(ref, context, importMode);
if (emitted !== null) {
return emitted;
}
}
throw new Error(
`Unable to write a reference to ${nodeNameForError(ref.node)} in ${ref.node.getSourceFile().fileName} from ${context.fileName}`);
}
}
/**
* A `ReferenceEmitStrategy` which will refer to declarations by any local `ts.Identifier`s, if
* such identifiers are available.
*/
export class LocalIdentifierStrategy implements ReferenceEmitStrategy {
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importMode: ImportMode): Expression|null {
// If the emitter has specified ForceNewImport, then LocalIdentifierStrategy should not use a
// local identifier at all, *except* in the source file where the node is actually declared.
if (importMode === ImportMode.ForceNewImport &&
getSourceFile(ref.node) !== getSourceFile(context)) {
return null;
}
// A Reference can have multiple identities in different files, so it may already have an
// Identifier in the requested context file.
const identifier = ref.getIdentityIn(context);
if (identifier !== null) {
return new WrappedNodeExpr(identifier);
} else {
return null;
}
}
}
/**
* A `ReferenceEmitStrategy` which will refer to declarations that come from `node_modules` using
* an absolute import.
*
* Part of this strategy involves looking at the target entry point and identifying the exported
* name of the targeted declaration, as it might be different from the declared name (e.g. a
* directive might be declared as FooDirImpl, but exported as FooDir). If no export can be found
* which maps back to the original directive, an error is thrown.
*/
export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
/**
* A cache of the exports of specific modules, because resolving a module to its exports is a
* costly operation.
*/
private moduleExportsCache = new Map<string, Map<ts.Declaration, string>|null>();
constructor(
private program: ts.Program, private checker: ts.TypeChecker,
private options: ts.CompilerOptions, private host: ts.CompilerHost) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importMode: ImportMode): Expression|null {
if (ref.bestGuessOwningModule === null) {
// There is no module name available for this Reference, meaning it was arrived at via a
// relative path.
return null;
} else if (!isDeclaration(ref.node)) {
// It's not possible to import something which isn't a declaration.
throw new Error('Debug assert: importing a Reference to non-declaration?');
}
// Try to find the exported name of the declaration, if one is available.
const {specifier, resolutionContext} = ref.bestGuessOwningModule;
const symbolName = this.resolveImportName(specifier, ref.node, resolutionContext);
if (symbolName === null) {
// TODO(alxhub): make this error a ts.Diagnostic pointing at whatever caused this import to be
// triggered.
throw new Error(
`Symbol ${ref.debugName} declared in ${getSourceFile(ref.node).fileName} is not exported from ${specifier} (import into ${context.fileName})`);
}
return new ExternalExpr(new ExternalReference(specifier, symbolName));
}
private resolveImportName(moduleName: string, target: ts.Declaration, fromFile: string): string
|null {
const exports = this.getExportsOfModule(moduleName, fromFile);
if (exports !== null && exports.has(target)) {
return exports.get(target) !;
} else {
return null;
}
}
private getExportsOfModule(moduleName: string, fromFile: string):
Map<ts.Declaration, string>|null {
if (!this.moduleExportsCache.has(moduleName)) {
this.moduleExportsCache.set(moduleName, this.enumerateExportsOfModule(moduleName, fromFile));
}
return this.moduleExportsCache.get(moduleName) !;
}
private enumerateExportsOfModule(specifier: string, fromFile: string):
Map<ts.Declaration, string>|null {
// First, resolve the module specifier to its entry point, and get the ts.Symbol for it.
const resolved = ts.resolveModuleName(specifier, fromFile, this.options, this.host);
if (resolved.resolvedModule === undefined) {
return null;
}
const entryPointFile = this.program.getSourceFile(resolved.resolvedModule.resolvedFileName);
if (entryPointFile === undefined) {
return null;
}
const entryPointSymbol = this.checker.getSymbolAtLocation(entryPointFile);
if (entryPointSymbol === undefined) {
return null;
}
// Next, build a Map of all the ts.Declarations exported via the specifier and their exported
// names.
const exportMap = new Map<ts.Declaration, string>();
const exports = this.checker.getExportsOfModule(entryPointSymbol);
for (const expSymbol of exports) {
// Resolve export symbols to their actual declarations.
const declSymbol = expSymbol.flags & ts.SymbolFlags.Alias ?
this.checker.getAliasedSymbol(expSymbol) :
expSymbol;
// At this point the valueDeclaration of the symbol should be defined.
const decl = declSymbol.valueDeclaration;
if (decl === undefined) {
continue;
}
// Prefer importing the symbol via its declared name, but take any export of it otherwise.
if (declSymbol.name === expSymbol.name || !exportMap.has(decl)) {
exportMap.set(decl, expSymbol.name);
}
}
return exportMap;
}
}
/**
* A `ReferenceEmitStrategy` which will refer to declarations via relative paths, provided they're
* both in the logical project "space" of paths.
*
* This is trickier than it sounds, as the two files may be in different root directories in the
* project. Simply calculating a file system relative path between the two is not sufficient.
* Instead, `LogicalProjectPath`s are used.
*/
export class LogicalProjectStrategy implements ReferenceEmitStrategy {
constructor(private checker: ts.TypeChecker, private logicalFs: LogicalFileSystem) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null {
const destSf = getSourceFile(ref.node);
// Compute the relative path from the importing file to the file being imported. This is done
// as a logical path computation, because the two files might be in different rootDirs.
const destPath = this.logicalFs.logicalPathOfSf(destSf);
if (destPath === null) {
// The imported file is not within the logical project filesystem.
return null;
}
const originPath = this.logicalFs.logicalPathOfSf(context);
if (originPath === null) {
throw new Error(
`Debug assert: attempt to import from ${context.fileName} but it's outside the program?`);
}
// There's no way to emit a relative reference from a file to itself.
if (destPath === originPath) {
return null;
}
const name = findExportedNameOfNode(ref.node, destSf, this.checker);
if (name === null) {
// The target declaration isn't exported from the file it's declared in. This is an issue!
return null;
}
// With both files expressed as LogicalProjectPaths, getting the module specifier as a relative
// path is now straightforward.
const moduleName = LogicalProjectPath.relativePathBetween(originPath, destPath);
return new ExternalExpr({moduleName, name});
}
}
/**
* A `ReferenceEmitStrategy` which uses a `FileToModuleHost` to generate absolute import references.
*/
export class FileToModuleStrategy implements ReferenceEmitStrategy {
constructor(private checker: ts.TypeChecker, private fileToModuleHost: FileToModuleHost) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null {
const destSf = getSourceFile(ref.node);
const name = findExportedNameOfNode(ref.node, destSf, this.checker);
if (name === null) {
return null;
}
const moduleName =
this.fileToModuleHost.fileNameToModuleName(destSf.fileName, context.fileName);
return new ExternalExpr({moduleName, name});
}
}

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
/**
* Find the name, if any, by which a node is exported from a given file.
*/
export function findExportedNameOfNode(
target: ts.Node, file: ts.SourceFile, checker: ts.TypeChecker): string|null {
// First, get the exports of the file.
const symbol = checker.getSymbolAtLocation(file);
if (symbol === undefined) {
return null;
}
const exports = checker.getExportsOfModule(symbol);
// Look for the export which declares the node.
const found = exports.find(sym => symbolDeclaresNode(sym, target, checker));
if (found === undefined) {
throw new Error(`failed to find target in ${file.fileName}`);
}
return found !== undefined ? found.name : null;
}
/**
* Check whether a given `ts.Symbol` represents a declaration of a given node.
*
* This is not quite as trivial as just checking the declarations, as some nodes are
* `ts.ExportSpecifier`s and need to be unwrapped.
*/
function symbolDeclaresNode(sym: ts.Symbol, node: ts.Node, checker: ts.TypeChecker): boolean {
return sym.declarations.some(decl => {
if (ts.isExportSpecifier(decl)) {
const exportedSymbol = checker.getExportSpecifierLocalTargetSymbol(decl);
if (exportedSymbol !== undefined) {
return symbolDeclaresNode(exportedSymbol, node, checker);
}
}
return decl === node;
});
}

View File

@ -6,145 +6,98 @@
* found in the LICENSE file at https://angular.io/license
*/
/// <reference types="node" />
import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
import {identifierOfNode} from '../../util/src/typescript';
export enum ImportMode {
UseExistingImport,
ForceNewImport,
}
export interface OwningModule {
specifier: string;
resolutionContext: string;
}
/**
* A reference to a `ts.Node`.
* A `ts.Node` plus the context in which it was discovered.
*
* For example, if an expression evaluates to a function or class definition, it will be returned
* as a `Reference` (assuming references are allowed in evaluation).
* A `Reference` is a pointer to a `ts.Node` that was extracted from the program somehow. It
* contains not only the node itself, but the information regarding how the node was located. In
* particular, it might track different identifiers by which the node is exposed, as well as
* potentially a module specifier which might expose the node.
*
* The Angular compiler uses `Reference`s instead of `ts.Node`s when tracking classes or generating
* imports.
*/
export abstract class Reference<T extends ts.Node = ts.Node> {
constructor(readonly node: T) {}
export class Reference<T extends ts.Node = ts.Node> {
/**
* Whether an `Expression` can be generated which references the node.
*/
// TODO(issue/24571): remove '!'.
readonly expressable !: boolean;
/**
* Generate an `Expression` representing this type, in the context of the given SourceFile.
* The compiler's best guess at an absolute module specifier which owns this `Reference`.
*
* This could be a local variable reference, if the symbol is imported, or it could be a new
* import if needed.
* This is usually determined by tracking the import statements which led the compiler to a given
* node. If any of these imports are absolute, it's an indication that the node being imported
* might come from that module.
*
* It is not _guaranteed_ that the node in question is exported from its `bestGuessOwningModule` -
* that is mostly a convention that applies in certain package formats.
*
* If `bestGuessOwningModule` is `null`, then it's likely the node came from the current program.
*/
abstract toExpression(context: ts.SourceFile, importMode?: ImportMode): Expression|null;
readonly bestGuessOwningModule: OwningModule|null;
abstract addIdentifier(identifier: ts.Identifier): void;
}
/**
* A reference to a node only, without any ability to get an `Expression` representing that node.
*
* This is used for returning references to things like method declarations, which are not directly
* referenceable.
*/
export class NodeReference<T extends ts.Node = ts.Node> extends Reference<T> {
constructor(node: T, readonly moduleName: string|null) { super(node); }
toExpression(context: ts.SourceFile): null { return null; }
addIdentifier(identifier: ts.Identifier): void {}
}
/**
* A reference to a node which has a `ts.Identifier` and can be resolved to an `Expression`.
*
* Imports generated by `ResolvedReference`s are always relative.
*/
export class ResolvedReference<T extends ts.Node = ts.Node> extends Reference<T> {
protected identifiers: ts.Identifier[] = [];
constructor(node: T, protected primaryIdentifier: ts.Identifier) { super(node); }
readonly expressable = true;
toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.UseExistingImport):
Expression {
const localIdentifier =
pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode);
if (localIdentifier !== null) {
return new WrappedNodeExpr(localIdentifier);
} else {
// Relative import from context -> this.node.getSourceFile().
// TODO(alxhub): investigate the impact of multiple source roots here.
// TODO(alxhub): investigate the need to map such paths via the Host for proper g3 support.
let relative =
path.posix.relative(path.dirname(context.fileName), this.node.getSourceFile().fileName)
.replace(TS_DTS_JS_EXTENSION, '');
// path.relative() does not include the leading './'.
if (!relative.startsWith('.')) {
relative = `./${relative}`;
}
// path.relative() returns the empty string (converted to './' above) if the two paths are the
// same.
if (relative === './') {
// Same file after all.
return new WrappedNodeExpr(this.primaryIdentifier);
} else {
return new ExternalExpr(new ExternalReference(relative, this.primaryIdentifier.text));
}
}
}
addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); }
}
/**
* A reference to a node which has a `ts.Identifer` and an expected absolute module name.
*
* An `AbsoluteReference` can be resolved to an `Expression`, and if that expression is an import
* the module specifier will be an absolute module name, not a relative path.
*/
export class AbsoluteReference<T extends ts.Node> extends Reference<T> {
private identifiers: ts.Identifier[] = [];
constructor(
node: T, private primaryIdentifier: ts.Identifier, readonly moduleName: string,
readonly symbolName: string) {
super(node);
}
readonly expressable = true;
constructor(readonly node: T, bestGuessOwningModule: OwningModule|null = null) {
this.bestGuessOwningModule = bestGuessOwningModule;
toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.UseExistingImport):
Expression {
const localIdentifier =
pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode);
if (localIdentifier !== null) {
return new WrappedNodeExpr(localIdentifier);
} else {
return new ExternalExpr(new ExternalReference(this.moduleName, this.symbolName));
const id = identifierOfNode(node);
if (id !== null) {
this.identifiers.push(id);
}
}
addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); }
}
function pickIdentifier(
context: ts.SourceFile, primary: ts.Identifier, secondaries: ts.Identifier[],
mode: ImportMode): ts.Identifier|null {
context = ts.getOriginalNode(context) as ts.SourceFile;
if (ts.getOriginalNode(primary).getSourceFile() === context) {
return primary;
} else if (mode === ImportMode.UseExistingImport) {
return secondaries.find(id => ts.getOriginalNode(id).getSourceFile() === context) || null;
} else {
return null;
/**
* The best guess at which module specifier owns this particular reference, or `null` if there
* isn't one.
*/
get ownedByModuleGuess(): string|null {
if (this.bestGuessOwningModule !== null) {
return this.bestGuessOwningModule.specifier;
} else {
return null;
}
}
}
/**
* Whether this reference has a potential owning module or not.
*
* See `bestGuessOwningModule`.
*/
get hasOwningModuleGuess(): boolean { return this.bestGuessOwningModule !== null; }
/**
* A name for the node, if one is available.
*
* This is only suited for debugging. Any actual references to this node should be made with
* `ts.Identifier`s (see `getIdentityIn`).
*/
get debugName(): string|null {
const id = identifierOfNode(this.node);
return id !== null ? id.text : null;
}
/**
* Record a `ts.Identifier` by which it's valid to refer to this node, within the context of this
* `Reference`.
*/
addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); }
/**
* Get a `ts.Identifier` within this `Reference` that can be used to refer within the context of a
* given `ts.SourceFile`, if any.
*/
getIdentityIn(context: ts.SourceFile): ts.Identifier|null {
return this.identifiers.find(id => id.getSourceFile() === context) || null;
}
}

View File

@ -8,9 +8,7 @@
import * as ts from 'typescript';
import {isFromDtsFile} from '../../util/src/typescript';
import {AbsoluteReference, Reference, ResolvedReference} from './references';
import {Reference} from './references';
export interface ReferenceResolver {
resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string):
@ -38,101 +36,3 @@ export class ModuleResolver {
return this.program.getSourceFile(resolved.resolvedFileName) || null;
}
}
export class TsReferenceResolver implements ReferenceResolver {
private moduleExportsCache = new Map<string, Map<ts.Declaration, string>|null>();
constructor(
private program: ts.Program, private checker: ts.TypeChecker,
private options: ts.CompilerOptions, private host: ts.CompilerHost) {}
resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string):
Reference<ts.Declaration> {
const id = identifierOfDeclaration(decl);
if (id === undefined) {
throw new Error(`Internal error: don't know how to refer to ${ts.SyntaxKind[decl.kind]}`);
}
if (!isFromDtsFile(decl) || importFromHint === null) {
return new ResolvedReference(decl, id);
} else {
const publicName = this.resolveImportName(importFromHint, decl, fromFile);
if (publicName !== null) {
return new AbsoluteReference(decl, id, importFromHint, publicName);
} else {
throw new Error(`Internal error: Symbol ${id.text} is not exported from ${importFromHint}`);
}
}
}
private resolveImportName(moduleName: string, target: ts.Declaration, fromFile: string): string
|null {
const exports = this.getExportsOfModule(moduleName, fromFile);
if (exports !== null && exports.has(target)) {
return exports.get(target) !;
} else {
return null;
}
}
private getExportsOfModule(moduleName: string, fromFile: string):
Map<ts.Declaration, string>|null {
if (!this.moduleExportsCache.has(moduleName)) {
this.moduleExportsCache.set(moduleName, this.enumerateExportsOfModule(moduleName, fromFile));
}
return this.moduleExportsCache.get(moduleName) !;
}
private enumerateExportsOfModule(moduleName: string, fromFile: string):
Map<ts.Declaration, string>|null {
const resolved = ts.resolveModuleName(moduleName, fromFile, this.options, this.host);
if (resolved.resolvedModule === undefined) {
return null;
}
const indexFile = this.program.getSourceFile(resolved.resolvedModule.resolvedFileName);
if (indexFile === undefined) {
return null;
}
const indexSymbol = this.checker.getSymbolAtLocation(indexFile);
if (indexSymbol === undefined) {
return null;
}
const exportMap = new Map<ts.Declaration, string>();
const exports = this.checker.getExportsOfModule(indexSymbol);
for (const expSymbol of exports) {
const declSymbol = expSymbol.flags & ts.SymbolFlags.Alias ?
this.checker.getAliasedSymbol(expSymbol) :
expSymbol;
const decl = declSymbol.valueDeclaration;
if (decl === undefined) {
continue;
}
if (declSymbol.name === expSymbol.name || !exportMap.has(decl)) {
exportMap.set(decl, expSymbol.name);
}
}
return exportMap;
}
}
function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined {
if (ts.isClassDeclaration(decl)) {
return decl.name;
} else if (ts.isEnumDeclaration(decl)) {
return decl.name;
} else if (ts.isFunctionDeclaration(decl)) {
return decl.name;
} else if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) {
return decl.name;
} else if (ts.isShorthandPropertyAssignment(decl)) {
return decl.name;
} else {
return undefined;
}
}

View File

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

View File

@ -8,7 +8,8 @@
import * as ts from 'typescript';
import {AbsoluteReference, NodeReference, Reference, ReferenceResolver, ResolvedReference} from '../../imports';
import {Reference} from '../../imports';
import {OwningModule} from '../../imports/src/references';
import {Declaration, ReflectionHost} from '../../reflection';
import {ArraySliceBuiltinFn} from './builtin';
@ -78,9 +79,7 @@ interface Context {
}
export class StaticInterpreter {
constructor(
private host: ReflectionHost, private checker: ts.TypeChecker,
private refResolver: ReferenceResolver) {}
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
visit(node: ts.Expression, context: Context): ResolvedValue {
return this.visitExpression(node, context);
@ -332,10 +331,7 @@ export class StaticInterpreter {
} else if (lhs instanceof Reference) {
const ref = lhs.node;
if (this.host.isClass(ref)) {
let absoluteModuleName = context.absoluteModuleName;
if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) {
absoluteModuleName = lhs.moduleName || absoluteModuleName;
}
const module = owningModule(context, lhs.bestGuessOwningModule);
let value: ResolvedValue = undefined;
const member = this.host.getMembersOfClass(ref).find(
member => member.isStatic && member.name === strIndex);
@ -343,9 +339,9 @@ export class StaticInterpreter {
if (member.value !== null) {
value = this.visitExpression(member.value, context);
} else if (member.implementation !== null) {
value = new NodeReference(member.implementation, absoluteModuleName);
value = new Reference(member.implementation, module);
} else if (member.node) {
value = new NodeReference(member.node, absoluteModuleName);
value = new Reference(member.node, module);
}
}
return value;
@ -391,11 +387,10 @@ export class StaticInterpreter {
// If the function is declared in a different file, resolve the foreign function expression
// using the absolute module name of that file (if any).
if ((lhs instanceof NodeReference || lhs instanceof AbsoluteReference) &&
lhs.moduleName !== null) {
if (lhs.bestGuessOwningModule !== null) {
context = {
...context,
absoluteModuleName: lhs.moduleName,
absoluteModuleName: lhs.bestGuessOwningModule.specifier,
resolutionContext: node.getSourceFile().fileName,
};
}
@ -496,7 +491,7 @@ export class StaticInterpreter {
}
private getReference(node: ts.Declaration, context: Context): Reference {
return this.refResolver.resolve(node, context.absoluteModuleName, context.resolutionContext);
return new Reference(node, owningModule(context));
}
}
@ -542,3 +537,18 @@ function joinModuleContext(existing: Context, node: ts.Node, decl: Declaration):
return EMPTY;
}
}
function owningModule(context: Context, override: OwningModule | null = null): OwningModule|null {
let specifier = context.absoluteModuleName;
if (override !== null) {
specifier = override.specifier;
}
if (specifier !== null) {
return {
specifier,
resolutionContext: context.resolutionContext,
};
} else {
return null;
}
}

View File

@ -6,10 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteReference, Reference, TsReferenceResolver} from '../../imports';
import {Reference} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {PartialEvaluator} from '../src/interface';
@ -44,8 +43,7 @@ function evaluate<T extends ResolvedValue>(
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): T {
const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles);
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);
return evaluator.evaluate(expression) as T;
}
@ -150,22 +148,17 @@ describe('ngtsc metadata', () => {
const reflectionHost = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const resolver = new TsReferenceResolver(program, checker, options, host);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference');
}
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !);
if (!(reference instanceof WrappedNodeExpr)) {
return fail('Expected expression reference to be a wrapped node');
const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !);
if (reference === null) {
return fail('Expected to get an identifier');
}
if (!ts.isIdentifier(reference.node)) {
return fail('Expected expression to be an Identifier');
}
expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
expect(reference.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
});
it('absolute imports work', () => {
@ -183,23 +176,16 @@ describe('ngtsc metadata', () => {
const reflectionHost = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const resolver = new TsReferenceResolver(program, checker, options, host);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof AbsoluteReference)) {
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to an absolute reference');
}
expect(resolved.moduleName).toBe('some_library');
expect(owningModuleOf(resolved)).toBe('some_library');
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !);
if (!(reference instanceof WrappedNodeExpr)) {
return fail('Expected expression reference to be a wrapped node');
}
if (!ts.isIdentifier(reference.node)) {
return fail('Expected expression to be an Identifier');
}
expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !);
expect(reference).not.toBeNull();
expect(reference !.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
});
it('reads values from default exports', () => {
@ -282,3 +268,7 @@ describe('ngtsc metadata', () => {
expect(value instanceof Reference).toBe(true);
});
});
function owningModuleOf(ref: Reference): string|null {
return ref.bestGuessOwningModule !== null ? ref.bestGuessOwningModule.specifier : null;
}

View File

@ -17,8 +17,10 @@ import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {CycleAnalyzer, ImportGraph} from './cycles';
import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {ImportRewriter, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, TsReferenceResolver} from './imports';
import {AbsoluteModuleStrategy, FileToModuleHost, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {FileToModuleStrategy} from './imports/src/emitter';
import {PartialEvaluator} from './partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from './path';
import {TypeScriptReflectionHost} from './reflection';
import {HostResourceLoader} from './resource_loader';
import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
@ -27,7 +29,7 @@ import {ivySwitchTransform} from './switch';
import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
import {TypeCheckContext, TypeCheckProgramHost} from './typecheck';
import {normalizeSeparators} from './util/src/path';
import {isDtsPath} from './util/src/typescript';
import {getRootDirs, isDtsPath} from './util/src/typescript';
export class NgtscProgram implements api.Program {
private tsProgram: ts.Program;
@ -40,7 +42,7 @@ export class NgtscProgram implements api.Program {
private _importRewriter: ImportRewriter|undefined = undefined;
private _reflector: TypeScriptReflectionHost|undefined = undefined;
private _isCore: boolean|undefined = undefined;
private rootDirs: string[];
private rootDirs: AbsoluteFsPath[];
private closureCompilerEnabled: boolean;
private entryPoint: ts.SourceFile|null;
private exportReferenceGraph: ReferenceGraph|null = null;
@ -51,22 +53,20 @@ export class NgtscProgram implements api.Program {
private moduleResolver: ModuleResolver;
private cycleAnalyzer: CycleAnalyzer;
private refEmitter: ReferenceEmitter|null = null;
private fileToModuleHost: FileToModuleHost|null = null;
constructor(
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
host: api.CompilerHost, oldProgram?: api.Program) {
this.rootDirs = [];
if (options.rootDirs !== undefined) {
this.rootDirs.push(...options.rootDirs);
} else if (options.rootDir !== undefined) {
this.rootDirs.push(options.rootDir);
} else {
this.rootDirs.push(host.getCurrentDirectory());
}
this.rootDirs = getRootDirs(host, options);
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
this.resourceManager = new HostResourceLoader(host, options);
const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
this.host = host;
if (host.fileNameToModuleName !== undefined) {
this.fileToModuleHost = host as FileToModuleHost;
}
let rootFiles = [...rootNames];
const generators: ShimGenerator[] = [];
@ -168,7 +168,7 @@ export class NgtscProgram implements api.Program {
const compilation = this.ensureAnalyzed();
const diagnostics = [...compilation.diagnostics];
if (!!this.options.fullTemplateTypeCheck) {
const ctx = new TypeCheckContext();
const ctx = new TypeCheckContext(this.refEmitter !);
compilation.typeCheck(ctx);
diagnostics.push(...this.compileTypeCheckProgram(ctx));
}
@ -315,9 +315,32 @@ export class NgtscProgram implements api.Program {
private makeCompilation(): IvyCompilation {
const checker = this.tsProgram.getTypeChecker();
const refResolver = new TsReferenceResolver(this.tsProgram, checker, this.options, this.host);
const evaluator = new PartialEvaluator(this.reflector, checker, refResolver);
const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, refResolver);
// Construct the ReferenceEmitter.
if (this.fileToModuleHost === null || !this.options._useHostForImportGeneration) {
// The CompilerHost doesn't have fileNameToModuleName, so build an NPM-centric reference
// resolution strategy.
this.refEmitter = new ReferenceEmitter([
// First, try to use local identifiers if available.
new LocalIdentifierStrategy(),
// Next, attempt to use an absolute import.
new AbsoluteModuleStrategy(this.tsProgram, checker, this.options, this.host),
// Finally, check if the reference is being written into a file within the project's logical
// file system, and use a relative import if so. If this fails, ReferenceEmitter will throw
// an error.
new LogicalProjectStrategy(checker, new LogicalFileSystem(this.rootDirs)),
]);
} else {
// The CompilerHost supports fileNameToModuleName, so use that to emit imports.
this.refEmitter = new ReferenceEmitter([
// First, try to use local identifiers if available.
new LocalIdentifierStrategy(),
// Then use fileNameToModuleName to emit imports.
new FileToModuleStrategy(checker, this.fileToModuleHost),
]);
}
const evaluator = new PartialEvaluator(this.reflector, checker);
const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, this.refEmitter);
// If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in
// order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there
@ -344,7 +367,7 @@ export class NgtscProgram implements api.Program {
this.reflector, this.isCore, this.options.strictInjectionParameters || false),
new NgModuleDecoratorHandler(
this.reflector, evaluator, scopeRegistry, referencesRegistry, this.isCore,
this.routeAnalyzer),
this.routeAnalyzer, this.refEmitter),
new PipeDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore),
];

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {AbsoluteReference, NodeReference, Reference} from '../../imports';
import {Reference} from '../../imports';
import {ForeignFunctionResolver, PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
import {NgModuleRawRouteData} from './analyzer';
@ -163,7 +163,9 @@ const routerModuleFFR: ForeignFunctionResolver =
null {
if (!isMethodNodeReference(ref) || !ts.isClassDeclaration(ref.node.parent)) {
return null;
} else if (ref.moduleName !== '@angular/router') {
} else if (
ref.bestGuessOwningModule === null ||
ref.bestGuessOwningModule.specifier !== '@angular/router') {
return null;
} else if (
ref.node.parent.name === undefined || ref.node.parent.name.text !== 'RouterModule') {
@ -188,11 +190,11 @@ function hasIdentifier(node: ts.Node): node is ts.Node&{name: ts.Identifier} {
function isMethodNodeReference(
ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>):
ref is NodeReference<ts.MethodDeclaration> {
return ref instanceof NodeReference && ts.isMethodDeclaration(ref.node);
ref is Reference<ts.MethodDeclaration> {
return ts.isMethodDeclaration(ref.node);
}
function isRouteToken(ref: ResolvedValue): boolean {
return ref instanceof AbsoluteReference && ref.moduleName === '@angular/router' &&
ref.symbolName === 'ROUTES';
return ref instanceof Reference && ref.bestGuessOwningModule !== null &&
ref.bestGuessOwningModule.specifier === '@angular/router' && ref.debugName === 'ROUTES';
}

View File

@ -9,7 +9,7 @@
import {R3TargetBinder, SelectorMatcher, TmplAstNode} from '@angular/compiler';
import * as ts from 'typescript';
import {NoopImportRewriter} from '../../imports';
import {NoopImportRewriter, ReferenceEmitter} from '../../imports';
import {ImportManager} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCtorMetadata} from './api';
@ -26,6 +26,8 @@ import {generateTypeCtor} from './type_constructor';
* checking code.
*/
export class TypeCheckContext {
constructor(private refEmitter: ReferenceEmitter) {}
/**
* A `Set` of classes which will be used to generate type constructors.
*/
@ -136,7 +138,7 @@ export class TypeCheckContext {
// Process each operation and use the printer to generate source code for it, inserting it into
// the source code in between the original chunks.
ops.forEach((op, idx) => {
const text = op.execute(importManager, sf, printer);
const text = op.execute(importManager, sf, this.refEmitter, printer);
code += text + textParts[idx + 1];
});
@ -182,7 +184,8 @@ interface Op {
/**
* Execute the operation and return the generated code as text.
*/
execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string;
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer):
string;
}
/**
@ -196,8 +199,9 @@ class TcbOp implements Op {
*/
get splitPoint(): number { return this.node.end + 1; }
execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string {
const tcb = generateTypeCheckBlock(this.node, this.meta, im);
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer):
string {
const tcb = generateTypeCheckBlock(this.node, this.meta, im, refEmitter);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
}
}
@ -213,7 +217,8 @@ class TypeCtorOp implements Op {
*/
get splitPoint(): number { return this.node.end - 1; }
execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string {
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer):
string {
const tcb = generateTypeCtor(this.node, this.meta);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
}

View File

@ -9,7 +9,7 @@
import {AST, BindingType, BoundTarget, ImplicitReceiver, PropertyRead, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {Reference, ReferenceEmitter} from '../../imports';
import {ImportManager, translateExpression} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api';
@ -28,9 +28,9 @@ import {astToTypescript} from './expression';
* @param importManager an `ImportManager` for the file into which the TCB will be written.
*/
export function generateTypeCheckBlock(
node: ts.ClassDeclaration, meta: TypeCheckBlockMetadata,
importManager: ImportManager): ts.FunctionDeclaration {
const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager);
node: ts.ClassDeclaration, meta: TypeCheckBlockMetadata, importManager: ImportManager,
refEmitter: ReferenceEmitter): ts.FunctionDeclaration {
const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager, refEmitter);
const scope = new Scope(tcb);
tcbProcessNodes(meta.boundTarget.target.template !, tcb, scope);
@ -59,7 +59,8 @@ class Context {
constructor(
readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>,
private sourceFile: ts.SourceFile, private importManager: ImportManager) {}
private sourceFile: ts.SourceFile, private importManager: ImportManager,
private refEmitter: ReferenceEmitter) {}
/**
* Allocate a new variable name for use within the `Context`.
@ -75,7 +76,7 @@ class Context {
* This may involve importing the node into the file if it's not declared there already.
*/
reference(ref: Reference<ts.Node>): ts.Expression {
const ngExpr = ref.toExpression(this.sourceFile);
const ngExpr = this.refEmitter.emit(ref, this.sourceFile);
if (ngExpr === null) {
throw new Error(`Unreachable reference: ${ref.node}`);
}

View File

@ -10,8 +10,11 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",
"@ngdeps//typescript",
],
)

View File

@ -8,7 +8,10 @@
import * as ts from 'typescript';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitter} from '../../imports';
import {LogicalFileSystem} from '../../path';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getRootDirs} from '../../util/src/typescript';
import {TypeCheckContext} from '../src/context';
import {TypeCheckProgramHost} from '../src/host';
@ -23,7 +26,6 @@ const LIB_D_TS = {
describe('ngtsc typechecking', () => {
describe('ctors', () => {
it('compiles a basic type constructor', () => {
const ctx = new TypeCheckContext();
const files = [
LIB_D_TS, {
name: 'main.ts',
@ -36,7 +38,15 @@ TestClass.ngTypeCtor({value: 'test'});
`
}
];
const {program, host} = makeProgram(files, undefined, undefined, false);
const {program, host, options} = makeProgram(files, undefined, undefined, false);
const checker = program.getTypeChecker();
const logicalFs = new LogicalFileSystem(getRootDirs(host, options));
const emitter = new ReferenceEmitter([
new LocalIdentifierStrategy(),
new AbsoluteModuleStrategy(program, checker, options, host),
new LogicalProjectStrategy(checker, logicalFs),
]);
const ctx = new TypeCheckContext(emitter);
const TestClass = getDeclaration(program, 'main.ts', 'TestClass', ts.isClassDeclaration);
ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, {
fnName: 'ngTypeCtor',

View File

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

View File

@ -10,6 +10,7 @@ const TS = /\.tsx?$/i;
const D_TS = /\.d\.ts$/i;
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../path';
export function isDtsPath(filePath: string): boolean {
return D_TS.test(filePath);
@ -27,6 +28,17 @@ export function isFromDtsFile(node: ts.Node): boolean {
return sf !== undefined && D_TS.test(sf.fileName);
}
export function nodeNameForError(node: ts.Node & {name?: ts.Node}): string {
if (node.name !== undefined && ts.isIdentifier(node.name)) {
return node.name.text;
} else {
const kind = ts.SyntaxKind[node.kind];
const {line, character} =
ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart());
return `${kind}@${line}:${character}`;
}
}
export function getSourceFile(node: ts.Node): ts.SourceFile {
// In certain transformation contexts, `ts.Node.getSourceFile()` can actually return `undefined`,
// despite the type signature not allowing it. In that event, get the `ts.SourceFile` via the
@ -34,3 +46,37 @@ export function getSourceFile(node: ts.Node): ts.SourceFile {
const directSf = node.getSourceFile() as ts.SourceFile | undefined;
return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile();
}
export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifier|null {
if (decl.name !== undefined && ts.isIdentifier(decl.name)) {
return decl.name;
} else {
return null;
}
}
export function isDeclaration(node: ts.Node): node is ts.Declaration {
return false || ts.isEnumDeclaration(node) || ts.isClassDeclaration(node) ||
ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node);
}
export function isExported(node: ts.Declaration): boolean {
let topLevel: ts.Node = node;
if (ts.isVariableDeclaration(node) && ts.isVariableDeclarationList(node.parent)) {
topLevel = node.parent.parent;
}
return topLevel.modifiers !== undefined &&
topLevel.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
}
export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions): AbsoluteFsPath[] {
const rootDirs: string[] = [];
if (options.rootDirs !== undefined) {
rootDirs.push(...options.rootDirs);
} else if (options.rootDir !== undefined) {
rootDirs.push(options.rootDir);
} else {
rootDirs.push(host.getCurrentDirectory());
}
return rootDirs.map(rootDir => AbsoluteFsPath.fromUnchecked(rootDir));
}

View File

@ -112,6 +112,12 @@ export interface CompilerOptions extends ts.CompilerOptions {
// This will be true be default in Angular 6.
fullTemplateTypeCheck?: boolean;
// Whether to use the CompilerHost's fileNameToModuleName utility (if available) to generate
// import module specifiers. This is false by default, and exists to support running ngtsc
// within Google. This option is internal and is used by the ng_module.bzl rule to switch
// behavior between Bazel and Blaze.
_useHostForImportGeneration?: boolean;
// Insert JSDoc type annotations needed by Closure Compiler
annotateForClosureCompiler?: boolean;