fix(compiler): StaticReflect now resolves re-exported symbols (#10453)
Fixes: #10451
This commit is contained in:
parent
3d53b33391
commit
82e7ecd611
|
@ -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 || !(<any>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((<any>sf).symbol).find(m => m.name === symbolName);
|
||||
|
@ -159,6 +156,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
|||
}
|
||||
|
||||
private typeCache = new Map<string, StaticSymbol>();
|
||||
private resolverCache = new Map<string, ModuleMetadata>();
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 <MetadataSymbolicExpression>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 = <ts.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 = (<ts.StringLiteral>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 = <ts.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)[];
|
||||
|
|
|
@ -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': `
|
||||
|
|
Loading…
Reference in New Issue