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()), "createExternalSymbolFactoryReexports": (not _is_bazel()),
# FIXME: wrong place to de-dupe # FIXME: wrong place to de-dupe
"expectedOut": depset([o.path for o in expected_outs]).to_list(), "expectedOut": depset([o.path for o in expected_outs]).to_list(),
"_useHostForImportGeneration": (not _is_bazel()),
} }
if _should_produce_flat_module_outs(ctx): 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/entry_point",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/routing", "//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/shims", "//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/cycles",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/translator", "//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 {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations';
import {CycleAnalyzer, ImportGraph} from '../../../ngtsc/cycles'; 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 {PartialEvaluator} from '../../../ngtsc/partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from '../../../ngtsc/path';
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../ngtsc/transform'; import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../ngtsc/transform';
import {DecoratedClass} from '../host/decorated_class'; import {DecoratedClass} from '../host/decorated_class';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
@ -62,9 +63,16 @@ class NgccResourceLoader implements ResourceLoader {
*/ */
export class DecorationAnalyzer { export class DecorationAnalyzer {
resourceManager = new NgccResourceLoader(); resourceManager = new NgccResourceLoader();
resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host); refEmitter = new ReferenceEmitter([
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver); new LocalIdentifierStrategy(),
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver); 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); moduleResolver = new ModuleResolver(this.program, this.options, this.host);
importGraph = new ImportGraph(this.moduleResolver); importGraph = new ImportGraph(this.moduleResolver);
cycleAnalyzer = new CycleAnalyzer(this.importGraph); cycleAnalyzer = new CycleAnalyzer(this.importGraph);
@ -79,7 +87,7 @@ export class DecorationAnalyzer {
new InjectableDecoratorHandler(this.reflectionHost, this.isCore, /* strictCtorDeps */ false), new InjectableDecoratorHandler(this.reflectionHost, this.isCore, /* strictCtorDeps */ false),
new NgModuleDecoratorHandler( new NgModuleDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry, 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), 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 program: ts.Program, private options: ts.CompilerOptions,
private host: ts.CompilerHost, private typeChecker: ts.TypeChecker, private host: ts.CompilerHost, private typeChecker: ts.TypeChecker,
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry, 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. * Analyze a program to find all the decorated files should be transformed.

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ReferencesRegistry} from '../../../ngtsc/annotations'; import {ReferencesRegistry} from '../../../ngtsc/annotations';
import {ResolvedReference} from '../../../ngtsc/imports'; import {Reference} from '../../../ngtsc/imports';
import {Declaration} from '../../../ngtsc/reflection'; import {Declaration} from '../../../ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils'; 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()}`); `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 // Record the usage of the internal module as it needs to become an exported symbol
this.referencesRegistry.add( const reference = new Reference(ngModule.node);
ngModule.node, new ResolvedReference(ngModule.node, fn.ngModule)); reference.addIdentifier(fn.ngModule);
this.referencesRegistry.add(ngModule.node, reference);
ngModule = {node: dtsNgModule, viaModule: null}; ngModule = {node: dtsNgModule, viaModule: null};
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {makeProgram} from '../../../ngtsc/testing/in_memory_typescript'; import {makeProgram} from '../../../ngtsc/testing/in_memory_typescript';
import {BundleProgram} from '../../src/packages/bundle_program'; import {BundleProgram} from '../../src/packages/bundle_program';
import {EntryPointFormat} from '../../src/packages/entry_point'; 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'; export {getDeclaration} from '../../../ngtsc/testing/in_memory_typescript';
/** /**
* *
* @param format The format of the bundle. * @param format The format of the bundle.
@ -27,7 +29,7 @@ export function makeTestEntryPointBundle(
const src = makeTestBundleProgram(files); const src = makeTestBundleProgram(files);
const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null; const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null;
const isFlat = src.r3SymbolsFile === 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 {dirname} from 'canonical-path';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; 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 typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm2015ReflectionHost(false, typeChecker); const host = new Esm2015ReflectionHost(false, typeChecker);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = new DecorationAnalyzer( const decorationAnalyses =
bundle.src.program, bundle.src.options, bundle.src.host, new DecorationAnalyzer(
typeChecker, host, referencesRegistry, [''], false) bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host,
.analyzeProgram(); referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new EsmRenderer(host, false, bundle, dir, dir); const renderer = new EsmRenderer(host, false, bundle, dir, dir);
return { return {

View File

@ -8,6 +8,7 @@
import {dirname} from 'canonical-path'; import {dirname} from 'canonical-path';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; 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 typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm5ReflectionHost(false, typeChecker); const host = new Esm5ReflectionHost(false, typeChecker);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = new DecorationAnalyzer( const decorationAnalyses =
bundle.src.program, bundle.src.options, bundle.src.host, new DecorationAnalyzer(
typeChecker, host, referencesRegistry, [''], false) bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host,
.analyzeProgram(); referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new Esm5Renderer(host, false, bundle, dir, dir); const renderer = new Esm5Renderer(host, false, bundle, dir, dir);
return { return {

View File

@ -12,7 +12,7 @@ import * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles'; import {CycleAnalyzer} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ModuleResolver, ResolvedReference} from '../../imports'; import {ModuleResolver, Reference} from '../../imports';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; 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 // 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. // when this component appears in an `@NgModule` scope, its selector can be determined.
if (metadata.selector !== null) { if (metadata.selector !== null) {
const ref = new ResolvedReference(node, node.name !); const ref = new Reference(node);
this.scopeRegistry.registerDirective(node, { this.scopeRegistry.registerDirective(node, {
ref, ref,
name: node.name !.text, name: node.name !.text,

View File

@ -10,7 +10,7 @@ import {ConstantPool, Expression, ParseError, R3DirectiveMetadata, R3QueryMetada
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Reference, ResolvedReference} from '../../imports'; import {Reference} from '../../imports';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; 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 // 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. // when this directive appears in an `@NgModule` scope, its selector can be determined.
if (analysis && analysis.selector !== null) { if (analysis && analysis.selector !== null) {
let ref = new ResolvedReference(node, node.name !); const ref = new Reference(node);
this.scopeRegistry.registerDirective(node, { this.scopeRegistry.registerDirective(node, {
ref, ref,
directive: ref, directive: ref,

View File

@ -10,11 +10,12 @@ import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, R3Identi
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Reference, ResolvedReference} from '../../imports'; import {Reference, ReferenceEmitter} from '../../imports';
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
import {NgModuleRouteAnalyzer} from '../../routing'; import {NgModuleRouteAnalyzer} from '../../routing';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {getSourceFile} from '../../util/src/typescript';
import {generateSetClassMetadataCall} from './metadata'; import {generateSetClassMetadataCall} from './metadata';
import {ReferencesRegistry} from './references_registry'; import {ReferencesRegistry} from './references_registry';
@ -37,7 +38,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
constructor( constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator, private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private scopeRegistry: SelectorScopeRegistry, private referencesRegistry: ReferencesRegistry, 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; readonly precedence = HandlerPrecedence.PRIMARY;
@ -177,10 +179,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
if (analysis.metadataStmt !== null) { if (analysis.metadataStmt !== null) {
ngModuleStatements.push(analysis.metadataStmt); ngModuleStatements.push(analysis.metadataStmt);
} }
let context = node.getSourceFile(); const context = getSourceFile(node);
if (context === undefined) {
context = ts.getOriginalNode(node).getSourceFile();
}
for (const decl of analysis.declarations) { for (const decl of analysis.declarations) {
if (this.scopeRegistry.requiresRemoteScope(decl.node)) { if (this.scopeRegistry.requiresRemoteScope(decl.node)) {
const scope = this.scopeRegistry.lookupCompilationScopeAsRefs(decl.node); const scope = this.scopeRegistry.lookupCompilationScopeAsRefs(decl.node);
@ -190,11 +189,11 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
const directives: Expression[] = []; const directives: Expression[] = [];
const pipes: Expression[] = []; const pipes: Expression[] = [];
scope.directives.forEach( scope.directives.forEach(
(directive, _) => { directives.push(directive.ref.toExpression(context) !); }); (directive, _) => { directives.push(this.refEmitter.emit(directive.ref, context) !); });
scope.pipes.forEach(pipe => pipes.push(pipe.toExpression(context) !)); scope.pipes.forEach(pipe => pipes.push(this.refEmitter.emit(pipe, context) !));
const directiveArray = new LiteralArrayExpr(directives); const directiveArray = new LiteralArrayExpr(directives);
const pipesArray = new LiteralArrayExpr(pipes); const pipesArray = new LiteralArrayExpr(pipes);
const declExpr = decl.toExpression(context) !; const declExpr = this.refEmitter.emit(decl, context) !;
const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope); const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope);
const callExpr = const callExpr =
new InvokeFunctionExpr(setComponentScope, [declExpr, directiveArray, pipesArray]); new InvokeFunctionExpr(setComponentScope, [declExpr, directiveArray, pipesArray]);
@ -221,15 +220,15 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
private _toR3Reference( private _toR3Reference(
valueRef: Reference<ts.Declaration>, valueContext: ts.SourceFile, valueRef: Reference<ts.Declaration>, valueContext: ts.SourceFile,
typeContext: ts.SourceFile): R3Reference { typeContext: ts.SourceFile): R3Reference {
if (!(valueRef instanceof ResolvedReference)) { if (valueRef.hasOwningModuleGuess) {
return toR3Reference(valueRef, valueRef, valueContext, valueContext); return toR3Reference(valueRef, valueRef, valueContext, valueContext, this.refEmitter);
} else { } else {
let typeRef = valueRef; let typeRef = valueRef;
let typeNode = this.reflector.getDtsDeclaration(typeRef.node); let typeNode = this.reflector.getDtsDeclaration(typeRef.node);
if (typeNode !== null && ts.isClassDeclaration(typeNode)) { 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. // Recurse into nested arrays.
refList.push(...this.resolveTypeList(expr, entry, name)); refList.push(...this.resolveTypeList(expr, entry, name));
} else if (isDeclarationReference(entry)) { } else if (isDeclarationReference(entry)) {
if (!entry.expressable) { if (!this.reflector.isClass(entry.node)) {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `One entry in ${name} is not a type`);
} else if (!this.reflector.isClass(entry.node)) {
throw new FatalDiagnosticError( throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, entry.node, ErrorCode.VALUE_HAS_WRONG_TYPE, entry.node,
`Entry is not a type, but is used as such in ${name} array`); `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 {Expression, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; 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 {ReflectionHost, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectTypeEntityToDeclaration} from '../../reflection';
import {TypeCheckableDirectiveMeta} from '../../typecheck'; import {TypeCheckableDirectiveMeta} from '../../typecheck';
@ -96,7 +96,7 @@ export class SelectorScopeRegistry {
constructor( constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost, private checker: ts.TypeChecker, private reflector: ReflectionHost,
private resolver: ReferenceResolver) {} private refEmitter: ReferenceEmitter) {}
/** /**
* Register a module's metadata with the registry. * Register a module's metadata with the registry.
@ -224,7 +224,7 @@ export class SelectorScopeRegistry {
*/ */
lookupCompilationScope(node: ts.Declaration): CompilationScope<Expression>|null { lookupCompilationScope(node: ts.Declaration): CompilationScope<Expression>|null {
const scope = this.lookupCompilationScopeAsRefs(node); const scope = this.lookupCompilationScopeAsRefs(node);
return scope !== null ? convertScopeToExpressions(scope, node) : null; return scope !== null ? convertScopeToExpressions(scope, node, this.refEmitter) : null;
} }
private lookupScopesOrDie( private lookupScopesOrDie(
@ -273,20 +273,20 @@ export class SelectorScopeRegistry {
// Expand imports to the exported scope of those imports. // Expand imports to the exported scope of those imports.
...flatten(data.imports.map( ...flatten(data.imports.map(
ref => ref =>
this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref), context) this.lookupScopesOrDie(ref.node as ts.Declaration, ref.ownedByModuleGuess, context)
.exported)), .exported)),
// And include the compilation scope of exported modules. // And include the compilation scope of exported modules.
...flatten( ...flatten(
data.exports data.exports
.map( .map(
ref => this.lookupScopes( 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) .filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null)
.map(scope => scope.exported)) .map(scope => scope.exported))
], ],
exported: flatten(data.exports.map(ref => { exported: flatten(data.exports.map(ref => {
const scope = 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) { if (scope !== null) {
return scope.exported; return scope.exported;
} else { } else {
@ -438,11 +438,11 @@ export class SelectorScopeRegistry {
const type = element.exprName; const type = element.exprName;
if (ngModuleImportedFrom !== null) { if (ngModuleImportedFrom !== null) {
const {node, from} = reflectTypeEntityToDeclaration(type, this.checker); const {node, from} = reflectTypeEntityToDeclaration(type, this.checker);
const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
return this.resolver.resolve(node, moduleName, resolutionContext); return new Reference(node, {specifier, resolutionContext});
} else { } else {
const {node} = reflectTypeEntityToDeclaration(type, this.checker); 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[]); }, [] as T[]);
} }
function absoluteModuleName(ref: Reference): string|null {
if (!(ref instanceof AbsoluteReference)) {
return null;
}
return ref.moduleName;
}
function convertDirectiveReferenceList( function convertDirectiveReferenceList(
input: ScopeDirective<Reference>[], context: ts.SourceFile): ScopeDirective<Expression>[] { input: ScopeDirective<Reference>[], context: ts.SourceFile,
refEmitter: ReferenceEmitter): ScopeDirective<Expression>[] {
return input.map(meta => { return input.map(meta => {
const directive = meta.directive.toExpression(context); const directive = refEmitter.emit(meta.directive, context);
if (directive === null) { if (directive === null) {
throw new Error(`Could not write expression to reference ${meta.directive.node}`); throw new Error(`Could not write expression to reference ${meta.directive.node}`);
} }
@ -474,10 +468,11 @@ function convertDirectiveReferenceList(
} }
function convertPipeReferenceMap( 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>(); const newMap = new Map<string, Expression>();
map.forEach((meta, selector) => { map.forEach((meta, selector) => {
const pipe = meta.toExpression(context); const pipe = refEmitter.emit(meta, context);
if (pipe === null) { if (pipe === null) {
throw new Error(`Could not write expression to reference ${meta.node}`); throw new Error(`Could not write expression to reference ${meta.node}`);
} }
@ -487,10 +482,11 @@ function convertPipeReferenceMap(
} }
function convertScopeToExpressions( 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 sourceContext = ts.getOriginalNode(context).getSourceFile();
const directives = convertDirectiveReferenceList(scope.directives, sourceContext); const directives = convertDirectiveReferenceList(scope.directives, sourceContext, refEmitter);
const pipes = convertPipeReferenceMap(scope.pipes, sourceContext); const pipes = convertPipeReferenceMap(scope.pipes, sourceContext, refEmitter);
const declPointer = maybeUnwrapNameOfDeclaration(context); const declPointer = maybeUnwrapNameOfDeclaration(context);
let containsForwardDecls = false; let containsForwardDecls = false;
directives.forEach(meta => { directives.forEach(meta => {

View File

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

View File

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

View File

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

View File

@ -8,9 +8,11 @@
import * as ts from 'typescript'; 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 {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getRootDirs} from '../../util/src/typescript';
import {SelectorScopeRegistry} from '../src/selector_scope'; import {SelectorScopeRegistry} from '../src/selector_scope';
describe('SelectorScopeRegistry', () => { describe('SelectorScopeRegistry', () => {
@ -63,18 +65,19 @@ describe('SelectorScopeRegistry', () => {
expect(ProgramModule).toBeDefined(); expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined(); expect(SomeModule).toBeDefined();
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); const ProgramCmpRef = new Reference(ProgramCmp);
const refEmitter = makeReferenceEmitter(program, checker, options, host);
const resolver = new TsReferenceResolver(program, checker, options, host); const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter);
const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver);
registry.registerModule(ProgramModule, { registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], declarations: [new Reference(ProgramCmp)],
exports: [], 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, { registry.registerDirective(ProgramCmp, {
name: 'ProgramCmp', name: 'ProgramCmp',
ref: ProgramCmpRef, ref: ProgramCmpRef,
@ -136,14 +139,15 @@ describe('SelectorScopeRegistry', () => {
expect(ProgramModule).toBeDefined(); expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined(); expect(SomeModule).toBeDefined();
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); const ProgramCmpRef = new Reference(ProgramCmp);
const refEmitter = makeReferenceEmitter(program, checker, options, host);
const resolver = new TsReferenceResolver(program, checker, options, host); const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter);
const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver);
registry.registerModule(ProgramModule, { registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], declarations: [new Reference(ProgramCmp)],
exports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')], exports: [new Reference(
SomeModule,
{specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})],
imports: [], imports: [],
}); });
@ -166,4 +170,15 @@ describe('SelectorScopeRegistry', () => {
expect(scope.directives).toBeDefined(); expect(scope.directives).toBeDefined();
expect(scope.directives.length).toBe(2); 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 = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/util", "//packages/compiler-cli/src/ngtsc/util",
"@ngdeps//@types/node", "@ngdeps//@types/node",
"@ngdeps//typescript", "@ngdeps//typescript",

View File

@ -7,5 +7,6 @@
*/ */
export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core'; export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core';
export {AbsoluteReference, ImportMode, NodeReference, Reference, ResolvedReference} from './src/references'; export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter} from './src/emitter';
export {ModuleResolver, ReferenceResolver, TsReferenceResolver} from './src/resolver'; 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 * 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'; import * as ts from 'typescript';
import {identifierOfNode} from '../../util/src/typescript';
const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
export enum ImportMode { export enum ImportMode {
UseExistingImport, UseExistingImport,
ForceNewImport, 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 * A `Reference` is a pointer to a `ts.Node` that was extracted from the program somehow. It
* as a `Reference` (assuming references are allowed in evaluation). * 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> { export class Reference<T extends ts.Node = ts.Node> {
constructor(readonly node: T) {}
/** /**
* Whether an `Expression` can be generated which references the node. * The compiler's best guess at an absolute module specifier which owns this `Reference`.
*/
// TODO(issue/24571): remove '!'.
readonly expressable !: boolean;
/**
* Generate an `Expression` representing this type, in the context of the given SourceFile.
* *
* This could be a local variable reference, if the symbol is imported, or it could be a new * This is usually determined by tracking the import statements which led the compiler to a given
* import if needed. * 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[] = []; 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): const id = identifierOfNode(node);
Expression { if (id !== null) {
const localIdentifier = this.identifiers.push(id);
pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode);
if (localIdentifier !== null) {
return new WrappedNodeExpr(localIdentifier);
} else {
return new ExternalExpr(new ExternalReference(this.moduleName, this.symbolName));
} }
} }
addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); } /**
} * The best guess at which module specifier owns this particular reference, or `null` if there
* isn't one.
function pickIdentifier( */
context: ts.SourceFile, primary: ts.Identifier, secondaries: ts.Identifier[], get ownedByModuleGuess(): string|null {
mode: ImportMode): ts.Identifier|null { if (this.bestGuessOwningModule !== null) {
context = ts.getOriginalNode(context) as ts.SourceFile; return this.bestGuessOwningModule.specifier;
} else {
if (ts.getOriginalNode(primary).getSourceFile() === context) { return null;
return primary; }
} else if (mode === ImportMode.UseExistingImport) {
return secondaries.find(id => ts.getOriginalNode(id).getSourceFile() === context) || null;
} 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 * as ts from 'typescript';
import {isFromDtsFile} from '../../util/src/typescript'; import {Reference} from './references';
import {AbsoluteReference, Reference, ResolvedReference} from './references';
export interface ReferenceResolver { export interface ReferenceResolver {
resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string): resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string):
@ -38,101 +36,3 @@ export class ModuleResolver {
return this.program.getSourceFile(resolved.resolvedFileName) || null; 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 * as ts from 'typescript';
import {Reference, ReferenceResolver} from '../../imports'; import {Reference} from '../../imports';
import {ReflectionHost} from '../../reflection'; import {ReflectionHost} from '../../reflection';
import {StaticInterpreter} from './interpreter'; import {StaticInterpreter} from './interpreter';
@ -19,12 +19,10 @@ export type ForeignFunctionResolver =
args: ReadonlyArray<ts.Expression>) => ts.Expression | null; args: ReadonlyArray<ts.Expression>) => ts.Expression | null;
export class PartialEvaluator { export class PartialEvaluator {
constructor( constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
private host: ReflectionHost, private checker: ts.TypeChecker,
private refResolver: ReferenceResolver) {}
evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue { evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue {
const interpreter = new StaticInterpreter(this.host, this.checker, this.refResolver); const interpreter = new StaticInterpreter(this.host, this.checker);
return interpreter.visit(expr, { return interpreter.visit(expr, {
absoluteModuleName: null, absoluteModuleName: null,
resolutionContext: expr.getSourceFile().fileName, resolutionContext: expr.getSourceFile().fileName,

View File

@ -8,7 +8,8 @@
import * as ts from 'typescript'; 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 {Declaration, ReflectionHost} from '../../reflection';
import {ArraySliceBuiltinFn} from './builtin'; import {ArraySliceBuiltinFn} from './builtin';
@ -78,9 +79,7 @@ interface Context {
} }
export class StaticInterpreter { export class StaticInterpreter {
constructor( constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
private host: ReflectionHost, private checker: ts.TypeChecker,
private refResolver: ReferenceResolver) {}
visit(node: ts.Expression, context: Context): ResolvedValue { visit(node: ts.Expression, context: Context): ResolvedValue {
return this.visitExpression(node, context); return this.visitExpression(node, context);
@ -332,10 +331,7 @@ export class StaticInterpreter {
} else if (lhs instanceof Reference) { } else if (lhs instanceof Reference) {
const ref = lhs.node; const ref = lhs.node;
if (this.host.isClass(ref)) { if (this.host.isClass(ref)) {
let absoluteModuleName = context.absoluteModuleName; const module = owningModule(context, lhs.bestGuessOwningModule);
if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) {
absoluteModuleName = lhs.moduleName || absoluteModuleName;
}
let value: ResolvedValue = undefined; let value: ResolvedValue = undefined;
const member = this.host.getMembersOfClass(ref).find( const member = this.host.getMembersOfClass(ref).find(
member => member.isStatic && member.name === strIndex); member => member.isStatic && member.name === strIndex);
@ -343,9 +339,9 @@ export class StaticInterpreter {
if (member.value !== null) { if (member.value !== null) {
value = this.visitExpression(member.value, context); value = this.visitExpression(member.value, context);
} else if (member.implementation !== null) { } else if (member.implementation !== null) {
value = new NodeReference(member.implementation, absoluteModuleName); value = new Reference(member.implementation, module);
} else if (member.node) { } else if (member.node) {
value = new NodeReference(member.node, absoluteModuleName); value = new Reference(member.node, module);
} }
} }
return value; return value;
@ -391,11 +387,10 @@ export class StaticInterpreter {
// If the function is declared in a different file, resolve the foreign function expression // If the function is declared in a different file, resolve the foreign function expression
// using the absolute module name of that file (if any). // using the absolute module name of that file (if any).
if ((lhs instanceof NodeReference || lhs instanceof AbsoluteReference) && if (lhs.bestGuessOwningModule !== null) {
lhs.moduleName !== null) {
context = { context = {
...context, ...context,
absoluteModuleName: lhs.moduleName, absoluteModuleName: lhs.bestGuessOwningModule.specifier,
resolutionContext: node.getSourceFile().fileName, resolutionContext: node.getSourceFile().fileName,
}; };
} }
@ -496,7 +491,7 @@ export class StaticInterpreter {
} }
private getReference(node: ts.Declaration, context: Context): Reference { 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; 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteReference, Reference, TsReferenceResolver} from '../../imports'; import {Reference} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection'; import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {PartialEvaluator} from '../src/interface'; import {PartialEvaluator} from '../src/interface';
@ -44,8 +43,7 @@ function evaluate<T extends ResolvedValue>(
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): T { code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): T {
const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles); const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles);
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const resolver = new TsReferenceResolver(program, checker, options, host); const evaluator = new PartialEvaluator(reflectionHost, checker);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
return evaluator.evaluate(expression) as T; return evaluator.evaluate(expression) as T;
} }
@ -150,22 +148,17 @@ describe('ngtsc metadata', () => {
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
const resolver = new TsReferenceResolver(program, checker, options, host); const evaluator = new PartialEvaluator(reflectionHost, checker);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const resolved = evaluator.evaluate(expr); const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Reference)) { if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference'); return fail('Expected expression to resolve to a reference');
} }
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true); const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !); if (reference === null) {
if (!(reference instanceof WrappedNodeExpr)) { return fail('Expected to get an identifier');
return fail('Expected expression reference to be a wrapped node');
} }
if (!ts.isIdentifier(reference.node)) { expect(reference.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
return fail('Expected expression to be an Identifier');
}
expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
}); });
it('absolute imports work', () => { it('absolute imports work', () => {
@ -183,23 +176,16 @@ describe('ngtsc metadata', () => {
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
const resolver = new TsReferenceResolver(program, checker, options, host); const evaluator = new PartialEvaluator(reflectionHost, checker);
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
const resolved = evaluator.evaluate(expr); const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof AbsoluteReference)) { if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to an absolute 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(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true); const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !); expect(reference).not.toBeNull();
if (!(reference instanceof WrappedNodeExpr)) { expect(reference !.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
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') !);
}); });
it('reads values from default exports', () => { it('reads values from default exports', () => {
@ -282,3 +268,7 @@ describe('ngtsc metadata', () => {
expect(value instanceof Reference).toBe(true); 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 {CycleAnalyzer, ImportGraph} from './cycles';
import {ErrorCode, ngErrorCode} from './diagnostics'; import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {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 {PartialEvaluator} from './partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from './path';
import {TypeScriptReflectionHost} from './reflection'; import {TypeScriptReflectionHost} from './reflection';
import {HostResourceLoader} from './resource_loader'; import {HostResourceLoader} from './resource_loader';
import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing'; import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
@ -27,7 +29,7 @@ import {ivySwitchTransform} from './switch';
import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform'; import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
import {TypeCheckContext, TypeCheckProgramHost} from './typecheck'; import {TypeCheckContext, TypeCheckProgramHost} from './typecheck';
import {normalizeSeparators} from './util/src/path'; 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 { export class NgtscProgram implements api.Program {
private tsProgram: ts.Program; private tsProgram: ts.Program;
@ -40,7 +42,7 @@ export class NgtscProgram implements api.Program {
private _importRewriter: ImportRewriter|undefined = undefined; private _importRewriter: ImportRewriter|undefined = undefined;
private _reflector: TypeScriptReflectionHost|undefined = undefined; private _reflector: TypeScriptReflectionHost|undefined = undefined;
private _isCore: boolean|undefined = undefined; private _isCore: boolean|undefined = undefined;
private rootDirs: string[]; private rootDirs: AbsoluteFsPath[];
private closureCompilerEnabled: boolean; private closureCompilerEnabled: boolean;
private entryPoint: ts.SourceFile|null; private entryPoint: ts.SourceFile|null;
private exportReferenceGraph: ReferenceGraph|null = null; private exportReferenceGraph: ReferenceGraph|null = null;
@ -51,22 +53,20 @@ export class NgtscProgram implements api.Program {
private moduleResolver: ModuleResolver; private moduleResolver: ModuleResolver;
private cycleAnalyzer: CycleAnalyzer; private cycleAnalyzer: CycleAnalyzer;
private refEmitter: ReferenceEmitter|null = null;
private fileToModuleHost: FileToModuleHost|null = null;
constructor( constructor(
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions, rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
host: api.CompilerHost, oldProgram?: api.Program) { host: api.CompilerHost, oldProgram?: api.Program) {
this.rootDirs = []; this.rootDirs = getRootDirs(host, options);
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.closureCompilerEnabled = !!options.annotateForClosureCompiler; this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
this.resourceManager = new HostResourceLoader(host, options); this.resourceManager = new HostResourceLoader(host, options);
const shouldGenerateShims = options.allowEmptyCodegenFiles || false; const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
this.host = host; this.host = host;
if (host.fileNameToModuleName !== undefined) {
this.fileToModuleHost = host as FileToModuleHost;
}
let rootFiles = [...rootNames]; let rootFiles = [...rootNames];
const generators: ShimGenerator[] = []; const generators: ShimGenerator[] = [];
@ -168,7 +168,7 @@ export class NgtscProgram implements api.Program {
const compilation = this.ensureAnalyzed(); const compilation = this.ensureAnalyzed();
const diagnostics = [...compilation.diagnostics]; const diagnostics = [...compilation.diagnostics];
if (!!this.options.fullTemplateTypeCheck) { if (!!this.options.fullTemplateTypeCheck) {
const ctx = new TypeCheckContext(); const ctx = new TypeCheckContext(this.refEmitter !);
compilation.typeCheck(ctx); compilation.typeCheck(ctx);
diagnostics.push(...this.compileTypeCheckProgram(ctx)); diagnostics.push(...this.compileTypeCheckProgram(ctx));
} }
@ -315,9 +315,32 @@ export class NgtscProgram implements api.Program {
private makeCompilation(): IvyCompilation { private makeCompilation(): IvyCompilation {
const checker = this.tsProgram.getTypeChecker(); const checker = this.tsProgram.getTypeChecker();
const refResolver = new TsReferenceResolver(this.tsProgram, checker, this.options, this.host); // Construct the ReferenceEmitter.
const evaluator = new PartialEvaluator(this.reflector, checker, refResolver); if (this.fileToModuleHost === null || !this.options._useHostForImportGeneration) {
const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, refResolver); // 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 // If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in
// order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there // order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there
@ -344,7 +367,7 @@ export class NgtscProgram implements api.Program {
this.reflector, this.isCore, this.options.strictInjectionParameters || false), this.reflector, this.isCore, this.options.strictInjectionParameters || false),
new NgModuleDecoratorHandler( new NgModuleDecoratorHandler(
this.reflector, evaluator, scopeRegistry, referencesRegistry, this.isCore, this.reflector, evaluator, scopeRegistry, referencesRegistry, this.isCore,
this.routeAnalyzer), this.routeAnalyzer, this.refEmitter),
new PipeDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore), new PipeDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore),
]; ];

View File

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

View File

@ -9,7 +9,7 @@
import {R3TargetBinder, SelectorMatcher, TmplAstNode} from '@angular/compiler'; import {R3TargetBinder, SelectorMatcher, TmplAstNode} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NoopImportRewriter} from '../../imports'; import {NoopImportRewriter, ReferenceEmitter} from '../../imports';
import {ImportManager} from '../../translator'; import {ImportManager} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCtorMetadata} from './api'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCtorMetadata} from './api';
@ -26,6 +26,8 @@ import {generateTypeCtor} from './type_constructor';
* checking code. * checking code.
*/ */
export class TypeCheckContext { export class TypeCheckContext {
constructor(private refEmitter: ReferenceEmitter) {}
/** /**
* A `Set` of classes which will be used to generate type constructors. * 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 // Process each operation and use the printer to generate source code for it, inserting it into
// the source code in between the original chunks. // the source code in between the original chunks.
ops.forEach((op, idx) => { 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]; code += text + textParts[idx + 1];
}); });
@ -182,7 +184,8 @@ interface Op {
/** /**
* Execute the operation and return the generated code as text. * 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; } 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):
const tcb = generateTypeCheckBlock(this.node, this.meta, im); string {
const tcb = generateTypeCheckBlock(this.node, this.meta, im, refEmitter);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
} }
} }
@ -213,7 +217,8 @@ class TypeCtorOp implements Op {
*/ */
get splitPoint(): number { return this.node.end - 1; } 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); const tcb = generateTypeCtor(this.node, this.meta);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); 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 {AST, BindingType, BoundTarget, ImplicitReceiver, PropertyRead, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference, ReferenceEmitter} from '../../imports';
import {ImportManager, translateExpression} from '../../translator'; import {ImportManager, translateExpression} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api'; 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. * @param importManager an `ImportManager` for the file into which the TCB will be written.
*/ */
export function generateTypeCheckBlock( export function generateTypeCheckBlock(
node: ts.ClassDeclaration, meta: TypeCheckBlockMetadata, node: ts.ClassDeclaration, meta: TypeCheckBlockMetadata, importManager: ImportManager,
importManager: ImportManager): ts.FunctionDeclaration { refEmitter: ReferenceEmitter): ts.FunctionDeclaration {
const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager); const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager, refEmitter);
const scope = new Scope(tcb); const scope = new Scope(tcb);
tcbProcessNodes(meta.boundTarget.target.template !, tcb, scope); tcbProcessNodes(meta.boundTarget.target.template !, tcb, scope);
@ -59,7 +59,8 @@ class Context {
constructor( constructor(
readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>, 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`. * 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. * This may involve importing the node into the file if it's not declared there already.
*/ */
reference(ref: Reference<ts.Node>): ts.Expression { reference(ref: Reference<ts.Node>): ts.Expression {
const ngExpr = ref.toExpression(this.sourceFile); const ngExpr = this.refEmitter.emit(ref, this.sourceFile);
if (ngExpr === null) { if (ngExpr === null) {
throw new Error(`Unreachable reference: ${ref.node}`); throw new Error(`Unreachable reference: ${ref.node}`);
} }

View File

@ -10,8 +10,11 @@ ts_library(
]), ]),
deps = [ deps = [
"//packages:types", "//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/testing",
"//packages/compiler-cli/src/ngtsc/typecheck", "//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",
"@ngdeps//typescript", "@ngdeps//typescript",
], ],
) )

View File

@ -8,7 +8,10 @@
import * as ts from 'typescript'; 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 {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getRootDirs} from '../../util/src/typescript';
import {TypeCheckContext} from '../src/context'; import {TypeCheckContext} from '../src/context';
import {TypeCheckProgramHost} from '../src/host'; import {TypeCheckProgramHost} from '../src/host';
@ -23,7 +26,6 @@ const LIB_D_TS = {
describe('ngtsc typechecking', () => { describe('ngtsc typechecking', () => {
describe('ctors', () => { describe('ctors', () => {
it('compiles a basic type constructor', () => { it('compiles a basic type constructor', () => {
const ctx = new TypeCheckContext();
const files = [ const files = [
LIB_D_TS, { LIB_D_TS, {
name: 'main.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); const TestClass = getDeclaration(program, 'main.ts', 'TestClass', ts.isClassDeclaration);
ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, { ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, {
fnName: 'ngTypeCtor', fnName: 'ngTypeCtor',

View File

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

View File

@ -10,6 +10,7 @@ const TS = /\.tsx?$/i;
const D_TS = /\.d\.ts$/i; const D_TS = /\.d\.ts$/i;
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../path';
export function isDtsPath(filePath: string): boolean { export function isDtsPath(filePath: string): boolean {
return D_TS.test(filePath); return D_TS.test(filePath);
@ -27,6 +28,17 @@ export function isFromDtsFile(node: ts.Node): boolean {
return sf !== undefined && D_TS.test(sf.fileName); 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 { export function getSourceFile(node: ts.Node): ts.SourceFile {
// In certain transformation contexts, `ts.Node.getSourceFile()` can actually return `undefined`, // 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 // 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; const directSf = node.getSourceFile() as ts.SourceFile | undefined;
return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile(); 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. // This will be true be default in Angular 6.
fullTemplateTypeCheck?: boolean; 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 // Insert JSDoc type annotations needed by Closure Compiler
annotateForClosureCompiler?: boolean; annotateForClosureCompiler?: boolean;