From 82e7ecd611e0617b07d6f336724adf328b7f98f6 Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Tue, 2 Aug 2016 11:45:14 -0700 Subject: [PATCH] fix(compiler): StaticReflect now resolves re-exported symbols (#10453) Fixes: #10451 --- .../compiler-cli/src/reflector_host.ts | 80 +++++++++++++--- .../compiler-cli/test/reflector_host_spec.ts | 91 +++++++++++++++++++ tools/@angular/tsc-wrapped/src/collector.ts | 29 +++++- tools/@angular/tsc-wrapped/src/schema.ts | 6 ++ .../tsc-wrapped/test/collector.spec.ts | 16 ++++ 5 files changed, 208 insertions(+), 14 deletions(-) diff --git a/modules/@angular/compiler-cli/src/reflector_host.ts b/modules/@angular/compiler-cli/src/reflector_host.ts index 5ce03e5ef1..6f5e35d37d 100644 --- a/modules/@angular/compiler-cli/src/reflector_host.ts +++ b/modules/@angular/compiler-cli/src/reflector_host.ts @@ -43,6 +43,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator { provider: '@angular/core/src/di/provider' }; } + private resolve(m: string, containingFile: string) { const resolved = ts.resolveModuleName(m, containingFile, this.options, this.context).resolvedModule; @@ -72,12 +73,9 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator { importedFile = this.resolveAssetUrl(importedFile, containingFile); containingFile = this.resolveAssetUrl(containingFile, ''); - // TODO(tbosch): if a file does not yet exist (because we compile it later), - // we still need to create it so that the `resolve` method works! + // If a file does not yet exist (because we compile it later), we still need to + // assume it exists it so that the `resolve` method works! if (!this.compilerHost.fileExists(importedFile)) { - if (this.options.trace) { - console.log(`Generating empty file ${importedFile} to allow resolution of import`); - } this.context.assumeFileExists(importedFile); } @@ -133,11 +131,10 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator { const sf = this.program.getSourceFile(filePath); if (!sf || !(sf).symbol) { // The source file was not needed in the compile but we do need the values from - // the corresponding .ts files stored in the .metadata.json file. Just assume the - // symbol and file we resolved to be correct as we don't need this to be the - // cannonical reference as this reference could have only been generated by a - // .metadata.json file resolving values. - return this.getStaticSymbol(filePath, symbolName); + // the corresponding .ts files stored in the .metadata.json file. Check the file + // for exports to see if the file is exported. + return this.resolveExportedSymbol(filePath, symbolName) || + this.getStaticSymbol(filePath, symbolName); } let symbol = tc.getExportsOfModule((sf).symbol).find(m => m.name === symbolName); @@ -159,6 +156,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator { } private typeCache = new Map(); + private resolverCache = new Map(); /** * getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded. @@ -200,13 +198,71 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator { readMetadata(filePath: string) { try { - const result = JSON.parse(this.context.readFile(filePath)); - return result; + return this.resolverCache.get(filePath) || JSON.parse(this.context.readFile(filePath)); } catch (e) { console.error(`Failed to read JSON file ${filePath}`); throw e; } } + + private getResolverMetadata(filePath: string): ModuleMetadata { + let metadata = this.resolverCache.get(filePath); + if (!metadata) { + metadata = this.getMetadataFor(filePath); + this.resolverCache.set(filePath, metadata); + } + return metadata; + } + + private resolveExportedSymbol(filePath: string, symbolName: string): StaticSymbol { + const resolveModule = (moduleName: string): string => { + const resolvedModulePath = this.resolve(moduleName, filePath); + if (!resolvedModulePath) { + throw new Error(`Could not resolve module '${moduleName}' relative to file ${filePath}`); + } + return resolvedModulePath; + }; + let metadata = this.getResolverMetadata(filePath); + if (metadata) { + // If we have metadata for the symbol, this is the original exporting location. + if (metadata.metadata[symbolName]) { + return this.getStaticSymbol(filePath, symbolName); + } + + // If no, try to find the symbol in one of the re-export location + if (metadata.exports) { + // Try and find the symbol in the list of explicitly re-exported symbols. + for (const moduleExport of metadata.exports) { + if (moduleExport.export) { + const exportSymbol = moduleExport.export.find(symbol => { + if (typeof symbol === 'string') { + return symbol == symbolName; + } else { + return symbol.as == symbolName; + } + }); + if (exportSymbol) { + let symName = symbolName; + if (typeof exportSymbol !== 'string') { + symName = exportSymbol.name; + } + return this.resolveExportedSymbol(resolveModule(moduleExport.from), symName); + } + } + } + + // Try to find the symbol via export * directives. + for (const moduleExport of metadata.exports) { + if (!moduleExport.export) { + const resolvedModule = resolveModule(moduleExport.from); + const candidateSymbol = this.resolveExportedSymbol(resolvedModule, symbolName); + if (candidateSymbol) return candidateSymbol; + } + } + } + } + return null; + } } export class NodeReflectorHostContext implements ReflectorHostContext { diff --git a/modules/@angular/compiler-cli/test/reflector_host_spec.ts b/modules/@angular/compiler-cli/test/reflector_host_spec.ts index ee6ee3dfab..29e15db664 100644 --- a/modules/@angular/compiler-cli/test/reflector_host_spec.ts +++ b/modules/@angular/compiler-cli/test/reflector_host_spec.ts @@ -105,6 +105,34 @@ describe('reflector_host', () => { it('should return undefined for missing modules', () => { expect(reflectorHost.getMetadataFor('node_modules/@angular/missing.d.ts')).toBeUndefined(); }); + + it('should be able to trace a named export', () => { + const symbol = + reflectorHost.findDeclaration('./reexport/reexport.d.ts', 'One', '/tmp/src/main.ts'); + expect(symbol.name).toEqual('One'); + expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts'); + }); + + it('should be able to trace a renamed export', () => { + const symbol = + reflectorHost.findDeclaration('./reexport/reexport.d.ts', 'Four', '/tmp/src/main.ts'); + expect(symbol.name).toEqual('Three'); + expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts'); + }); + + it('should be able to trace an export * export', () => { + const symbol = + reflectorHost.findDeclaration('./reexport/reexport.d.ts', 'Five', '/tmp/src/main.ts'); + expect(symbol.name).toEqual('Five'); + expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin5.d.ts'); + }); + + it('should be able to trace a multi-level re-export', () => { + const symbol = + reflectorHost.findDeclaration('./reexport/reexport.d.ts', 'Thirty', '/tmp/src/main.ts'); + expect(symbol.name).toEqual('Thirty'); + expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin30.d.ts'); + }); }); const dummyModule = 'export let foo: any[];'; @@ -124,6 +152,69 @@ const FILES: Entry = { 'collections.ts': dummyModule, }, 'lib2': {'utils2.ts': dummyModule}, + 'reexport': { + 'reexport.d.ts': ` + import * as c from '@angular/core'; + `, + 'reexport.metadata.json': JSON.stringify({ + __symbolic: 'module', + version: 1, + metadata: {}, + exports: [ + {from: './src/origin1', export: ['One', 'Two', {name: 'Three', as: 'Four'}]}, + {from: './src/origin5'}, {from: './src/reexport2'} + ] + }), + 'src': { + 'origin1.d.ts': ` + export class One {} + export class Two {} + export class Three {} + `, + 'origin1.metadata.json': JSON.stringify({ + __symbolic: 'module', + version: 1, + metadata: { + One: {__symbolic: 'class'}, + Two: {__symbolic: 'class'}, + Three: {__symbolic: 'class'}, + }, + }), + 'origin5.d.ts': ` + export class Five {} + `, + 'origin5.metadata.json': JSON.stringify({ + __symbolic: 'module', + version: 1, + metadata: { + Five: {__symbolic: 'class'}, + }, + }), + 'origin30.d.ts': ` + export class Thirty {} + `, + 'origin30.metadata.json': JSON.stringify({ + __symbolic: 'module', + version: 1, + metadata: { + Thirty: {__symbolic: 'class'}, + }, + }), + 'originNone.d.ts': dummyModule, + 'originNone.metadata.json': JSON.stringify({ + __symbolic: 'module', + version: 1, + metadata: {}, + }), + 'reexport2.d.ts': dummyModule, + 'reexport2.metadata.json': JSON.stringify({ + __symbolic: 'module', + version: 1, + metadata: {}, + exports: [{from: './originNone'}, {from: './origin30'}] + }) + } + }, 'node_modules': { '@angular': { 'core.d.ts': dummyModule, diff --git a/tools/@angular/tsc-wrapped/src/collector.ts b/tools/@angular/tsc-wrapped/src/collector.ts index 4e6a822bee..4130a772c3 100644 --- a/tools/@angular/tsc-wrapped/src/collector.ts +++ b/tools/@angular/tsc-wrapped/src/collector.ts @@ -1,7 +1,7 @@ import * as ts from 'typescript'; import {Evaluator, errorSymbol, isPrimitive} from './evaluator'; -import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} from './schema'; +import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} from './schema'; import {Symbols} from './symbols'; @@ -20,6 +20,7 @@ export class MetadataCollector { const locals = new Symbols(sourceFile); const evaluator = new Evaluator(locals); let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined; + let exports: ModuleExportMetadata[]; function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { return evaluator.evaluateNode(decoratorNode.expression); @@ -202,6 +203,25 @@ export class MetadataCollector { }); ts.forEachChild(sourceFile, node => { switch (node.kind) { + case ts.SyntaxKind.ExportDeclaration: + // Record export declarations + const exportDeclaration = node; + const moduleSpecifier = exportDeclaration.moduleSpecifier; + if (moduleSpecifier && moduleSpecifier.kind == ts.SyntaxKind.StringLiteral) { + // Ignore exports that don't have string literals as exports. + // This is allowed by the syntax but will be flagged as an error by the type checker. + const from = (moduleSpecifier).text; + const moduleExport: ModuleExportMetadata = {from}; + if (exportDeclaration.exportClause) { + moduleExport.export = exportDeclaration.exportClause.elements.map( + element => element.propertyName ? + {name: element.propertyName.text, as: element.name.text} : + element.name.text) + } + if (!exports) exports = []; + exports.push(moduleExport); + } + break; case ts.SyntaxKind.ClassDeclaration: const classDeclaration = node; const className = classDeclaration.name.text; @@ -320,7 +340,12 @@ export class MetadataCollector { } }); - return metadata && {__symbolic: 'module', version: VERSION, metadata}; + if (metadata || exports) { + if (!metadata) metadata = {}; + const result: ModuleMetadata = {__symbolic: 'module', version: VERSION, metadata}; + if (exports) result.exports = exports; + return result; + } } } diff --git a/tools/@angular/tsc-wrapped/src/schema.ts b/tools/@angular/tsc-wrapped/src/schema.ts index ee255db1d4..fea3146b3f 100644 --- a/tools/@angular/tsc-wrapped/src/schema.ts +++ b/tools/@angular/tsc-wrapped/src/schema.ts @@ -12,12 +12,18 @@ export const VERSION = 1; export interface ModuleMetadata { __symbolic: 'module'; version: number; + exports?: ModuleExportMetadata[]; metadata: {[name: string]: (ClassMetadata | FunctionMetadata | MetadataValue)}; } export function isModuleMetadata(value: any): value is ModuleMetadata { return value && value.__symbolic === 'module'; } +export interface ModuleExportMetadata { + export?: (string|{name: string, as: string})[]; + from: string; +} + export interface ClassMetadata { __symbolic: 'class'; decorators?: (MetadataSymbolicExpression|MetadataError)[]; diff --git a/tools/@angular/tsc-wrapped/test/collector.spec.ts b/tools/@angular/tsc-wrapped/test/collector.spec.ts index 3a9bd2b932..0d6ea26b09 100644 --- a/tools/@angular/tsc-wrapped/test/collector.spec.ts +++ b/tools/@angular/tsc-wrapped/test/collector.spec.ts @@ -24,6 +24,7 @@ describe('Collector', () => { 'exported-functions.ts', 'exported-enum.ts', 'exported-consts.ts', + 're-exports.ts', 'static-field-reference.ts', 'static-method.ts', 'static-method-call.ts', @@ -475,6 +476,16 @@ describe('Collector', () => { } }); }); + + it('should be able to collect re-exported symbols', () => { + let source = program.getSourceFile('/re-exports.ts'); + let metadata = collector.getMetadata(source); + expect(metadata.exports).toEqual([ + {from: './static-field', export: ['MyModule']}, + {from: './static-field-reference.ts', export: [{name: 'Foo', as: 'OtherModule'}]}, + {from: 'angular2/core'} + ]); + }); }); // TODO: Do not use \` in a template literal as it confuses clang-format @@ -783,6 +794,11 @@ const FILES: Directory = { } } `, + 're-exports.ts': ` + export {MyModule} from './static-field'; + export {Foo as OtherModule} from './static-field-reference.ts'; + export * from 'angular2/core'; + `, 'node_modules': { 'angular2': { 'core.d.ts': `