diff --git a/modules/@angular/compiler/src/aot/compiler_factory.ts b/modules/@angular/compiler/src/aot/compiler_factory.ts index 36a2ad2c42..9975018128 100644 --- a/modules/@angular/compiler/src/aot/compiler_factory.ts +++ b/modules/@angular/compiler/src/aot/compiler_factory.ts @@ -77,7 +77,8 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom const importResolver = { getImportAs: (symbol: StaticSymbol) => symbolResolver.getImportAs(symbol), fileNameToModuleName: (fileName: string, containingFilePath: string) => - compilerHost.fileNameToModuleName(fileName, containingFilePath) + compilerHost.fileNameToModuleName(fileName, containingFilePath), + getTypeArity: (symbol: StaticSymbol) => symbolResolver.getTypeArity(symbol) }; const compiler = new AotCompiler( compilerHost, resolver, tmplParser, new StyleCompiler(urlResolver), diff --git a/modules/@angular/compiler/src/aot/static_symbol_resolver.ts b/modules/@angular/compiler/src/aot/static_symbol_resolver.ts index 7d5e447d80..0485b5c767 100644 --- a/modules/@angular/compiler/src/aot/static_symbol_resolver.ts +++ b/modules/@angular/compiler/src/aot/static_symbol_resolver.ts @@ -10,6 +10,7 @@ import {SummaryResolver} from '../summary_resolver'; import {ValueTransformer, visitValue} from '../util'; import {StaticSymbol, StaticSymbolCache} from './static_symbol'; +import {isNgFactoryFile} from './util'; export class ResolvedStaticSymbol { constructor(public symbol: StaticSymbol, public metadata: any) {} @@ -83,6 +84,15 @@ export class StaticSymbolResolver { return result; } + /** + * getImportAs produces a symbol that can be used to import the given symbol. + * The import might be different than the symbol if the symbol is exported from + * a library with a summary; in which case we want to import the symbol from the + * ngfactory re-export instead of directly to avoid introducing a direct dependency + * on an otherwise indirect dependency. + * + * @param staticSymbol the symbol for which to generate a import symbol + */ getImportAs(staticSymbol: StaticSymbol): StaticSymbol { if (staticSymbol.members.length) { const baseSymbol = this.getStaticSymbol(staticSymbol.filePath, staticSymbol.name); @@ -98,6 +108,25 @@ export class StaticSymbolResolver { return result; } + /** + * getTypeArity returns the number of generic type parameters the given symbol + * has. If the symbol is not a type the result is null. + */ + getTypeArity(staticSymbol: StaticSymbol): number /*|null*/ { + // If the file is a factory file, don't resolve the symbol as doing so would + // cause the metadata for an factory file to be loaded which doesn't exist. + // All references to generated classes must include the correct arity whenever + // generating code. + if (isNgFactoryFile(staticSymbol.filePath)) { + return null; + } + let resolvedSymbol = this.resolveSymbol(staticSymbol); + while (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) { + resolvedSymbol = this.resolveSymbol(resolvedSymbol.metadata); + } + return (resolvedSymbol && resolvedSymbol.metadata && resolvedSymbol.metadata.arity) || null; + } + private _resolveSymbolMembers(staticSymbol: StaticSymbol): ResolvedStaticSymbol { const members = staticSymbol.members; const baseResolvedSymbol = @@ -134,6 +163,7 @@ export class StaticSymbolResolver { * * @param declarationFile the absolute path of the file where the symbol is declared * @param name the name of the type. + * @param members a symbol for a static member of the named type */ getStaticSymbol(declarationFile: string, name: string, members?: string[]): StaticSymbol { return this.staticSymbolCache.get(declarationFile, name, members); diff --git a/modules/@angular/compiler/src/aot/summary_serializer.ts b/modules/@angular/compiler/src/aot/summary_serializer.ts index b2481c696e..6bf3011dd8 100644 --- a/modules/@angular/compiler/src/aot/summary_serializer.ts +++ b/modules/@angular/compiler/src/aot/summary_serializer.ts @@ -94,10 +94,10 @@ class Serializer extends ValueTransformer { addOrMergeSummary(summary: Summary) { let symbolMeta = summary.metadata; if (symbolMeta && symbolMeta.__symbolic === 'class') { - // For classes, we only keep their statics, but not the metadata + // For classes, we only keep their statics and arity, but not the metadata // of the class itself as that has been captured already via other summaries // (e.g. DirectiveSummary, ...). - symbolMeta = {__symbolic: 'class', statics: symbolMeta.statics}; + symbolMeta = {__symbolic: 'class', statics: symbolMeta.statics, arity: symbolMeta.arity}; } let processedSummary = this.processedSummaryBySymbol.get(summary.symbol); @@ -106,11 +106,11 @@ class Serializer extends ValueTransformer { this.processedSummaries.push(processedSummary); this.processedSummaryBySymbol.set(summary.symbol, processedSummary); } - // Note: == by purpose to compare with undefined! + // Note: == on purpose to compare with undefined! if (processedSummary.metadata == null && symbolMeta != null) { processedSummary.metadata = this.processValue(symbolMeta); } - // Note: == by purpose to compare with undefined! + // Note: == on purpose to compare with undefined! if (processedSummary.type == null && summary.type != null) { processedSummary.type = this.processValue(summary.type); } @@ -147,7 +147,7 @@ class Serializer extends ValueTransformer { if (value instanceof StaticSymbol) { const baseSymbol = this.symbolResolver.getStaticSymbol(value.filePath, value.name); let index = this.indexBySymbol.get(baseSymbol); - // Note: == by purpose to compare with undefined! + // Note: == on purpose to compare with undefined! if (index == null) { index = this.indexBySymbol.size; this.indexBySymbol.set(baseSymbol, index); diff --git a/modules/@angular/compiler/src/aot/util.ts b/modules/@angular/compiler/src/aot/util.ts index 60e3347f10..4285f805ec 100644 --- a/modules/@angular/compiler/src/aot/util.ts +++ b/modules/@angular/compiler/src/aot/util.ts @@ -7,6 +7,7 @@ */ const STRIP_SRC_FILE_SUFFIXES = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; +const NG_FACTORY = /\.ngfactory\./; export function ngfactoryFilePath(filePath: string): string { const urlWithSuffix = splitTypescriptSuffix(filePath); @@ -14,7 +15,11 @@ export function ngfactoryFilePath(filePath: string): string { } export function stripNgFactory(filePath: string): string { - return filePath.replace(/\.ngfactory\./, '.'); + return filePath.replace(NG_FACTORY, '.'); +} + +export function isNgFactoryFile(filePath: string): boolean { + return NG_FACTORY.test(filePath); } export function splitTypescriptSuffix(path: string): string[] { diff --git a/modules/@angular/compiler/src/output/output_ast.ts b/modules/@angular/compiler/src/output/output_ast.ts index 0daf19705c..31e5d556f2 100644 --- a/modules/@angular/compiler/src/output/output_ast.ts +++ b/modules/@angular/compiler/src/output/output_ast.ts @@ -44,11 +44,7 @@ export class BuiltinType extends Type { } export class ExpressionType extends Type { - constructor( - public value: Expression, public typeParams: Type[] = null, - modifiers: TypeModifier[] = null) { - super(modifiers); - } + constructor(public value: Expression, modifiers: TypeModifier[] = null) { super(modifiers); } visitType(visitor: TypeVisitor, context: any): any { return visitor.visitExpressionType(this, context); } @@ -881,13 +877,12 @@ export function importExpr(id: CompileIdentifierMetadata, typeParams: Type[] = n export function importType( id: CompileIdentifierMetadata, typeParams: Type[] = null, typeModifiers: TypeModifier[] = null): ExpressionType { - return isPresent(id) ? expressionType(importExpr(id), typeParams, typeModifiers) : null; + return isPresent(id) ? expressionType(importExpr(id, typeParams), typeModifiers) : null; } export function expressionType( - expr: Expression, typeParams: Type[] = null, - typeModifiers: TypeModifier[] = null): ExpressionType { - return isPresent(expr) ? new ExpressionType(expr, typeParams, typeModifiers) : null; + expr: Expression, typeModifiers: TypeModifier[] = null): ExpressionType { + return isPresent(expr) ? new ExpressionType(expr, typeModifiers) : null; } export function literalArr(values: Expression[], type: Type = null): LiteralArrayExpr { diff --git a/modules/@angular/compiler/src/output/path_util.ts b/modules/@angular/compiler/src/output/path_util.ts index b80d861382..bc262193da 100644 --- a/modules/@angular/compiler/src/output/path_util.ts +++ b/modules/@angular/compiler/src/output/path_util.ts @@ -24,4 +24,9 @@ export abstract class ImportResolver { * to generate the import from. */ abstract getImportAs(symbol: StaticSymbol): StaticSymbol /*|null*/; + + /** + * Determine the airty of a type. + */ + abstract getTypeArity(symbol: StaticSymbol): number /*|null*/; } diff --git a/modules/@angular/compiler/src/output/ts_emitter.ts b/modules/@angular/compiler/src/output/ts_emitter.ts index 1cddf917eb..cfc6817ac7 100644 --- a/modules/@angular/compiler/src/output/ts_emitter.ts +++ b/modules/@angular/compiler/src/output/ts_emitter.ts @@ -21,7 +21,8 @@ export function debugOutputAstAsTypeScript(ast: o.Statement | o.Expression | o.T string { const converter = new _TsEmitterVisitor(_debugFilePath, { fileNameToModuleName(filePath: string, containingFilePath: string) { return filePath; }, - getImportAs(symbol: StaticSymbol) { return null; } + getImportAs(symbol: StaticSymbol) { return null; }, + getTypeArity: symbol => null }); const ctx = EmitterVisitorContext.createRoot([]); const asts: any[] = Array.isArray(ast) ? ast : [ast]; @@ -65,6 +66,8 @@ export class TypeScriptEmitter implements OutputEmitter { } class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor { + private typeExpression = 0; + constructor(private _genFilePath: string, private _importResolver: ImportResolver) { super(false); } @@ -74,7 +77,9 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor visitType(t: o.Type, ctx: EmitterVisitorContext, defaultType: string = 'any') { if (isPresent(t)) { + this.typeExpression++; t.visitType(this, ctx); + this.typeExpression--; } else { ctx.print(defaultType); } @@ -149,6 +154,17 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor return null; } + visitInstantiateExpr(ast: o.InstantiateExpr, ctx: EmitterVisitorContext): any { + ctx.print(`new `); + this.typeExpression++; + ast.classExpr.visitExpression(this, ctx); + this.typeExpression--; + ctx.print(`(`); + this.visitAllExpressions(ast.args, ctx, ','); + ctx.print(`)`); + return null; + } + visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any { ctx.pushClass(stmt); if (ctx.isExportedVar(stmt.name)) { @@ -157,7 +173,9 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor ctx.print(`class ${stmt.name}`); if (isPresent(stmt.parent)) { ctx.print(` extends `); + this.typeExpression++; stmt.parent.visitExpression(this, ctx); + this.typeExpression--; } ctx.println(` {`); ctx.incIndent(); @@ -299,11 +317,6 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor visitExpressionType(ast: o.ExpressionType, ctx: EmitterVisitorContext): any { ast.value.visitExpression(this, ctx); - if (isPresent(ast.typeParams) && ast.typeParams.length > 0) { - ctx.print(`<`); - this.visitAllObjects(type => type.visitType(this, ctx), ast.typeParams, ctx, ','); - ctx.print(`>`); - } return null; } @@ -346,17 +359,24 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor }, params, ctx, ','); } - private _resolveStaticSymbol(value: CompileIdentifierMetadata): StaticSymbol { + private _resolveStaticSymbol(value: CompileIdentifierMetadata): + {name: string, filePath: string, members?: string[], arity?: number} { const reference = value.reference; if (!(reference instanceof StaticSymbol)) { throw new Error(`Internal error: unknown identifier ${JSON.stringify(value)}`); } - return this._importResolver.getImportAs(reference) || reference; + const arity = this._importResolver.getTypeArity(reference) || undefined; + const importReference = this._importResolver.getImportAs(reference) || reference; + return { + name: importReference.name, + filePath: importReference.filePath, + members: importReference.members, arity + }; } private _visitIdentifier( value: CompileIdentifierMetadata, typeParams: o.Type[], ctx: EmitterVisitorContext): void { - const {name, filePath, members} = this._resolveStaticSymbol(value); + const {name, filePath, members, arity} = this._resolveStaticSymbol(value); if (filePath != this._genFilePath) { let prefix = this.importsWithPrefixes.get(filePath); if (isBlank(prefix)) { @@ -372,10 +392,28 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor } else { ctx.print(name); } - if (isPresent(typeParams) && typeParams.length > 0) { - ctx.print(`<`); - this.visitAllObjects(type => type.visitType(this, ctx), typeParams, ctx, ','); - ctx.print(`>`); + + if (this.typeExpression > 0) { + // If we are in a type expreession that refers to a generic type then supply + // the required type parameters. If there were not enough type parameters + // supplied, supply any as the type. Outside a type expression the reference + // should not supply type parameters and be treated as a simple value reference + // to the constructor function itself. + const suppliedParameters = (typeParams && typeParams.length) || 0; + const additionalParameters = (arity || 0) - suppliedParameters; + if (suppliedParameters > 0 || additionalParameters > 0) { + ctx.print(`<`); + if (suppliedParameters > 0) { + this.visitAllObjects(type => type.visitType(this, ctx), typeParams, ctx, ','); + } + if (additionalParameters > 0) { + for (let i = 0; i < additionalParameters; i++) { + if (i > 0 || suppliedParameters > 0) ctx.print(','); + ctx.print('any'); + } + } + ctx.print(`>`); + } } } } diff --git a/modules/@angular/compiler/test/output/js_emitter_spec.ts b/modules/@angular/compiler/test/output/js_emitter_spec.ts index 6d53cef55b..0afa6caaa5 100644 --- a/modules/@angular/compiler/test/output/js_emitter_spec.ts +++ b/modules/@angular/compiler/test/output/js_emitter_spec.ts @@ -27,6 +27,7 @@ class SimpleJsImportGenerator implements ImportResolver { return importedUrlStr; } getImportAs(symbol: StaticSymbol): StaticSymbol { return null; } + getTypeArity(symbol: StaticSymbol): number /*|null*/ { return null; } } export function main() { diff --git a/modules/@angular/compiler/test/output/output_emitter_util.ts b/modules/@angular/compiler/test/output/output_emitter_util.ts index 1a32940b59..bd30ef9380 100644 --- a/modules/@angular/compiler/test/output/output_emitter_util.ts +++ b/modules/@angular/compiler/test/output/output_emitter_util.ts @@ -264,4 +264,5 @@ export class SimpleJsImportGenerator implements ImportResolver { return importedUrlStr; } getImportAs(symbol: StaticSymbol): StaticSymbol { return null; } + getTypeArity(symbol: StaticSymbol): number /*|null*/ { return null; } } diff --git a/modules/@angular/compiler/test/output/ts_emitter_node_only_spec.ts b/modules/@angular/compiler/test/output/ts_emitter_node_only_spec.ts index af274a9457..b130871ddb 100644 --- a/modules/@angular/compiler/test/output/ts_emitter_node_only_spec.ts +++ b/modules/@angular/compiler/test/output/ts_emitter_node_only_spec.ts @@ -75,4 +75,5 @@ class StubReflectorHost implements StaticSymbolResolverHost { class StubImportResolver extends ImportResolver { fileNameToModuleName(importedFilePath: string, containingFilePath: string): string { return ''; } getImportAs(symbol: StaticSymbol): StaticSymbol { return null; } + getTypeArity(symbol: StaticSymbol): number /*|null*/ { return null; } } diff --git a/modules/@angular/compiler/test/output/ts_emitter_spec.ts b/modules/@angular/compiler/test/output/ts_emitter_spec.ts index 58506f5530..9cd0c5c322 100644 --- a/modules/@angular/compiler/test/output/ts_emitter_spec.ts +++ b/modules/@angular/compiler/test/output/ts_emitter_spec.ts @@ -28,6 +28,7 @@ class SimpleJsImportGenerator implements ImportResolver { return importedUrlStr; } getImportAs(symbol: StaticSymbol): StaticSymbol { return null; } + getTypeArity(symbol: StaticSymbol): number /*|null*/ { return null; } } export function main() { @@ -429,11 +430,19 @@ export function main() { }); it('should support expression types', () => { + expect( + emitStmt(o.variable('a').set(o.NULL_EXPR).toDeclStmt(o.expressionType(o.variable('b'))))) + .toEqual('var a:b = (null as any);'); + }); + + it('should support expressions with type parameters', () => { expect(emitStmt(o.variable('a') .set(o.NULL_EXPR) - .toDeclStmt(o.expressionType( - o.variable('b'), [o.expressionType(o.variable('c'))])))) - .toEqual('var a:b = (null as any);'); + .toDeclStmt(o.importType(externalModuleIdentifier, [o.STRING_TYPE])))) + .toEqual([ + `import * as import0 from 'somePackage/someOtherPath';`, + `var a:import0.someExternalId = (null as any);` + ].join('\n')); }); it('should support combined types', () => {