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'
|
provider: '@angular/core/src/di/provider'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolve(m: string, containingFile: string) {
|
private resolve(m: string, containingFile: string) {
|
||||||
const resolved =
|
const resolved =
|
||||||
ts.resolveModuleName(m, containingFile, this.options, this.context).resolvedModule;
|
ts.resolveModuleName(m, containingFile, this.options, this.context).resolvedModule;
|
||||||
|
@ -72,12 +73,9 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
||||||
importedFile = this.resolveAssetUrl(importedFile, containingFile);
|
importedFile = this.resolveAssetUrl(importedFile, containingFile);
|
||||||
containingFile = this.resolveAssetUrl(containingFile, '');
|
containingFile = this.resolveAssetUrl(containingFile, '');
|
||||||
|
|
||||||
// TODO(tbosch): if a file does not yet exist (because we compile it later),
|
// If a file does not yet exist (because we compile it later), we still need to
|
||||||
// we still need to create it so that the `resolve` method works!
|
// assume it exists it so that the `resolve` method works!
|
||||||
if (!this.compilerHost.fileExists(importedFile)) {
|
if (!this.compilerHost.fileExists(importedFile)) {
|
||||||
if (this.options.trace) {
|
|
||||||
console.log(`Generating empty file ${importedFile} to allow resolution of import`);
|
|
||||||
}
|
|
||||||
this.context.assumeFileExists(importedFile);
|
this.context.assumeFileExists(importedFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,11 +131,10 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
||||||
const sf = this.program.getSourceFile(filePath);
|
const sf = this.program.getSourceFile(filePath);
|
||||||
if (!sf || !(<any>sf).symbol) {
|
if (!sf || !(<any>sf).symbol) {
|
||||||
// The source file was not needed in the compile but we do need the values from
|
// 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
|
// the corresponding .ts files stored in the .metadata.json file. Check the file
|
||||||
// symbol and file we resolved to be correct as we don't need this to be the
|
// for exports to see if the file is exported.
|
||||||
// cannonical reference as this reference could have only been generated by a
|
return this.resolveExportedSymbol(filePath, symbolName) ||
|
||||||
// .metadata.json file resolving values.
|
this.getStaticSymbol(filePath, symbolName);
|
||||||
return this.getStaticSymbol(filePath, symbolName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let symbol = tc.getExportsOfModule((<any>sf).symbol).find(m => m.name === 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 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.
|
* 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) {
|
readMetadata(filePath: string) {
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(this.context.readFile(filePath));
|
return this.resolverCache.get(filePath) || JSON.parse(this.context.readFile(filePath));
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to read JSON file ${filePath}`);
|
console.error(`Failed to read JSON file ${filePath}`);
|
||||||
throw e;
|
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 {
|
export class NodeReflectorHostContext implements ReflectorHostContext {
|
||||||
|
|
|
@ -105,6 +105,34 @@ describe('reflector_host', () => {
|
||||||
it('should return undefined for missing modules', () => {
|
it('should return undefined for missing modules', () => {
|
||||||
expect(reflectorHost.getMetadataFor('node_modules/@angular/missing.d.ts')).toBeUndefined();
|
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[];';
|
const dummyModule = 'export let foo: any[];';
|
||||||
|
@ -124,6 +152,69 @@ const FILES: Entry = {
|
||||||
'collections.ts': dummyModule,
|
'collections.ts': dummyModule,
|
||||||
},
|
},
|
||||||
'lib2': {'utils2.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': {
|
'node_modules': {
|
||||||
'@angular': {
|
'@angular': {
|
||||||
'core.d.ts': dummyModule,
|
'core.d.ts': dummyModule,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {Evaluator, errorSymbol, isPrimitive} from './evaluator';
|
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';
|
import {Symbols} from './symbols';
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ export class MetadataCollector {
|
||||||
const locals = new Symbols(sourceFile);
|
const locals = new Symbols(sourceFile);
|
||||||
const evaluator = new Evaluator(locals);
|
const evaluator = new Evaluator(locals);
|
||||||
let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined;
|
let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined;
|
||||||
|
let exports: ModuleExportMetadata[];
|
||||||
|
|
||||||
function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression {
|
function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression {
|
||||||
return <MetadataSymbolicExpression>evaluator.evaluateNode(decoratorNode.expression);
|
return <MetadataSymbolicExpression>evaluator.evaluateNode(decoratorNode.expression);
|
||||||
|
@ -202,6 +203,25 @@ export class MetadataCollector {
|
||||||
});
|
});
|
||||||
ts.forEachChild(sourceFile, node => {
|
ts.forEachChild(sourceFile, node => {
|
||||||
switch (node.kind) {
|
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:
|
case ts.SyntaxKind.ClassDeclaration:
|
||||||
const classDeclaration = <ts.ClassDeclaration>node;
|
const classDeclaration = <ts.ClassDeclaration>node;
|
||||||
const className = classDeclaration.name.text;
|
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 {
|
export interface ModuleMetadata {
|
||||||
__symbolic: 'module';
|
__symbolic: 'module';
|
||||||
version: number;
|
version: number;
|
||||||
|
exports?: ModuleExportMetadata[];
|
||||||
metadata: {[name: string]: (ClassMetadata | FunctionMetadata | MetadataValue)};
|
metadata: {[name: string]: (ClassMetadata | FunctionMetadata | MetadataValue)};
|
||||||
}
|
}
|
||||||
export function isModuleMetadata(value: any): value is ModuleMetadata {
|
export function isModuleMetadata(value: any): value is ModuleMetadata {
|
||||||
return value && value.__symbolic === 'module';
|
return value && value.__symbolic === 'module';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModuleExportMetadata {
|
||||||
|
export?: (string|{name: string, as: string})[];
|
||||||
|
from: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClassMetadata {
|
export interface ClassMetadata {
|
||||||
__symbolic: 'class';
|
__symbolic: 'class';
|
||||||
decorators?: (MetadataSymbolicExpression|MetadataError)[];
|
decorators?: (MetadataSymbolicExpression|MetadataError)[];
|
||||||
|
|
|
@ -24,6 +24,7 @@ describe('Collector', () => {
|
||||||
'exported-functions.ts',
|
'exported-functions.ts',
|
||||||
'exported-enum.ts',
|
'exported-enum.ts',
|
||||||
'exported-consts.ts',
|
'exported-consts.ts',
|
||||||
|
're-exports.ts',
|
||||||
'static-field-reference.ts',
|
'static-field-reference.ts',
|
||||||
'static-method.ts',
|
'static-method.ts',
|
||||||
'static-method-call.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
|
// 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': {
|
'node_modules': {
|
||||||
'angular2': {
|
'angular2': {
|
||||||
'core.d.ts': `
|
'core.d.ts': `
|
||||||
|
|
Loading…
Reference in New Issue