diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 54abf74c97..e6289e8640 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -254,6 +254,7 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs): "createExternalSymbolFactoryReexports": (not _is_bazel()), # FIXME: wrong place to de-dupe "expectedOut": depset([o.path for o in expected_outs]).to_list(), + "_useHostForImportGeneration": (not _is_bazel()), } if _should_produce_flat_module_outs(ctx): diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index 4e4688a6f4..dcd274b1e1 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -29,6 +29,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/entry_point", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/routing", "//packages/compiler-cli/src/ngtsc/shims", diff --git a/packages/compiler-cli/src/ngcc/BUILD.bazel b/packages/compiler-cli/src/ngcc/BUILD.bazel index bcc5f8f01f..1915c0e0fd 100644 --- a/packages/compiler-cli/src/ngcc/BUILD.bazel +++ b/packages/compiler-cli/src/ngcc/BUILD.bazel @@ -15,6 +15,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/translator", diff --git a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts index 2224c3723a..a5dc35592b 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts @@ -12,8 +12,9 @@ import * as ts from 'typescript'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations'; import {CycleAnalyzer, ImportGraph} from '../../../ngtsc/cycles'; -import {ModuleResolver, TsReferenceResolver} from '../../../ngtsc/imports'; +import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, ReferenceEmitter} from '../../../ngtsc/imports'; import {PartialEvaluator} from '../../../ngtsc/partial_evaluator'; +import {AbsoluteFsPath, LogicalFileSystem} from '../../../ngtsc/path'; import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../ngtsc/transform'; import {DecoratedClass} from '../host/decorated_class'; import {NgccReflectionHost} from '../host/ngcc_host'; @@ -62,9 +63,16 @@ class NgccResourceLoader implements ResourceLoader { */ export class DecorationAnalyzer { resourceManager = new NgccResourceLoader(); - resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host); - scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver); - evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver); + refEmitter = new ReferenceEmitter([ + new LocalIdentifierStrategy(), + new AbsoluteModuleStrategy(this.program, this.typeChecker, this.options, this.host), + // TODO(alxhub): there's no reason why ngcc needs the "logical file system" logic here, as ngcc + // projects only ever have one rootDir. Instead, ngcc should just switch its emitted imort based + // on whether a bestGuessOwningModule is present in the Reference. + new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)), + ]); + scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.refEmitter); + evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker); moduleResolver = new ModuleResolver(this.program, this.options, this.host); importGraph = new ImportGraph(this.moduleResolver); cycleAnalyzer = new CycleAnalyzer(this.importGraph); @@ -79,7 +87,7 @@ export class DecorationAnalyzer { new InjectableDecoratorHandler(this.reflectionHost, this.isCore, /* strictCtorDeps */ false), new NgModuleDecoratorHandler( this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry, - this.isCore, /* routeAnalyzer */ null), + this.isCore, /* routeAnalyzer */ null, this.refEmitter), new PipeDecoratorHandler(this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore), ]; @@ -87,7 +95,7 @@ export class DecorationAnalyzer { private program: ts.Program, private options: ts.CompilerOptions, private host: ts.CompilerHost, private typeChecker: ts.TypeChecker, private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry, - private rootDirs: string[], private isCore: boolean) {} + private rootDirs: AbsoluteFsPath[], private isCore: boolean) {} /** * Analyze a program to find all the decorated files should be transformed. diff --git a/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts index 5456f19543..6155c19890 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; import {ReferencesRegistry} from '../../../ngtsc/annotations'; -import {ResolvedReference} from '../../../ngtsc/imports'; +import {Reference} from '../../../ngtsc/imports'; import {Declaration} from '../../../ngtsc/reflection'; import {NgccReflectionHost} from '../host/ngcc_host'; import {isDefined} from '../utils'; @@ -62,8 +62,9 @@ export class ModuleWithProvidersAnalyzer { `The referenced NgModule in ${fn.declaration.getText()} is not a class declaration in the typings program; instead we get ${dtsNgModule.getText()}`); } // Record the usage of the internal module as it needs to become an exported symbol - this.referencesRegistry.add( - ngModule.node, new ResolvedReference(ngModule.node, fn.ngModule)); + const reference = new Reference(ngModule.node); + reference.addIdentifier(fn.ngModule); + this.referencesRegistry.add(ngModule.node, reference); ngModule = {node: dtsNgModule, viaModule: null}; } diff --git a/packages/compiler-cli/src/ngcc/src/analysis/ngcc_references_registry.ts b/packages/compiler-cli/src/ngcc/src/analysis/ngcc_references_registry.ts index c0000c1ac2..08a7f86e24 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/ngcc_references_registry.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/ngcc_references_registry.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; import {ReferencesRegistry} from '../../../ngtsc/annotations'; -import {Reference, ResolvedReference} from '../../../ngtsc/imports'; +import {Reference} from '../../../ngtsc/imports'; import {Declaration, ReflectionHost} from '../../../ngtsc/reflection'; import {hasNameIdentifier} from '../utils'; @@ -31,8 +31,8 @@ export class NgccReferencesRegistry implements ReferencesRegistry { */ add(source: ts.Declaration, ...references: Reference[]): void { references.forEach(ref => { - // Only store resolved references. We are not interested in literals. - if (ref instanceof ResolvedReference && hasNameIdentifier(ref.node)) { + // Only store relative references. We are not interested in literals. + if (ref.bestGuessOwningModule === null && hasNameIdentifier(ref.node)) { const declaration = this.host.getDeclarationOfIdentifier(ref.node.name); if (declaration && hasNameIdentifier(declaration.node)) { this.map.set(declaration.node.name, declaration); diff --git a/packages/compiler-cli/src/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/src/ngcc/src/packages/entry_point_bundle.ts index 3dbbaee262..d04da8fe7e 100644 --- a/packages/compiler-cli/src/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/src/ngcc/src/packages/entry_point_bundle.ts @@ -7,10 +7,13 @@ */ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; + import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPoint, EntryPointFormat} from './entry_point'; + /** * A bundle of files and paths (and TS programs) that correspond to a particular * format of a package entry-point. @@ -18,7 +21,7 @@ import {EntryPoint, EntryPointFormat} from './entry_point'; export interface EntryPointBundle { format: EntryPointFormat; isFlat: boolean; - rootDirs: string[]; + rootDirs: AbsoluteFsPath[]; src: BundleProgram; dts: BundleProgram|null; } @@ -45,7 +48,7 @@ export function makeEntryPointBundle( rootDir: entryPoint.path, }; const host = ts.createCompilerHost(options); - const rootDirs = [entryPoint.path]; + const rootDirs = [AbsoluteFsPath.from(entryPoint.path)]; // Create the bundle programs, as necessary. const src = makeBundleProgram(isCore, path, 'r3_symbols.js', options, host); diff --git a/packages/compiler-cli/src/ngcc/test/BUILD.bazel b/packages/compiler-cli/src/ngcc/test/BUILD.bazel index 72ccc7d222..f1b6941b95 100644 --- a/packages/compiler-cli/src/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngcc/test/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( "//packages/compiler-cli/src/ngcc", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/transform", diff --git a/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts index 889b8dbbec..5798fae8b9 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; import {Decorator} from '../../../ngtsc/reflection'; import {DecoratorHandler, DetectResult} from '../../../ngtsc/transform'; import {DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; @@ -99,7 +100,7 @@ describe('DecorationAnalyzer', () => { const referencesRegistry = new NgccReferencesRegistry(reflectionHost); const analyzer = new DecorationAnalyzer( program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, - [''], false); + [AbsoluteFsPath.fromUnchecked('/')], false); testHandler = createTestHandler(); analyzer.handlers = [testHandler]; result = analyzer.analyzeProgram(); @@ -143,7 +144,7 @@ describe('DecorationAnalyzer', () => { const referencesRegistry = new NgccReferencesRegistry(reflectionHost); const analyzer = new DecorationAnalyzer( program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, - [''], false); + [AbsoluteFsPath.fromUnchecked('/')], false); const testHandler = createTestHandler(); analyzer.handlers = [testHandler]; const result = analyzer.analyzeProgram(); @@ -161,7 +162,7 @@ describe('DecorationAnalyzer', () => { const referencesRegistry = new NgccReferencesRegistry(reflectionHost); const analyzer = new DecorationAnalyzer( program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, - [''], false); + [AbsoluteFsPath.fromUnchecked('/')], false); const testHandler = createTestHandler(); analyzer.handlers = [testHandler]; const result = analyzer.analyzeProgram(); diff --git a/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts index 4adbf8bbaa..ed74e29212 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ResolvedReference} from '../../../ngtsc/imports'; +import {Reference} from '../../../ngtsc/imports'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; @@ -136,19 +136,13 @@ describe('PrivateDeclarationsAnalyzer', () => { // decoration handlers in the `DecorationAnalyzer`. const publicComponentDeclaration = getDeclaration(program, '/src/a.js', 'PublicComponent', ts.isClassDeclaration); - referencesRegistry.add( - null !, - new ResolvedReference(publicComponentDeclaration, publicComponentDeclaration.name !)); + referencesRegistry.add(null !, new Reference(publicComponentDeclaration)); const privateComponentDeclaration = getDeclaration(program, '/src/b.js', 'PrivateComponent', ts.isClassDeclaration); - referencesRegistry.add( - null !, new ResolvedReference( - privateComponentDeclaration, privateComponentDeclaration.name !)); + referencesRegistry.add(null !, new Reference(privateComponentDeclaration)); const internalComponentDeclaration = getDeclaration(program, '/src/c.js', 'InternalComponent', ts.isClassDeclaration); - referencesRegistry.add( - null !, new ResolvedReference( - internalComponentDeclaration, internalComponentDeclaration.name !)); + referencesRegistry.add(null !, new Reference(internalComponentDeclaration)); const analyses = analyzer.analyzeProgram(program); expect(analyses.length).toEqual(2); diff --git a/packages/compiler-cli/src/ngcc/test/analysis/references_registry_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/references_registry_spec.ts index 230ed7f8c5..5f75dae924 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/references_registry_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/references_registry_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {Reference, TsReferenceResolver} from '../../../ngtsc/imports'; +import {Reference} from '../../../ngtsc/imports'; import {PartialEvaluator} from '../../../ngtsc/partial_evaluator'; import {TypeScriptReflectionHost} from '../../../ngtsc/reflection'; import {getDeclaration, makeProgram} from '../../../ngtsc/testing/in_memory_typescript'; @@ -39,11 +39,11 @@ describe('NgccReferencesRegistry', () => { const testArrayExpression = testArrayDeclaration.initializer !; const reflectionHost = new TypeScriptReflectionHost(checker); - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); const registry = new NgccReferencesRegistry(reflectionHost); - const references = evaluator.evaluate(testArrayExpression) as Reference[]; + const references = (evaluator.evaluate(testArrayExpression) as any[]) + .filter(ref => ref instanceof Reference) as Reference[]; registry.add(null !, ...references); const map = registry.getDeclarationMap(); diff --git a/packages/compiler-cli/src/ngcc/test/helpers/utils.ts b/packages/compiler-cli/src/ngcc/test/helpers/utils.ts index 68e3f5c9b0..917ac21422 100644 --- a/packages/compiler-cli/src/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/src/ngcc/test/helpers/utils.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; import {makeProgram} from '../../../ngtsc/testing/in_memory_typescript'; import {BundleProgram} from '../../src/packages/bundle_program'; import {EntryPointFormat} from '../../src/packages/entry_point'; @@ -15,6 +16,7 @@ import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; export {getDeclaration} from '../../../ngtsc/testing/in_memory_typescript'; + /** * * @param format The format of the bundle. @@ -27,7 +29,7 @@ export function makeTestEntryPointBundle( const src = makeTestBundleProgram(files); const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null; const isFlat = src.r3SymbolsFile === null; - return {format, rootDirs: ['/'], src, dts, isFlat}; + return {format, rootDirs: [AbsoluteFsPath.fromUnchecked('/')], src, dts, isFlat}; } /** diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts index bce94403d7..a45ad7e28f 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -8,6 +8,7 @@ import {dirname} from 'canonical-path'; import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; @@ -21,10 +22,11 @@ function setup(file: {name: string, contents: string}) { const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const decorationAnalyses = new DecorationAnalyzer( - bundle.src.program, bundle.src.options, bundle.src.host, - typeChecker, host, referencesRegistry, [''], false) - .analyzeProgram(); + const decorationAnalyses = + new DecorationAnalyzer( + bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new EsmRenderer(host, false, bundle, dir, dir); return { diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts index ccc4299f3d..fe8bf2aaa3 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts @@ -8,6 +8,7 @@ import {dirname} from 'canonical-path'; import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; @@ -21,10 +22,11 @@ function setup(file: {name: string, contents: string}) { const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm5ReflectionHost(false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const decorationAnalyses = new DecorationAnalyzer( - bundle.src.program, bundle.src.options, bundle.src.host, - typeChecker, host, referencesRegistry, [''], false) - .analyzeProgram(); + const decorationAnalyses = + new DecorationAnalyzer( + bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new Esm5Renderer(host, false, bundle, dir, dir); return { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 019c87a685..20650763d4 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -12,7 +12,7 @@ import * as ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {ModuleResolver, ResolvedReference} from '../../imports'; +import {ModuleResolver, Reference} from '../../imports'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; @@ -225,7 +225,7 @@ export class ComponentDecoratorHandler implements // If the component has a selector, it should be registered with the `SelectorScopeRegistry` so // when this component appears in an `@NgModule` scope, its selector can be determined. if (metadata.selector !== null) { - const ref = new ResolvedReference(node, node.name !); + const ref = new Reference(node); this.scopeRegistry.registerDirective(node, { ref, name: node.name !.text, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 6a7ddfc0c2..f68244a849 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -10,7 +10,7 @@ import {ConstantPool, Expression, ParseError, R3DirectiveMetadata, R3QueryMetada import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {Reference, ResolvedReference} from '../../imports'; +import {Reference} from '../../imports'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; @@ -57,7 +57,7 @@ export class DirectiveDecoratorHandler implements // If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so // when this directive appears in an `@NgModule` scope, its selector can be determined. if (analysis && analysis.selector !== null) { - let ref = new ResolvedReference(node, node.name !); + const ref = new Reference(node); this.scopeRegistry.registerDirective(node, { ref, directive: ref, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 9b5cc2d361..33ae5ee7f3 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -10,11 +10,12 @@ import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, R3Identi import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {Reference, ResolvedReference} from '../../imports'; +import {Reference, ReferenceEmitter} from '../../imports'; import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {NgModuleRouteAnalyzer} from '../../routing'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; +import {getSourceFile} from '../../util/src/typescript'; import {generateSetClassMetadataCall} from './metadata'; import {ReferencesRegistry} from './references_registry'; @@ -37,7 +38,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { directives.push(directive.ref.toExpression(context) !); }); - scope.pipes.forEach(pipe => pipes.push(pipe.toExpression(context) !)); + (directive, _) => { directives.push(this.refEmitter.emit(directive.ref, context) !); }); + scope.pipes.forEach(pipe => pipes.push(this.refEmitter.emit(pipe, context) !)); const directiveArray = new LiteralArrayExpr(directives); const pipesArray = new LiteralArrayExpr(pipes); - const declExpr = decl.toExpression(context) !; + const declExpr = this.refEmitter.emit(decl, context) !; const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope); const callExpr = new InvokeFunctionExpr(setComponentScope, [declExpr, directiveArray, pipesArray]); @@ -221,15 +220,15 @@ export class NgModuleDecoratorHandler implements DecoratorHandler, valueContext: ts.SourceFile, typeContext: ts.SourceFile): R3Reference { - if (!(valueRef instanceof ResolvedReference)) { - return toR3Reference(valueRef, valueRef, valueContext, valueContext); + if (valueRef.hasOwningModuleGuess) { + return toR3Reference(valueRef, valueRef, valueContext, valueContext, this.refEmitter); } else { let typeRef = valueRef; let typeNode = this.reflector.getDtsDeclaration(typeRef.node); if (typeNode !== null && ts.isClassDeclaration(typeNode)) { - typeRef = new ResolvedReference(typeNode, typeNode.name !); + typeRef = new Reference(typeNode); } - return toR3Reference(valueRef, typeRef, valueContext, typeContext); + return toR3Reference(valueRef, typeRef, valueContext, typeContext, this.refEmitter); } } @@ -328,10 +327,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler|null { const scope = this.lookupCompilationScopeAsRefs(node); - return scope !== null ? convertScopeToExpressions(scope, node) : null; + return scope !== null ? convertScopeToExpressions(scope, node, this.refEmitter) : null; } private lookupScopesOrDie( @@ -273,20 +273,20 @@ export class SelectorScopeRegistry { // Expand imports to the exported scope of those imports. ...flatten(data.imports.map( ref => - this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref), context) + this.lookupScopesOrDie(ref.node as ts.Declaration, ref.ownedByModuleGuess, context) .exported)), // And include the compilation scope of exported modules. ...flatten( data.exports .map( ref => this.lookupScopes( - ref.node as ts.Declaration, absoluteModuleName(ref), context)) + ref.node as ts.Declaration, ref.ownedByModuleGuess, context)) .filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null) .map(scope => scope.exported)) ], exported: flatten(data.exports.map(ref => { const scope = - this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref), context); + this.lookupScopes(ref.node as ts.Declaration, ref.ownedByModuleGuess, context); if (scope !== null) { return scope.exported; } else { @@ -438,11 +438,11 @@ export class SelectorScopeRegistry { const type = element.exprName; if (ngModuleImportedFrom !== null) { const {node, from} = reflectTypeEntityToDeclaration(type, this.checker); - const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); - return this.resolver.resolve(node, moduleName, resolutionContext); + const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); + return new Reference(node, {specifier, resolutionContext}); } else { const {node} = reflectTypeEntityToDeclaration(type, this.checker); - return this.resolver.resolve(node, null, resolutionContext); + return new Reference(node); } }); } @@ -455,17 +455,11 @@ function flatten(array: T[][]): T[] { }, [] as T[]); } -function absoluteModuleName(ref: Reference): string|null { - if (!(ref instanceof AbsoluteReference)) { - return null; - } - return ref.moduleName; -} - function convertDirectiveReferenceList( - input: ScopeDirective[], context: ts.SourceFile): ScopeDirective[] { + input: ScopeDirective[], context: ts.SourceFile, + refEmitter: ReferenceEmitter): ScopeDirective[] { return input.map(meta => { - const directive = meta.directive.toExpression(context); + const directive = refEmitter.emit(meta.directive, context); if (directive === null) { throw new Error(`Could not write expression to reference ${meta.directive.node}`); } @@ -474,10 +468,11 @@ function convertDirectiveReferenceList( } function convertPipeReferenceMap( - map: Map, context: ts.SourceFile): Map { + map: Map, context: ts.SourceFile, + refEmitter: ReferenceEmitter): Map { const newMap = new Map(); map.forEach((meta, selector) => { - const pipe = meta.toExpression(context); + const pipe = refEmitter.emit(meta, context); if (pipe === null) { throw new Error(`Could not write expression to reference ${meta.node}`); } @@ -487,10 +482,11 @@ function convertPipeReferenceMap( } function convertScopeToExpressions( - scope: CompilationScope, context: ts.Declaration): CompilationScope { + scope: CompilationScope, context: ts.Declaration, + refEmitter: ReferenceEmitter): CompilationScope { const sourceContext = ts.getOriginalNode(context).getSourceFile(); - const directives = convertDirectiveReferenceList(scope.directives, sourceContext); - const pipes = convertPipeReferenceMap(scope.pipes, sourceContext); + const directives = convertDirectiveReferenceList(scope.directives, sourceContext, refEmitter); + const pipes = convertPipeReferenceMap(scope.pipes, sourceContext, refEmitter); const declPointer = maybeUnwrapNameOfDeclaration(context); let containsForwardDecls = false; directives.forEach(meta => { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 947b6d332b..1bd49291ce 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -10,7 +10,7 @@ import {R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNode import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {AbsoluteReference, ImportMode, Reference} from '../../imports'; +import {ImportMode, Reference, ReferenceEmitter} from '../../imports'; import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost} from '../../reflection'; export enum ConstructorDepErrorKind { @@ -119,9 +119,9 @@ export function validateConstructorDependencies( export function toR3Reference( valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile, - typeContext: ts.SourceFile): R3Reference { - const value = valueRef.toExpression(valueContext, ImportMode.UseExistingImport); - const type = typeRef.toExpression(typeContext, ImportMode.ForceNewImport); + typeContext: ts.SourceFile, refEmitter: ReferenceEmitter): R3Reference { + const value = refEmitter.emit(valueRef, valueContext, ImportMode.UseExistingImport); + const type = refEmitter.emit(typeRef, typeContext, ImportMode.ForceNewImport); if (value === null || type === null) { throw new Error(`Could not refer to ${ts.SyntaxKind[valueRef.node.kind]}`); } @@ -133,8 +133,7 @@ export function isAngularCore(decorator: Decorator): boolean { } export function isAngularCoreReference(reference: Reference, symbolName: string) { - return reference instanceof AbsoluteReference && reference.moduleName === '@angular/core' && - reference.symbolName === symbolName; + return reference.ownedByModuleGuess === '@angular/core' && reference.debugName === symbolName; } /** @@ -209,8 +208,7 @@ export function unwrapForwardRef(node: ts.Expression, reflector: ReflectionHost) export function forwardRefResolver( ref: Reference, args: ts.Expression[]): ts.Expression|null { - if (!(ref instanceof AbsoluteReference) || ref.moduleName !== '@angular/core' || - ref.symbolName !== 'forwardRef' || args.length !== 1) { + if (!isAngularCoreReference(ref, 'forwardRef') || args.length !== 1) { return null; } return expandForwardRef(args[0]); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel index 0379b2da14..b179d74b6b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel @@ -16,9 +16,11 @@ ts_library( "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/translator", + "//packages/compiler-cli/src/ngtsc/util", "@ngdeps//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index 08abb0b890..f8a65e2a4b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {CycleAnalyzer, ImportGraph} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {ModuleResolver, TsReferenceResolver} from '../../imports'; +import {ModuleResolver, ReferenceEmitter} from '../../imports'; import {PartialEvaluator} from '../../partial_evaluator'; import {TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; @@ -44,15 +44,15 @@ describe('ComponentDecoratorHandler', () => { ]); const checker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(checker); - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); const moduleResolver = new ModuleResolver(program, options, host); const importGraph = new ImportGraph(moduleResolver); const cycleAnalyzer = new CycleAnalyzer(importGraph); const handler = new ComponentDecoratorHandler( - reflectionHost, evaluator, new SelectorScopeRegistry(checker, reflectionHost, resolver), - false, new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer); + reflectionHost, evaluator, + new SelectorScopeRegistry(checker, reflectionHost, new ReferenceEmitter([])), false, + new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer); const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration); const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); if (detected === undefined) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts index 56785a28c8..92f231950f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts @@ -8,9 +8,11 @@ import * as ts from 'typescript'; -import {AbsoluteReference, ResolvedReference, TsReferenceResolver} from '../../imports'; +import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports'; +import {LogicalFileSystem} from '../../path'; import {TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getRootDirs} from '../../util/src/typescript'; import {SelectorScopeRegistry} from '../src/selector_scope'; describe('SelectorScopeRegistry', () => { @@ -63,18 +65,19 @@ describe('SelectorScopeRegistry', () => { expect(ProgramModule).toBeDefined(); expect(SomeModule).toBeDefined(); - const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); - - const resolver = new TsReferenceResolver(program, checker, options, host); - const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver); + const ProgramCmpRef = new Reference(ProgramCmp); + const refEmitter = makeReferenceEmitter(program, checker, options, host); + const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter); registry.registerModule(ProgramModule, { - declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], + declarations: [new Reference(ProgramCmp)], exports: [], - imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')], + imports: [new Reference( + SomeModule, + {specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})], }); - const ref = new ResolvedReference(ProgramCmp, ProgramCmp.name !); + const ref = new Reference(ProgramCmp); registry.registerDirective(ProgramCmp, { name: 'ProgramCmp', ref: ProgramCmpRef, @@ -136,14 +139,15 @@ describe('SelectorScopeRegistry', () => { expect(ProgramModule).toBeDefined(); expect(SomeModule).toBeDefined(); - const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); - - const resolver = new TsReferenceResolver(program, checker, options, host); - const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver); + const ProgramCmpRef = new Reference(ProgramCmp); + const refEmitter = makeReferenceEmitter(program, checker, options, host); + const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter); registry.registerModule(ProgramModule, { - declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], - exports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')], + declarations: [new Reference(ProgramCmp)], + exports: [new Reference( + SomeModule, + {specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})], imports: [], }); @@ -166,4 +170,15 @@ describe('SelectorScopeRegistry', () => { expect(scope.directives).toBeDefined(); expect(scope.directives.length).toBe(2); }); -}); \ No newline at end of file +}); + +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)), + ]); +} diff --git a/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel b/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel index 7765082b2b..6f8cb487a9 100644 --- a/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/util", "@ngdeps//@types/node", "@ngdeps//typescript", diff --git a/packages/compiler-cli/src/ngtsc/imports/index.ts b/packages/compiler-cli/src/ngtsc/imports/index.ts index 386775da9d..50af9d405d 100644 --- a/packages/compiler-cli/src/ngtsc/imports/index.ts +++ b/packages/compiler-cli/src/ngtsc/imports/index.ts @@ -7,5 +7,6 @@ */ export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core'; -export {AbsoluteReference, ImportMode, NodeReference, Reference, ResolvedReference} from './src/references'; -export {ModuleResolver, ReferenceResolver, TsReferenceResolver} from './src/resolver'; +export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter} from './src/emitter'; +export {ImportMode, OwningModule, Reference} from './src/references'; +export {ModuleResolver} from './src/resolver'; diff --git a/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts b/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts new file mode 100644 index 0000000000..c52d5a4d39 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts @@ -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, 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|null>(); + + constructor( + private program: ts.Program, private checker: ts.TypeChecker, + private options: ts.CompilerOptions, private host: ts.CompilerHost) {} + + emit(ref: Reference, 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|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|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(); + + 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, 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, 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}); + } +} diff --git a/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts b/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts new file mode 100644 index 0000000000..71a4d45d35 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts @@ -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; + }); +} diff --git a/packages/compiler-cli/src/ngtsc/imports/src/references.ts b/packages/compiler-cli/src/ngtsc/imports/src/references.ts index 2be1cc4a03..348f77fb32 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/references.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/references.ts @@ -6,145 +6,98 @@ * found in the LICENSE file at https://angular.io/license */ -/// - -import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler'; -import * as path from 'path'; import * as ts from 'typescript'; - -const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/; +import {identifierOfNode} from '../../util/src/typescript'; export enum ImportMode { UseExistingImport, ForceNewImport, } +export interface OwningModule { + specifier: string; + resolutionContext: string; +} + /** - * A reference to a `ts.Node`. + * A `ts.Node` plus the context in which it was discovered. * - * For example, if an expression evaluates to a function or class definition, it will be returned - * as a `Reference` (assuming references are allowed in evaluation). + * A `Reference` is a pointer to a `ts.Node` that was extracted from the program somehow. It + * contains not only the node itself, but the information regarding how the node was located. In + * particular, it might track different identifiers by which the node is exposed, as well as + * potentially a module specifier which might expose the node. + * + * The Angular compiler uses `Reference`s instead of `ts.Node`s when tracking classes or generating + * imports. */ -export abstract class Reference { - constructor(readonly node: T) {} - +export class Reference { /** - * Whether an `Expression` can be generated which references the node. - */ - // TODO(issue/24571): remove '!'. - readonly expressable !: boolean; - - /** - * Generate an `Expression` representing this type, in the context of the given SourceFile. + * The compiler's best guess at an absolute module specifier which owns this `Reference`. * - * This could be a local variable reference, if the symbol is imported, or it could be a new - * import if needed. + * This is usually determined by tracking the import statements which led the compiler to a given + * node. If any of these imports are absolute, it's an indication that the node being imported + * might come from that module. + * + * It is not _guaranteed_ that the node in question is exported from its `bestGuessOwningModule` - + * that is mostly a convention that applies in certain package formats. + * + * If `bestGuessOwningModule` is `null`, then it's likely the node came from the current program. */ - abstract toExpression(context: ts.SourceFile, importMode?: ImportMode): Expression|null; + readonly bestGuessOwningModule: OwningModule|null; - abstract addIdentifier(identifier: ts.Identifier): void; -} - -/** - * A reference to a node only, without any ability to get an `Expression` representing that node. - * - * This is used for returning references to things like method declarations, which are not directly - * referenceable. - */ -export class NodeReference extends Reference { - 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 extends Reference { - 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 extends Reference { private identifiers: ts.Identifier[] = []; - constructor( - node: T, private primaryIdentifier: ts.Identifier, readonly moduleName: string, - readonly symbolName: string) { - super(node); - } - readonly expressable = true; + constructor(readonly node: T, bestGuessOwningModule: OwningModule|null = null) { + this.bestGuessOwningModule = bestGuessOwningModule; - toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.UseExistingImport): - Expression { - const localIdentifier = - pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode); - if (localIdentifier !== null) { - return new WrappedNodeExpr(localIdentifier); - } else { - return new ExternalExpr(new ExternalReference(this.moduleName, this.symbolName)); + const id = identifierOfNode(node); + if (id !== null) { + this.identifiers.push(id); } } - addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); } -} - -function pickIdentifier( - context: ts.SourceFile, primary: ts.Identifier, secondaries: ts.Identifier[], - mode: ImportMode): ts.Identifier|null { - context = ts.getOriginalNode(context) as ts.SourceFile; - - if (ts.getOriginalNode(primary).getSourceFile() === context) { - return primary; - } else if (mode === ImportMode.UseExistingImport) { - return secondaries.find(id => ts.getOriginalNode(id).getSourceFile() === context) || null; - } else { - return null; + /** + * The best guess at which module specifier owns this particular reference, or `null` if there + * isn't one. + */ + get ownedByModuleGuess(): string|null { + if (this.bestGuessOwningModule !== null) { + return this.bestGuessOwningModule.specifier; + } else { + return null; + } } -} \ No newline at end of file + + /** + * 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; + } +} diff --git a/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts b/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts index fbca9427ec..6fd244555a 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts @@ -8,9 +8,7 @@ import * as ts from 'typescript'; -import {isFromDtsFile} from '../../util/src/typescript'; - -import {AbsoluteReference, Reference, ResolvedReference} from './references'; +import {Reference} from './references'; export interface ReferenceResolver { resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string): @@ -38,101 +36,3 @@ export class ModuleResolver { return this.program.getSourceFile(resolved.resolvedFileName) || null; } } - -export class TsReferenceResolver implements ReferenceResolver { - private moduleExportsCache = new Map|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 { - 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|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|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(); - - 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; - } -} diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts index 3f88ab1fd6..28617d1ca1 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {Reference, ReferenceResolver} from '../../imports'; +import {Reference} from '../../imports'; import {ReflectionHost} from '../../reflection'; import {StaticInterpreter} from './interpreter'; @@ -19,12 +19,10 @@ export type ForeignFunctionResolver = args: ReadonlyArray) => ts.Expression | null; export class PartialEvaluator { - constructor( - private host: ReflectionHost, private checker: ts.TypeChecker, - private refResolver: ReferenceResolver) {} + constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {} evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue { - const interpreter = new StaticInterpreter(this.host, this.checker, this.refResolver); + const interpreter = new StaticInterpreter(this.host, this.checker); return interpreter.visit(expr, { absoluteModuleName: null, resolutionContext: expr.getSourceFile().fileName, diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index b172def528..8a3f662780 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -8,7 +8,8 @@ import * as ts from 'typescript'; -import {AbsoluteReference, NodeReference, Reference, ReferenceResolver, ResolvedReference} from '../../imports'; +import {Reference} from '../../imports'; +import {OwningModule} from '../../imports/src/references'; import {Declaration, ReflectionHost} from '../../reflection'; import {ArraySliceBuiltinFn} from './builtin'; @@ -78,9 +79,7 @@ interface Context { } export class StaticInterpreter { - constructor( - private host: ReflectionHost, private checker: ts.TypeChecker, - private refResolver: ReferenceResolver) {} + constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {} visit(node: ts.Expression, context: Context): ResolvedValue { return this.visitExpression(node, context); @@ -332,10 +331,7 @@ export class StaticInterpreter { } else if (lhs instanceof Reference) { const ref = lhs.node; if (this.host.isClass(ref)) { - let absoluteModuleName = context.absoluteModuleName; - if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) { - absoluteModuleName = lhs.moduleName || absoluteModuleName; - } + const module = owningModule(context, lhs.bestGuessOwningModule); let value: ResolvedValue = undefined; const member = this.host.getMembersOfClass(ref).find( member => member.isStatic && member.name === strIndex); @@ -343,9 +339,9 @@ export class StaticInterpreter { if (member.value !== null) { value = this.visitExpression(member.value, context); } else if (member.implementation !== null) { - value = new NodeReference(member.implementation, absoluteModuleName); + value = new Reference(member.implementation, module); } else if (member.node) { - value = new NodeReference(member.node, absoluteModuleName); + value = new Reference(member.node, module); } } return value; @@ -391,11 +387,10 @@ export class StaticInterpreter { // If the function is declared in a different file, resolve the foreign function expression // using the absolute module name of that file (if any). - if ((lhs instanceof NodeReference || lhs instanceof AbsoluteReference) && - lhs.moduleName !== null) { + if (lhs.bestGuessOwningModule !== null) { context = { ...context, - absoluteModuleName: lhs.moduleName, + absoluteModuleName: lhs.bestGuessOwningModule.specifier, resolutionContext: node.getSourceFile().fileName, }; } @@ -496,7 +491,7 @@ export class StaticInterpreter { } private getReference(node: ts.Declaration, context: Context): Reference { - return this.refResolver.resolve(node, context.absoluteModuleName, context.resolutionContext); + return new Reference(node, owningModule(context)); } } @@ -542,3 +537,18 @@ function joinModuleContext(existing: Context, node: ts.Node, decl: Declaration): return EMPTY; } } + +function owningModule(context: Context, override: OwningModule | null = null): OwningModule|null { + let specifier = context.absoluteModuleName; + if (override !== null) { + specifier = override.specifier; + } + if (specifier !== null) { + return { + specifier, + resolutionContext: context.resolutionContext, + }; + } else { + return null; + } +} diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts index e45057aba9..640c332b31 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; -import {AbsoluteReference, Reference, TsReferenceResolver} from '../../imports'; +import {Reference} from '../../imports'; import {TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {PartialEvaluator} from '../src/interface'; @@ -44,8 +43,7 @@ function evaluate( code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): T { const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles); const reflectionHost = new TypeScriptReflectionHost(checker); - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); return evaluator.evaluate(expression) as T; } @@ -150,22 +148,17 @@ describe('ngtsc metadata', () => { const reflectionHost = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); const resolved = evaluator.evaluate(expr); if (!(resolved instanceof Reference)) { return fail('Expected expression to resolve to a reference'); } expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); - expect(resolved.expressable).toBe(true); - const reference = resolved.toExpression(program.getSourceFile('entry.ts') !); - if (!(reference instanceof WrappedNodeExpr)) { - return fail('Expected expression reference to be a wrapped node'); + const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !); + if (reference === null) { + return fail('Expected to get an identifier'); } - if (!ts.isIdentifier(reference.node)) { - return fail('Expected expression to be an Identifier'); - } - expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); + expect(reference.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); }); it('absolute imports work', () => { @@ -183,23 +176,16 @@ describe('ngtsc metadata', () => { const reflectionHost = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); const resolved = evaluator.evaluate(expr); - if (!(resolved instanceof AbsoluteReference)) { + if (!(resolved instanceof Reference)) { return fail('Expected expression to resolve to an absolute reference'); } - expect(resolved.moduleName).toBe('some_library'); + expect(owningModuleOf(resolved)).toBe('some_library'); expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); - expect(resolved.expressable).toBe(true); - const reference = resolved.toExpression(program.getSourceFile('entry.ts') !); - if (!(reference instanceof WrappedNodeExpr)) { - return fail('Expected expression reference to be a wrapped node'); - } - if (!ts.isIdentifier(reference.node)) { - return fail('Expected expression to be an Identifier'); - } - expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); + const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !); + expect(reference).not.toBeNull(); + expect(reference !.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); }); it('reads values from default exports', () => { @@ -282,3 +268,7 @@ describe('ngtsc metadata', () => { expect(value instanceof Reference).toBe(true); }); }); + +function owningModuleOf(ref: Reference): string|null { + return ref.bestGuessOwningModule !== null ? ref.bestGuessOwningModule.specifier : null; +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 2906e9fd3e..aa39cfc5b7 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -17,8 +17,10 @@ import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {CycleAnalyzer, ImportGraph} from './cycles'; import {ErrorCode, ngErrorCode} from './diagnostics'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; -import {ImportRewriter, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, TsReferenceResolver} from './imports'; +import {AbsoluteModuleStrategy, FileToModuleHost, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports'; +import {FileToModuleStrategy} from './imports/src/emitter'; import {PartialEvaluator} from './partial_evaluator'; +import {AbsoluteFsPath, LogicalFileSystem} from './path'; import {TypeScriptReflectionHost} from './reflection'; import {HostResourceLoader} from './resource_loader'; import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing'; @@ -27,7 +29,7 @@ import {ivySwitchTransform} from './switch'; import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform'; import {TypeCheckContext, TypeCheckProgramHost} from './typecheck'; import {normalizeSeparators} from './util/src/path'; -import {isDtsPath} from './util/src/typescript'; +import {getRootDirs, isDtsPath} from './util/src/typescript'; export class NgtscProgram implements api.Program { private tsProgram: ts.Program; @@ -40,7 +42,7 @@ export class NgtscProgram implements api.Program { private _importRewriter: ImportRewriter|undefined = undefined; private _reflector: TypeScriptReflectionHost|undefined = undefined; private _isCore: boolean|undefined = undefined; - private rootDirs: string[]; + private rootDirs: AbsoluteFsPath[]; private closureCompilerEnabled: boolean; private entryPoint: ts.SourceFile|null; private exportReferenceGraph: ReferenceGraph|null = null; @@ -51,22 +53,20 @@ export class NgtscProgram implements api.Program { private moduleResolver: ModuleResolver; private cycleAnalyzer: CycleAnalyzer; + private refEmitter: ReferenceEmitter|null = null; + private fileToModuleHost: FileToModuleHost|null = null; constructor( rootNames: ReadonlyArray, private options: api.CompilerOptions, host: api.CompilerHost, oldProgram?: api.Program) { - this.rootDirs = []; - if (options.rootDirs !== undefined) { - this.rootDirs.push(...options.rootDirs); - } else if (options.rootDir !== undefined) { - this.rootDirs.push(options.rootDir); - } else { - this.rootDirs.push(host.getCurrentDirectory()); - } + this.rootDirs = getRootDirs(host, options); this.closureCompilerEnabled = !!options.annotateForClosureCompiler; this.resourceManager = new HostResourceLoader(host, options); const shouldGenerateShims = options.allowEmptyCodegenFiles || false; this.host = host; + if (host.fileNameToModuleName !== undefined) { + this.fileToModuleHost = host as FileToModuleHost; + } let rootFiles = [...rootNames]; const generators: ShimGenerator[] = []; @@ -168,7 +168,7 @@ export class NgtscProgram implements api.Program { const compilation = this.ensureAnalyzed(); const diagnostics = [...compilation.diagnostics]; if (!!this.options.fullTemplateTypeCheck) { - const ctx = new TypeCheckContext(); + const ctx = new TypeCheckContext(this.refEmitter !); compilation.typeCheck(ctx); diagnostics.push(...this.compileTypeCheckProgram(ctx)); } @@ -315,9 +315,32 @@ export class NgtscProgram implements api.Program { private makeCompilation(): IvyCompilation { const checker = this.tsProgram.getTypeChecker(); - const refResolver = new TsReferenceResolver(this.tsProgram, checker, this.options, this.host); - const evaluator = new PartialEvaluator(this.reflector, checker, refResolver); - const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, refResolver); + // Construct the ReferenceEmitter. + if (this.fileToModuleHost === null || !this.options._useHostForImportGeneration) { + // The CompilerHost doesn't have fileNameToModuleName, so build an NPM-centric reference + // resolution strategy. + this.refEmitter = new ReferenceEmitter([ + // First, try to use local identifiers if available. + new LocalIdentifierStrategy(), + // Next, attempt to use an absolute import. + new AbsoluteModuleStrategy(this.tsProgram, checker, this.options, this.host), + // Finally, check if the reference is being written into a file within the project's logical + // file system, and use a relative import if so. If this fails, ReferenceEmitter will throw + // an error. + new LogicalProjectStrategy(checker, new LogicalFileSystem(this.rootDirs)), + ]); + } else { + // The CompilerHost supports fileNameToModuleName, so use that to emit imports. + this.refEmitter = new ReferenceEmitter([ + // First, try to use local identifiers if available. + new LocalIdentifierStrategy(), + // Then use fileNameToModuleName to emit imports. + new FileToModuleStrategy(checker, this.fileToModuleHost), + ]); + } + + const evaluator = new PartialEvaluator(this.reflector, checker); + const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, this.refEmitter); // If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in // order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there @@ -344,7 +367,7 @@ export class NgtscProgram implements api.Program { this.reflector, this.isCore, this.options.strictInjectionParameters || false), new NgModuleDecoratorHandler( this.reflector, evaluator, scopeRegistry, referencesRegistry, this.isCore, - this.routeAnalyzer), + this.routeAnalyzer, this.refEmitter), new PipeDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore), ]; diff --git a/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts b/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts index 3bfa9d2004..3e1eb580c3 100644 --- a/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts +++ b/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {AbsoluteReference, NodeReference, Reference} from '../../imports'; +import {Reference} from '../../imports'; import {ForeignFunctionResolver, PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {NgModuleRawRouteData} from './analyzer'; @@ -163,7 +163,9 @@ const routerModuleFFR: ForeignFunctionResolver = null { if (!isMethodNodeReference(ref) || !ts.isClassDeclaration(ref.node.parent)) { return null; - } else if (ref.moduleName !== '@angular/router') { + } else if ( + ref.bestGuessOwningModule === null || + ref.bestGuessOwningModule.specifier !== '@angular/router') { return null; } else if ( ref.node.parent.name === undefined || ref.node.parent.name.text !== 'RouterModule') { @@ -188,11 +190,11 @@ function hasIdentifier(node: ts.Node): node is ts.Node&{name: ts.Identifier} { function isMethodNodeReference( ref: Reference): - ref is NodeReference { - return ref instanceof NodeReference && ts.isMethodDeclaration(ref.node); + ref is Reference { + return ts.isMethodDeclaration(ref.node); } function isRouteToken(ref: ResolvedValue): boolean { - return ref instanceof AbsoluteReference && ref.moduleName === '@angular/router' && - ref.symbolName === 'ROUTES'; + return ref instanceof Reference && ref.bestGuessOwningModule !== null && + ref.bestGuessOwningModule.specifier === '@angular/router' && ref.debugName === 'ROUTES'; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index b76a88f9b2..43500d5955 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -9,7 +9,7 @@ import {R3TargetBinder, SelectorMatcher, TmplAstNode} from '@angular/compiler'; import * as ts from 'typescript'; -import {NoopImportRewriter} from '../../imports'; +import {NoopImportRewriter, ReferenceEmitter} from '../../imports'; import {ImportManager} from '../../translator'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCtorMetadata} from './api'; @@ -26,6 +26,8 @@ import {generateTypeCtor} from './type_constructor'; * checking code. */ export class TypeCheckContext { + constructor(private refEmitter: ReferenceEmitter) {} + /** * A `Set` of classes which will be used to generate type constructors. */ @@ -136,7 +138,7 @@ export class TypeCheckContext { // Process each operation and use the printer to generate source code for it, inserting it into // the source code in between the original chunks. ops.forEach((op, idx) => { - const text = op.execute(importManager, sf, printer); + const text = op.execute(importManager, sf, this.refEmitter, printer); code += text + textParts[idx + 1]; }); @@ -182,7 +184,8 @@ interface Op { /** * Execute the operation and return the generated code as text. */ - execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string; + execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): + string; } /** @@ -196,8 +199,9 @@ class TcbOp implements Op { */ get splitPoint(): number { return this.node.end + 1; } - execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string { - const tcb = generateTypeCheckBlock(this.node, this.meta, im); + execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): + string { + const tcb = generateTypeCheckBlock(this.node, this.meta, im, refEmitter); return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); } } @@ -213,7 +217,8 @@ class TypeCtorOp implements Op { */ get splitPoint(): number { return this.node.end - 1; } - execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string { + execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): + string { const tcb = generateTypeCtor(this.node, this.meta); return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 08d207f5ac..3dfcc90f13 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -9,7 +9,7 @@ import {AST, BindingType, BoundTarget, ImplicitReceiver, PropertyRead, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; import * as ts from 'typescript'; -import {Reference} from '../../imports'; +import {Reference, ReferenceEmitter} from '../../imports'; import {ImportManager, translateExpression} from '../../translator'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api'; @@ -28,9 +28,9 @@ import {astToTypescript} from './expression'; * @param importManager an `ImportManager` for the file into which the TCB will be written. */ export function generateTypeCheckBlock( - node: ts.ClassDeclaration, meta: TypeCheckBlockMetadata, - importManager: ImportManager): ts.FunctionDeclaration { - const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager); + node: ts.ClassDeclaration, meta: TypeCheckBlockMetadata, importManager: ImportManager, + refEmitter: ReferenceEmitter): ts.FunctionDeclaration { + const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager, refEmitter); const scope = new Scope(tcb); tcbProcessNodes(meta.boundTarget.target.template !, tcb, scope); @@ -59,7 +59,8 @@ class Context { constructor( readonly boundTarget: BoundTarget, - private sourceFile: ts.SourceFile, private importManager: ImportManager) {} + private sourceFile: ts.SourceFile, private importManager: ImportManager, + private refEmitter: ReferenceEmitter) {} /** * Allocate a new variable name for use within the `Context`. @@ -75,7 +76,7 @@ class Context { * This may involve importing the node into the file if it's not declared there already. */ reference(ref: Reference): ts.Expression { - const ngExpr = ref.toExpression(this.sourceFile); + const ngExpr = this.refEmitter.emit(ref, this.sourceFile); if (ngExpr === null) { throw new Error(`Unreachable reference: ${ref.node}`); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel index 97da7159e6..233796c5ff 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel @@ -10,8 +10,11 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/util", "@ngdeps//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts index 29779fbb44..e92a7a8840 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts @@ -8,7 +8,10 @@ import * as ts from 'typescript'; +import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitter} from '../../imports'; +import {LogicalFileSystem} from '../../path'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getRootDirs} from '../../util/src/typescript'; import {TypeCheckContext} from '../src/context'; import {TypeCheckProgramHost} from '../src/host'; @@ -23,7 +26,6 @@ const LIB_D_TS = { describe('ngtsc typechecking', () => { describe('ctors', () => { it('compiles a basic type constructor', () => { - const ctx = new TypeCheckContext(); const files = [ LIB_D_TS, { name: 'main.ts', @@ -36,7 +38,15 @@ TestClass.ngTypeCtor({value: 'test'}); ` } ]; - const {program, host} = makeProgram(files, undefined, undefined, false); + const {program, host, options} = makeProgram(files, undefined, undefined, false); + const checker = program.getTypeChecker(); + const logicalFs = new LogicalFileSystem(getRootDirs(host, options)); + const emitter = new ReferenceEmitter([ + new LocalIdentifierStrategy(), + new AbsoluteModuleStrategy(program, checker, options, host), + new LogicalProjectStrategy(checker, logicalFs), + ]); + const ctx = new TypeCheckContext(emitter); const TestClass = getDeclaration(program, 'main.ts', 'TestClass', ts.isClassDeclaration); ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, { fnName: 'ngTypeCtor', diff --git a/packages/compiler-cli/src/ngtsc/util/BUILD.bazel b/packages/compiler-cli/src/ngtsc/util/BUILD.bazel index 1f63dd3f2e..4f385af484 100644 --- a/packages/compiler-cli/src/ngtsc/util/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/util/BUILD.bazel @@ -10,6 +10,7 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/path", "@ngdeps//@types/node", "@ngdeps//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts index 3ef6e50cab..db4860a429 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts @@ -10,6 +10,7 @@ const TS = /\.tsx?$/i; const D_TS = /\.d\.ts$/i; import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../path'; export function isDtsPath(filePath: string): boolean { return D_TS.test(filePath); @@ -27,6 +28,17 @@ export function isFromDtsFile(node: ts.Node): boolean { return sf !== undefined && D_TS.test(sf.fileName); } +export function nodeNameForError(node: ts.Node & {name?: ts.Node}): string { + if (node.name !== undefined && ts.isIdentifier(node.name)) { + return node.name.text; + } else { + const kind = ts.SyntaxKind[node.kind]; + const {line, character} = + ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()); + return `${kind}@${line}:${character}`; + } +} + export function getSourceFile(node: ts.Node): ts.SourceFile { // In certain transformation contexts, `ts.Node.getSourceFile()` can actually return `undefined`, // despite the type signature not allowing it. In that event, get the `ts.SourceFile` via the @@ -34,3 +46,37 @@ export function getSourceFile(node: ts.Node): ts.SourceFile { const directSf = node.getSourceFile() as ts.SourceFile | undefined; return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile(); } + +export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifier|null { + if (decl.name !== undefined && ts.isIdentifier(decl.name)) { + return decl.name; + } else { + return null; + } +} + +export function isDeclaration(node: ts.Node): node is ts.Declaration { + return false || ts.isEnumDeclaration(node) || ts.isClassDeclaration(node) || + ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node); +} + +export function isExported(node: ts.Declaration): boolean { + let topLevel: ts.Node = node; + if (ts.isVariableDeclaration(node) && ts.isVariableDeclarationList(node.parent)) { + topLevel = node.parent.parent; + } + return topLevel.modifiers !== undefined && + topLevel.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword); +} + +export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions): AbsoluteFsPath[] { + const rootDirs: string[] = []; + if (options.rootDirs !== undefined) { + rootDirs.push(...options.rootDirs); + } else if (options.rootDir !== undefined) { + rootDirs.push(options.rootDir); + } else { + rootDirs.push(host.getCurrentDirectory()); + } + return rootDirs.map(rootDir => AbsoluteFsPath.fromUnchecked(rootDir)); +} diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 1ca98f4034..f0c553d229 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -112,6 +112,12 @@ export interface CompilerOptions extends ts.CompilerOptions { // This will be true be default in Angular 6. fullTemplateTypeCheck?: boolean; + // Whether to use the CompilerHost's fileNameToModuleName utility (if available) to generate + // import module specifiers. This is false by default, and exists to support running ngtsc + // within Google. This option is internal and is used by the ng_module.bzl rule to switch + // behavior between Bazel and Blaze. + _useHostForImportGeneration?: boolean; + // Insert JSDoc type annotations needed by Closure Compiler annotateForClosureCompiler?: boolean;