diff --git a/modules/@angular/compiler_cli/integrationtest/test/basic_spec.ts b/modules/@angular/compiler_cli/integrationtest/test/basic_spec.ts index eb775c3c19..546184a8b4 100644 --- a/modules/@angular/compiler_cli/integrationtest/test/basic_spec.ts +++ b/modules/@angular/compiler_cli/integrationtest/test/basic_spec.ts @@ -27,7 +27,7 @@ describe("template codegen output", () => { expect(fs.existsSync(metadataOutput)).toBeTruthy(); const output = fs.readFileSync(metadataOutput, {encoding: 'utf-8'}); expect(output).toContain('"decorators":'); - expect(output).toContain('"name":"Component","module":"@angular/core"'); + expect(output).toContain('"module":"@angular/core","name":"Component"'); }); it("should write .d.ts files", () => { diff --git a/modules/@angular/compiler_cli/src/codegen.ts b/modules/@angular/compiler_cli/src/codegen.ts index 9e2c9e927d..1fc62d1ea0 100644 --- a/modules/@angular/compiler_cli/src/codegen.ts +++ b/modules/@angular/compiler_cli/src/codegen.ts @@ -62,17 +62,21 @@ export class CodeGenerator { private readComponents(absSourcePath: string) { const result: Promise[] = []; - const metadata = this.staticReflector.getModuleMetadata(absSourcePath); - if (!metadata) { + const moduleMetadata = this.staticReflector.getModuleMetadata(absSourcePath); + if (!moduleMetadata) { console.log(`WARNING: no metadata found for ${absSourcePath}`); return result; } - - const symbols = Object.keys(metadata['metadata']); + const metadata = moduleMetadata['metadata']; + const symbols = metadata && Object.keys(metadata); if (!symbols || !symbols.length) { return result; } for (const symbol of symbols) { + if (metadata[symbol] && metadata[symbol].__symbolic == 'error') { + // Ignore symbols that are only included to record error information. + continue; + } const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath); let directive: compiler.CompileDirectiveMetadata; directive = this.resolver.maybeGetDirectiveMetadata(staticType); diff --git a/modules/@angular/compiler_cli/src/reflector_host.ts b/modules/@angular/compiler_cli/src/reflector_host.ts index c85b255382..e40dbd571a 100644 --- a/modules/@angular/compiler_cli/src/reflector_host.ts +++ b/modules/@angular/compiler_cli/src/reflector_host.ts @@ -153,7 +153,7 @@ export class NodeReflectorHost implements StaticReflectorHost, ImportGenerator { if (!sf) { throw new Error(`Source file ${filePath} not present in program.`); } - const metadata = this.metadataCollector.getMetadata(sf, this.program.getTypeChecker()); + const metadata = this.metadataCollector.getMetadata(sf); return metadata; } diff --git a/modules/@angular/compiler_cli/src/static_reflector.ts b/modules/@angular/compiler_cli/src/static_reflector.ts index 114ee8e835..df1a4a17c9 100644 --- a/modules/@angular/compiler_cli/src/static_reflector.ts +++ b/modules/@angular/compiler_cli/src/static_reflector.ts @@ -359,6 +359,8 @@ export class StaticReflector implements ReflectorReader { } else { return context; } + case "error": + throw new Error(expression['message']); } return null; } diff --git a/tools/@angular/tsc-wrapped/src/collector.ts b/tools/@angular/tsc-wrapped/src/collector.ts index 7cdaeb57f5..a74c52e756 100644 --- a/tools/@angular/tsc-wrapped/src/collector.ts +++ b/tools/@angular/tsc-wrapped/src/collector.ts @@ -1,81 +1,28 @@ import * as ts from 'typescript'; -import {Evaluator, ImportMetadata, ImportSpecifierMetadata} from './evaluator'; -import {ClassMetadata, ConstructorMetadata, ModuleMetadata, MemberMetadata, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata} from './schema'; +import {Evaluator, ImportMetadata, ImportSpecifierMetadata, isPrimitive} from './evaluator'; +import {ClassMetadata, ConstructorMetadata, ModuleMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, isMetadataError, isMetadataSymbolicReferenceExpression,} from './schema'; import {Symbols} from './symbols'; - /** * Collect decorator metadata from a TypeScript module. */ export class MetadataCollector { constructor() {} - collectImports(sourceFile: ts.SourceFile) { - let imports: ImportMetadata[] = []; - const stripQuotes = (s: string) => s.replace(/^['"]|['"]$/g, ''); - function visit(node: ts.Node) { - switch (node.kind) { - case ts.SyntaxKind.ImportDeclaration: - const importDecl = node; - const from = stripQuotes(importDecl.moduleSpecifier.getText()); - const newImport = {from}; - if (!importDecl.importClause) { - // Bare imports do not bring symbols into scope, so we don't need to record them - break; - } - if (importDecl.importClause.name) { - (newImport)['defaultName'] = importDecl.importClause.name.text; - } - const bindings = importDecl.importClause.namedBindings; - if (bindings) { - switch (bindings.kind) { - case ts.SyntaxKind.NamedImports: - const namedImports: ImportSpecifierMetadata[] = []; - (bindings).elements.forEach(i => { - const namedImport = {name: i.name.text}; - if (i.propertyName) { - (namedImport)['propertyName'] = i.propertyName.text; - } - namedImports.push(namedImport); - }); - (newImport)['namedImports'] = namedImports; - break; - case ts.SyntaxKind.NamespaceImport: - (newImport)['namespace'] = (bindings).name.text; - break; - } - } - imports.push(newImport); - break; - } - ts.forEachChild(node, visit); - } - ts.forEachChild(sourceFile, visit); - return imports; - } - /** * Returns a JSON.stringify friendly form describing the decorators of the exported classes from * the source file that is expected to correspond to a module. */ - public getMetadata(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker): ModuleMetadata { - const locals = new Symbols(); - const evaluator = new Evaluator(typeChecker, locals, this.collectImports(sourceFile)); + public getMetadata(sourceFile: ts.SourceFile): ModuleMetadata { + const locals = new Symbols(sourceFile); + const evaluator = new Evaluator(locals); + let metadata: {[name: string]: MetadataValue | ClassMetadata}|undefined; function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { return evaluator.evaluateNode(decoratorNode.expression); } - function referenceFromType(type: ts.Type): MetadataSymbolicReferenceExpression { - if (type) { - let symbol = type.getSymbol(); - if (symbol) { - return evaluator.symbolReference(symbol); - } - } - } - function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata { let result: ClassMetadata = {__symbolic: 'class'}; @@ -85,6 +32,15 @@ export class MetadataCollector { return undefined; } + function referenceFrom(node: ts.Node): MetadataSymbolicReferenceExpression|MetadataError { + const result = evaluator.evaluateNode(node); + if (isMetadataError(result) || isMetadataSymbolicReferenceExpression(result)) { + return result; + } else { + return {__symbolic: 'error', message: 'Symbol reference expected'}; + } + } + // Add class decorators if (classDeclaration.decorators) { result.decorators = getDecorators(classDeclaration.decorators); @@ -108,8 +64,9 @@ export class MetadataCollector { const method = member; const methodDecorators = getDecorators(method.decorators); const parameters = method.parameters; - const parameterDecoratorData: MetadataSymbolicExpression[][] = []; - const parametersData: MetadataSymbolicReferenceExpression[] = []; + const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = []; + const parametersData: (MetadataSymbolicReferenceExpression | MetadataError | null)[] = + []; let hasDecoratorData: boolean = false; let hasParameterData: boolean = false; for (const parameter of parameters) { @@ -117,8 +74,11 @@ export class MetadataCollector { parameterDecoratorData.push(parameterData); hasDecoratorData = hasDecoratorData || !!parameterData; if (isConstructor) { - const parameterType = typeChecker.getTypeAtLocation(parameter); - parametersData.push(referenceFromType(parameterType) || null); + if (parameter.type) { + parametersData.push(referenceFrom(parameter.type)); + } else { + parametersData.push(null); + } hasParameterData = true; } } @@ -133,7 +93,9 @@ export class MetadataCollector { if (hasParameterData) { (data).parameters = parametersData; } - recordMember(name, data); + if (!isMetadataError(name)) { + recordMember(name, data); + } break; case ts.SyntaxKind.PropertyDeclaration: case ts.SyntaxKind.GetAccessor: @@ -141,9 +103,10 @@ export class MetadataCollector { const property = member; const propertyDecorators = getDecorators(property.decorators); if (propertyDecorators) { - recordMember( - evaluator.nameOf(property.name), - {__symbolic: 'property', decorators: propertyDecorators}); + let name = evaluator.nameOf(property.name); + if (!isMetadataError(name)) { + recordMember(name, {__symbolic: 'property', decorators: propertyDecorators}); + } } break; } @@ -155,36 +118,94 @@ export class MetadataCollector { return result.decorators || members ? result : undefined; } - let metadata: {[name: string]: (ClassMetadata | MetadataValue)}; - const symbols = typeChecker.getSymbolsInScope(sourceFile, ts.SymbolFlags.ExportValue); - for (var symbol of symbols) { - for (var declaration of symbol.getDeclarations()) { - switch (declaration.kind) { - case ts.SyntaxKind.ClassDeclaration: - const classDeclaration = declaration; + // Predeclare classes + ts.forEachChild(sourceFile, node => { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + const classDeclaration = node; + const className = classDeclaration.name.text; + if (node.flags & ts.NodeFlags.Export) { + locals.define(className, {__symbolic: 'reference', name: className}); + } else { + locals.define( + className, + {__symbolic: 'error', message: `Reference to non-exported class ${className}`}); + } + break; + } + }); + ts.forEachChild(sourceFile, node => { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + const classDeclaration = node; + const className = classDeclaration.name.text; + if (node.flags & ts.NodeFlags.Export) { if (classDeclaration.decorators) { if (!metadata) metadata = {}; - metadata[classDeclaration.name.text] = classMetadataOf(classDeclaration); + metadata[className] = classMetadataOf(classDeclaration); } - break; - case ts.SyntaxKind.VariableDeclaration: - const variableDeclaration = declaration; - if (variableDeclaration.initializer) { - const value = evaluator.evaluateNode(variableDeclaration.initializer); - if (value !== undefined) { - if (evaluator.isFoldable(variableDeclaration.initializer)) { - // Record the value for use in other initializers - locals.set(symbol, value); - } - if (!metadata) metadata = {}; - metadata[evaluator.nameOf(variableDeclaration.name)] = - evaluator.evaluateNode(variableDeclaration.initializer); + } + // Otherwise don't record metadata for the class. + break; + case ts.SyntaxKind.VariableStatement: + const variableStatement = node; + for (let variableDeclaration of variableStatement.declarationList.declarations) { + if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier) { + let nameNode = variableDeclaration.name; + let varValue: MetadataValue; + if (variableDeclaration.initializer) { + varValue = evaluator.evaluateNode(variableDeclaration.initializer); + } else { + varValue = { + __symbolic: 'error', + message: 'Only intialized variables and constants can be referenced statically' + }; } + if (variableStatement.flags & ts.NodeFlags.Export || + variableDeclaration.flags & ts.NodeFlags.Export) { + if (!metadata) metadata = {}; + metadata[nameNode.text] = varValue; + } + if (isPrimitive(varValue)) { + locals.define(nameNode.text, varValue); + } + } else { + // Destructuring (or binding) declarations are not supported, + // var {[, ]+} = ; + // or + // var [[, ; + // are not supported. + let varValue = { + __symbolc: 'error', + message: 'Destructuring declarations cannot be referenced statically' + }; + const report = (nameNode: ts.Node) => { + switch (nameNode.kind) { + case ts.SyntaxKind.Identifier: + const name = nameNode; + locals.define(name.text, varValue); + if (node.flags & ts.NodeFlags.Export) { + if (!metadata) metadata = {}; + metadata[name.text] = varValue; + } + break; + case ts.SyntaxKind.BindingElement: + const bindingElement = nameNode; + report(bindingElement.name); + break; + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ArrayBindingPattern: + const bindings = nameNode; + bindings.elements.forEach(report); + break; + } + }; + report(variableDeclaration.name); } - break; - } + } + break; } - } + }); return metadata && {__symbolic: 'module', metadata}; } diff --git a/tools/@angular/tsc-wrapped/src/compiler_host.ts b/tools/@angular/tsc-wrapped/src/compiler_host.ts index 31a6fbe12d..4167b6502d 100644 --- a/tools/@angular/tsc-wrapped/src/compiler_host.ts +++ b/tools/@angular/tsc-wrapped/src/compiler_host.ts @@ -67,8 +67,7 @@ export class MetadataWriterHost extends DelegatingHost { // released if (/*DTS*/ /\.js$/.test(emitFilePath)) { const path = emitFilePath.replace(/*DTS*/ /\.js$/, '.metadata.json'); - const metadata = - this.metadataCollector.getMetadata(sourceFile, this.program.getTypeChecker()); + const metadata = this.metadataCollector.getMetadata(sourceFile); if (metadata && metadata.metadata) { const metadataText = JSON.stringify(metadata); writeFileSync(path, metadataText, {encoding: 'utf-8'}); diff --git a/tools/@angular/tsc-wrapped/src/evaluator.ts b/tools/@angular/tsc-wrapped/src/evaluator.ts index 1368e66880..28dad992a9 100644 --- a/tools/@angular/tsc-wrapped/src/evaluator.ts +++ b/tools/@angular/tsc-wrapped/src/evaluator.ts @@ -1,21 +1,8 @@ import * as ts from 'typescript'; -import {MetadataValue, MetadataSymbolicCallExpression, MetadataSymbolicReferenceExpression} from './schema'; +import {MetadataValue, MetadataSymbolicCallExpression, MetadataSymbolicReferenceExpression, MetadataError, isMetadataError, isMetadataModuleReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataGlobalReferenceExpression,} from './schema'; import {Symbols} from './symbols'; - -// TOOD: Remove when tools directory is upgraded to support es6 target -interface Map { - has(k: K): boolean; - set(k: K, v: V): void; - get(k: K): V; - delete (k: K): void; -} -interface MapConstructor { - new(): Map; -} -declare var Map: MapConstructor; - function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean { const expression = callExpression.expression; if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) { @@ -46,7 +33,7 @@ function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) { return !ts.forEachChild(node, node => !cb(node)); } -function isPrimitive(value: any): boolean { +export function isPrimitive(value: any): boolean { return Object(value) !== value; } @@ -72,63 +59,21 @@ export interface ImportMetadata { * possible. */ export class Evaluator { - constructor( - private typeChecker: ts.TypeChecker, private symbols: Symbols, - private imports: ImportMetadata[]) {} + constructor(private symbols: Symbols) {} - symbolReference(symbol: ts.Symbol): MetadataSymbolicReferenceExpression { - if (symbol) { - let module: string; - let name = symbol.name; - for (const eachImport of this.imports) { - if (symbol.name === eachImport.defaultName) { - module = eachImport.from; - name = undefined; - } - if (eachImport.namedImports) { - for (const named of eachImport.namedImports) { - if (symbol.name === named.name) { - name = named.propertyName ? named.propertyName : named.name; - module = eachImport.from; - break; - } - } - } - } - return {__symbolic: 'reference', name, module}; - } - } - - private findImportNamespace(node: ts.Node) { - if (node.kind === ts.SyntaxKind.PropertyAccessExpression) { - const lhs = (node).expression; - if (lhs.kind === ts.SyntaxKind.Identifier) { - // TOOD: Use Array.find when tools directory is upgraded to support es6 target - for (const eachImport of this.imports) { - if (eachImport.namespace === (lhs).text) { - return eachImport; - } - } - } - } - } - - private nodeSymbolReference(node: ts.Node): MetadataSymbolicReferenceExpression { - const importNamespace = this.findImportNamespace(node); - if (importNamespace) { - const result = this.symbolReference( - this.typeChecker.getSymbolAtLocation((node).name)); - result.module = importNamespace.from; - return result; - } - return this.symbolReference(this.typeChecker.getSymbolAtLocation(node)); - } - - nameOf(node: ts.Node): string { + nameOf(node: ts.Node): string|MetadataError { if (node.kind == ts.SyntaxKind.Identifier) { return (node).text; } - return this.evaluateNode(node); + const result = this.evaluateNode(node); + if (isMetadataError(result) || typeof result === 'string') { + return result; + } else { + return { + __symbolic: 'error', + message: `Name expected a string or an identifier but received "${node.getText()}""` + }; + } } /** @@ -214,25 +159,10 @@ export class Evaluator { return this.isFoldableWorker(elementAccessExpression.expression, folding) && this.isFoldableWorker(elementAccessExpression.argumentExpression, folding); case ts.SyntaxKind.Identifier: - let symbol = this.typeChecker.getSymbolAtLocation(node); - if (symbol.flags & ts.SymbolFlags.Alias) { - symbol = this.typeChecker.getAliasedSymbol(symbol); - } - if (this.symbols.has(symbol)) return true; - - // If this is a reference to a foldable variable then it is foldable too. - const variableDeclaration = ( - symbol.declarations && symbol.declarations.length && symbol.declarations[0]); - if (variableDeclaration.kind === ts.SyntaxKind.VariableDeclaration) { - const initializer = variableDeclaration.initializer; - if (folding.has(initializer)) { - // A recursive reference is not foldable. - return false; - } - folding.set(initializer, true); - const result = this.isFoldableWorker(initializer, folding); - folding.delete(initializer); - return result; + let identifier = node; + let reference = this.symbols.resolve(identifier.text); + if (isPrimitive(reference)) { + return true; } break; } @@ -245,46 +175,43 @@ export class Evaluator { * tree are folded. For example, a node representing `1 + 2` is folded into `3`. */ public evaluateNode(node: ts.Node): MetadataValue { + let error: MetadataError|undefined; switch (node.kind) { case ts.SyntaxKind.ObjectLiteralExpression: - let obj: MetadataValue = {}; - let allPropertiesDefined = true; + let obj: {[name: string]: any} = {}; ts.forEachChild(node, child => { switch (child.kind) { case ts.SyntaxKind.PropertyAssignment: const assignment = child; const propertyName = this.nameOf(assignment.name); + if (isMetadataError(propertyName)) { + return propertyName; + } const propertyValue = this.evaluateNode(assignment.initializer); - (obj)[propertyName] = propertyValue; - allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined; + if (isMetadataError(propertyValue)) { + error = propertyValue; + return true; // Stop the forEachChild. + } else { + obj[propertyName] = propertyValue; + } } }); - if (allPropertiesDefined) return obj; - break; + if (error) return error; + return obj; case ts.SyntaxKind.ArrayLiteralExpression: let arr: MetadataValue[] = []; - let allElementsDefined = true; ts.forEachChild(node, child => { const value = this.evaluateNode(child); + if (isMetadataError(value)) { + error = value; + return true; // Stop the forEachChild. + } arr.push(value); - allElementsDefined = isDefined(value) && allElementsDefined; }); - if (allElementsDefined) return arr; - break; + if (error) return error; + return arr; case ts.SyntaxKind.CallExpression: const callExpression = node; - const args = callExpression.arguments.map(arg => this.evaluateNode(arg)); - if (this.isFoldable(callExpression)) { - if (isMethodCallOf(callExpression, 'concat')) { - const arrayValue = this.evaluateNode( - (callExpression.expression).expression); - return arrayValue.concat(args[0]); - } - } - // Always fold a CONST_EXPR even if the argument is not foldable. - if (isCallOf(callExpression, 'CONST_EXPR') && callExpression.arguments.length === 1) { - return args[0]; - } if (isCallOf(callExpression, 'forwardRef') && callExpression.arguments.length === 1) { const firstArgument = callExpression.arguments[0]; if (firstArgument.kind == ts.SyntaxKind.ArrowFunction) { @@ -292,72 +219,136 @@ export class Evaluator { return this.evaluateNode(arrowFunction.body); } } - const expression = this.evaluateNode(callExpression.expression); - if (isDefined(expression) && args.every(isDefined)) { - const result: - MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression}; - if (args && args.length) { - result.arguments = args; - } - return result; + const args = callExpression.arguments.map(arg => this.evaluateNode(arg)); + if (args.some(isMetadataError)) { + return args.find(isMetadataError); } - break; + if (this.isFoldable(callExpression)) { + if (isMethodCallOf(callExpression, 'concat')) { + const arrayValue = this.evaluateNode( + (callExpression.expression).expression); + if (isMetadataError(arrayValue)) return arrayValue; + return arrayValue.concat(args[0]); + } + } + // Always fold a CONST_EXPR even if the argument is not foldable. + if (isCallOf(callExpression, 'CONST_EXPR') && callExpression.arguments.length === 1) { + return args[0]; + } + const expression = this.evaluateNode(callExpression.expression); + if (isMetadataError(expression)) { + return expression; + } + let result: MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression}; + if (args && args.length) { + result.arguments = args; + } + return result; case ts.SyntaxKind.NewExpression: const newExpression = node; const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg)); - const newTarget = this.evaluateNode(newExpression.expression); - if (isDefined(newTarget) && newArgs.every(isDefined)) { - const result: MetadataSymbolicCallExpression = {__symbolic: 'new', expression: newTarget}; - if (newArgs.length) { - result.arguments = newArgs; - } - return result; + if (newArgs.some(isMetadataError)) { + return newArgs.find(isMetadataError); } - break; + const newTarget = this.evaluateNode(newExpression.expression); + if (isMetadataError(newTarget)) { + return newTarget; + } + const call: MetadataSymbolicCallExpression = {__symbolic: 'new', expression: newTarget}; + if (newArgs.length) { + call.arguments = newArgs; + } + return call; case ts.SyntaxKind.PropertyAccessExpression: { const propertyAccessExpression = node; const expression = this.evaluateNode(propertyAccessExpression.expression); + if (isMetadataError(expression)) { + return expression; + } const member = this.nameOf(propertyAccessExpression.name); - if (this.isFoldable(propertyAccessExpression.expression)) return (expression)[member]; - if (this.findImportNamespace(propertyAccessExpression)) { - return this.nodeSymbolReference(propertyAccessExpression); + if (isMetadataError(member)) { + return member; } - if (isDefined(expression)) { - return {__symbolic: 'select', expression, member}; + if (this.isFoldable(propertyAccessExpression.expression)) + return (expression)[member]; + if (isMetadataModuleReferenceExpression(expression)) { + // A select into a module refrence and be converted into a reference to the symbol + // in the module + return {__symbolic: 'reference', module: expression.module, name: member}; } - break; + return {__symbolic: 'select', expression, member}; } case ts.SyntaxKind.ElementAccessExpression: { const elementAccessExpression = node; const expression = this.evaluateNode(elementAccessExpression.expression); + if (isMetadataError(expression)) { + return expression; + } const index = this.evaluateNode(elementAccessExpression.argumentExpression); + if (isMetadataError(expression)) { + return expression; + } if (this.isFoldable(elementAccessExpression.expression) && this.isFoldable(elementAccessExpression.argumentExpression)) return (expression)[index]; - if (isDefined(expression) && isDefined(index)) { - return {__symbolic: 'index', expression, index}; - } - break; + return {__symbolic: 'index', expression, index}; } case ts.SyntaxKind.Identifier: - let symbol = this.typeChecker.getSymbolAtLocation(node); - if (symbol.flags & ts.SymbolFlags.Alias) { - symbol = this.typeChecker.getAliasedSymbol(symbol); + const identifier = node; + const name = identifier.text; + const reference = this.symbols.resolve(name); + if (reference === undefined) { + // Encode as a global reference. StaticReflector will check the reference. + return { __symbolic: 'reference', name } } - if (this.symbols.has(symbol)) return this.symbols.get(symbol); - if (this.isFoldable(node)) { - // isFoldable implies, in this context, symbol declaration is a VariableDeclaration - const variableDeclaration = ( - symbol.declarations && symbol.declarations.length && symbol.declarations[0]); - return this.evaluateNode(variableDeclaration.initializer); + return reference; + case ts.SyntaxKind.TypeReference: + const typeReferenceNode = node; + const typeNameNode = typeReferenceNode.typeName; + if (typeNameNode.kind != ts.SyntaxKind.Identifier) { + return { __symbolic: 'error', message: 'Qualified type names not supported' } } - return this.nodeSymbolReference(node); + const typeNameIdentifier = typeReferenceNode.typeName; + const typeName = typeNameIdentifier.text; + const typeReference = this.symbols.resolve(typeName); + if (!typeReference) { + return {__symbolic: 'error', message: `Could not resolve type ${typeName}`}; + } + if (typeReferenceNode.typeArguments && typeReferenceNode.typeArguments.length) { + const args = typeReferenceNode.typeArguments.map(element => this.evaluateNode(element)); + if (isMetadataImportedSymbolReferenceExpression(typeReference)) { + return { + __symbolic: 'reference', + module: typeReference.module, + name: typeReference.name, + arguments: args + }; + } else if (isMetadataGlobalReferenceExpression(typeReference)) { + return {__symbolic: 'reference', name: typeReference.name, arguments: args}; + } + } + return typeReference; case ts.SyntaxKind.NoSubstitutionTemplateLiteral: return (node).text; case ts.SyntaxKind.StringLiteral: return (node).text; case ts.SyntaxKind.NumericLiteral: return parseFloat((node).text); + case ts.SyntaxKind.AnyKeyword: + return {__symbolic: 'reference', name: 'any'}; + case ts.SyntaxKind.StringKeyword: + return {__symbolic: 'reference', name: 'string'}; + case ts.SyntaxKind.NumberKeyword: + return {__symbolic: 'reference', name: 'number'}; + case ts.SyntaxKind.BooleanKeyword: + return {__symbolic: 'reference', name: 'boolean'}; + case ts.SyntaxKind.ArrayType: + const arrayTypeNode = node; + return { + __symbolic: 'reference', + name: 'Array', + arguments: [this.evaluateNode(arrayTypeNode.elementType)] + }; case ts.SyntaxKind.NullKeyword: return null; case ts.SyntaxKind.TrueKeyword: @@ -462,6 +453,6 @@ export class Evaluator { } break; } - return undefined; + return {__symbolic: 'error', message: 'Expression is too complex to resolve statically'}; } } diff --git a/tools/@angular/tsc-wrapped/src/schema.ts b/tools/@angular/tsc-wrapped/src/schema.ts index 10f2cdc65b..a40a7ebbb3 100644 --- a/tools/@angular/tsc-wrapped/src/schema.ts +++ b/tools/@angular/tsc-wrapped/src/schema.ts @@ -8,7 +8,7 @@ export function isModuleMetadata(value: any): value is ModuleMetadata { export interface ClassMetadata { __symbolic: 'class'; - decorators?: MetadataSymbolicExpression[]; + decorators?: (MetadataSymbolicExpression|MetadataError)[]; members?: MetadataMap; } export function isClassMetadata(value: any): value is ClassMetadata { @@ -19,7 +19,7 @@ export interface MetadataMap { [name: string]: MemberMetadata[]; } export interface MemberMetadata { __symbolic: 'constructor'|'method'|'property'; - decorators?: MetadataSymbolicExpression[]; + decorators?: (MetadataSymbolicExpression|MetadataError)[]; } export function isMemberMetadata(value: any): value is MemberMetadata { if (value) { @@ -35,7 +35,7 @@ export function isMemberMetadata(value: any): value is MemberMetadata { export interface MethodMetadata extends MemberMetadata { __symbolic: 'constructor'|'method'; - parameterDecorators?: MetadataSymbolicExpression[][]; + parameterDecorators?: (MetadataSymbolicExpression|MetadataError)[][]; } export function isMethodMetadata(value: any): value is MemberMetadata { return value && (value.__symbolic === 'constructor' || value.__symbolic === 'method'); @@ -43,14 +43,14 @@ export function isMethodMetadata(value: any): value is MemberMetadata { export interface ConstructorMetadata extends MethodMetadata { __symbolic: 'constructor'; - parameters?: MetadataSymbolicExpression[]; + parameters?: (MetadataSymbolicExpression|MetadataError|null)[]; } export function isConstructorMetadata(value: any): value is ConstructorMetadata { return value && value.__symbolic === 'constructor'; } -export type MetadataValue = - string | number | boolean | MetadataObject | MetadataArray | MetadataSymbolicExpression; +export type MetadataValue = string | number | boolean | MetadataObject | MetadataArray | + MetadataSymbolicExpression | MetadataError; export interface MetadataObject { [name: string]: MetadataValue; } @@ -117,11 +117,51 @@ export function isMetadataSymbolicPrefixExpression(value: any): return value && value.__symbolic === 'pre'; } -export interface MetadataSymbolicReferenceExpression extends MetadataSymbolicExpression { +export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression { __symbolic: 'reference'; name: string; + arguments?: MetadataValue[]; +} +export function isMetadataGlobalReferenceExpression(value: any): + value is MetadataGlobalReferenceExpression { + return isMetadataSymbolicReferenceExpression(value) && value.name && !value.module; +} + +export interface MetadataModuleReferenceExpression extends MetadataSymbolicExpression { + __symbolic: 'reference'; module: string; } +export function isMetadataModuleReferenceExpression(value: any): + value is MetadataModuleReferenceExpression { + return isMetadataSymbolicReferenceExpression(value) && value.module && !value.name && + !value.default; +} + +export interface MetadataImportedSymbolReferenceExpression extends MetadataSymbolicExpression { + __symbolic: 'reference'; + module: string; + name: string; + arguments?: MetadataValue[]; +} +export function isMetadataImportedSymbolReferenceExpression(value: any): + value is MetadataImportedSymbolReferenceExpression { + return isMetadataSymbolicReferenceExpression(value) && value.module && !!value.name; +} + +export interface MetadataImportedDefaultReferenceExpression extends MetadataSymbolicExpression { + __symbolic: 'reference'; + module: string; + default: + boolean; +} +export function isMetadataImportDefaultReference(value: any): + value is MetadataImportedDefaultReferenceExpression { + return isMetadataSymbolicReferenceExpression(value) && value.module && value.default; +} + +export type MetadataSymbolicReferenceExpression = MetadataGlobalReferenceExpression | + MetadataModuleReferenceExpression | MetadataImportedSymbolReferenceExpression | + MetadataImportedDefaultReferenceExpression; export function isMetadataSymbolicReferenceExpression(value: any): value is MetadataSymbolicReferenceExpression { return value && value.__symbolic === 'reference'; @@ -136,3 +176,11 @@ export function isMetadataSymbolicSelectExpression(value: any): value is MetadataSymbolicSelectExpression { return value && value.__symbolic === 'select'; } + +export interface MetadataError { + __symbolic: 'error'; + message: string; +} +export function isMetadataError(value: any): value is MetadataError { + return value && value.__symbolic === 'error'; +} \ No newline at end of file diff --git a/tools/@angular/tsc-wrapped/src/symbols.ts b/tools/@angular/tsc-wrapped/src/symbols.ts index f29a906b5a..a0ce8fbbda 100644 --- a/tools/@angular/tsc-wrapped/src/symbols.ts +++ b/tools/@angular/tsc-wrapped/src/symbols.ts @@ -1,36 +1,99 @@ import * as ts from 'typescript'; -// TOOD: Remove when tools directory is upgraded to support es6 target -interface Map { - has(v: V): boolean; - set(k: K, v: V): void; - get(k: K): V; -} -interface MapConstructor { - new(): Map; -} -declare var Map: MapConstructor; +import {MetadataValue} from './schema'; -var a: Array; - -/** - * A symbol table of ts.Symbol to a folded value used during expression folding in Evaluator. - * - * This is a thin wrapper around a Map<> using the first declaration location instead of the symbol - * itself as the key. In the TypeScript binder and type checker, mulitple symbols are sometimes - * created for a symbol depending on what scope it is in (e.g. export vs. local). Using the - * declaration node as the key results in these duplicate symbols being treated as identical. - */ export class Symbols { - private map = new Map(); + private _symbols: Map; - public has(symbol: ts.Symbol): boolean { return this.map.has(symbol.getDeclarations()[0]); } + constructor(private sourceFile: ts.SourceFile) {} - public set(symbol: ts.Symbol, value: any): void { - this.map.set(symbol.getDeclarations()[0], value); + resolve(name: string): MetadataValue|undefined { return this.symbols.get(name); } + + define(name: string, value: MetadataValue) { this.symbols.set(name, value); } + + has(name: string): boolean { return this.symbols.has(name); } + + private get symbols(): Map { + let result = this._symbols; + if (!result) { + result = this._symbols = new Map(); + populateBuiltins(result); + this.buildImports(); + } + return result; } - public get(symbol: ts.Symbol): any { return this.map.get(symbol.getDeclarations()[0]); } - - static empty: Symbols = new Symbols(); + private buildImports(): void { + let symbols = this._symbols; + // Collect the imported symbols into this.symbols + const stripQuotes = (s: string) => s.replace(/^['"]|['"]$/g, ''); + const visit = (node: ts.Node) => { + switch (node.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: + const importEqualsDeclaration = node; + if (importEqualsDeclaration.moduleReference.kind === + ts.SyntaxKind.ExternalModuleReference) { + const externalReference = + importEqualsDeclaration.moduleReference; + // An `import = require(); + const from = stripQuotes(externalReference.expression.getText()); + symbols.set(importEqualsDeclaration.name.text, {__symbolic: 'reference', module: from}); + } else { + symbols.set( + importEqualsDeclaration.name.text, + {__symbolic: 'error', message: `Unsupported import syntax`}); + } + break; + case ts.SyntaxKind.ImportDeclaration: + const importDecl = node; + if (!importDecl.importClause) { + // An `import ` clause which does not bring symbols into scope. + break; + } + const from = stripQuotes(importDecl.moduleSpecifier.getText()); + if (importDecl.importClause.name) { + // An `import form ` clause. Record the defualt symbol. + symbols.set( + importDecl.importClause.name.text, + {__symbolic: 'reference', module: from, default: true}); + } + const bindings = importDecl.importClause.namedBindings; + if (bindings) { + switch (bindings.kind) { + case ts.SyntaxKind.NamedImports: + // An `import { [ [, ] } from ` clause + for (let binding of (bindings).elements) { + symbols.set(binding.name.text, { + __symbolic: 'reference', + module: from, + name: binding.propertyName ? binding.propertyName.text : binding.name.text + }); + } + break; + case ts.SyntaxKind.NamespaceImport: + // An `input * as from ` clause. + symbols.set( + (bindings).name.text, + {__symbolic: 'reference', module: from}); + break; + } + } + break; + } + ts.forEachChild(node, visit); + }; + if (this.sourceFile) { + ts.forEachChild(this.sourceFile, visit); + } + } } + +function populateBuiltins(symbols: Map) { + // From lib.core.d.ts (all "define const") + ['Object', 'Function', 'String', 'Number', 'Array', 'Boolean', 'Map', 'NaN', 'Infinity', 'Math', + 'Date', 'RegExp', 'Error', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', + 'TypeError', 'URIError', 'JSON', 'ArrayBuffer', 'DataView', 'Int8Array', 'Uint8Array', + 'Uint8ClampedArray', 'Uint16Array', 'Int16Array', 'Int32Array', 'Uint32Array', 'Float32Array', + 'Float64Array'] + .forEach(name => symbols.set(name, {__symbolic: 'reference', name})); +} \ No newline at end of file diff --git a/tools/@angular/tsc-wrapped/test/collector.spec.ts b/tools/@angular/tsc-wrapped/test/collector.spec.ts index d852984456..dc7ab3d1fa 100644 --- a/tools/@angular/tsc-wrapped/test/collector.spec.ts +++ b/tools/@angular/tsc-wrapped/test/collector.spec.ts @@ -1,6 +1,6 @@ import * as ts from 'typescript'; import {MetadataCollector} from '../src/collector'; -import {ClassMetadata, ModuleMetadata} from '../src/schema'; +import {ClassMetadata, ConstructorMetadata, ModuleMetadata} from '../src/schema'; import {Directory, expectValidSources, Host} from './typescript.mocks'; @@ -8,16 +8,15 @@ describe('Collector', () => { let host: ts.LanguageServiceHost; let service: ts.LanguageService; let program: ts.Program; - let typeChecker: ts.TypeChecker; let collector: MetadataCollector; beforeEach(() => { - host = new Host( - FILES, - ['/app/app.component.ts', '/app/cases-data.ts', '/app/cases-no-data.ts', '/promise.ts']); + host = new Host(FILES, [ + '/app/app.component.ts', '/app/cases-data.ts', '/app/error-cases.ts', '/promise.ts', + '/unsupported-1.ts', '/unsupported-2.ts' + ]); service = ts.createLanguageService(host); program = service.getProgram(); - typeChecker = program.getTypeChecker(); collector = new MetadataCollector(); }); @@ -25,27 +24,13 @@ describe('Collector', () => { it('should return undefined for modules that have no metadata', () => { const sourceFile = program.getSourceFile('app/hero.ts'); - const metadata = collector.getMetadata(sourceFile, typeChecker); + const metadata = collector.getMetadata(sourceFile); expect(metadata).toBeUndefined(); }); - it('should be able to collect import statements', () => { - const sourceFile = program.getSourceFile('app/app.component.ts'); - expect(collector.collectImports(sourceFile)).toEqual([ - { - from: 'angular2/core', - namedImports: [{name: 'MyComponent', propertyName: 'Component'}, {name: 'OnInit'}] - }, - {from: 'angular2/common', namespace: 'common'}, - {from: './hero', namedImports: [{name: 'Hero'}]}, - {from: './hero-detail.component', namedImports: [{name: 'HeroDetailComponent'}]}, - {from: './hero.service', defaultName: 'HeroService'} - ]); - }); - it('should be able to collect a simple component\'s metadata', () => { const sourceFile = program.getSourceFile('app/hero-detail.component.ts'); - const metadata = collector.getMetadata(sourceFile, typeChecker); + const metadata = collector.getMetadata(sourceFile); expect(metadata).toEqual({ __symbolic: 'module', metadata: { @@ -53,7 +38,7 @@ describe('Collector', () => { __symbolic: 'class', decorators: [{ __symbolic: 'call', - expression: {__symbolic: 'reference', name: 'Component', module: 'angular2/core'}, + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, arguments: [{ selector: 'my-hero-detail', template: ` @@ -74,7 +59,7 @@ describe('Collector', () => { decorators: [{ __symbolic: 'call', expression: - {__symbolic: 'reference', name: 'Input', module: 'angular2/core'} + {__symbolic: 'reference', module: 'angular2/core', name: 'Input'} }] }] } @@ -85,7 +70,7 @@ describe('Collector', () => { it('should be able to get a more complicated component\'s metadata', () => { const sourceFile = program.getSourceFile('/app/app.component.ts'); - const metadata = collector.getMetadata(sourceFile, typeChecker); + const metadata = collector.getMetadata(sourceFile); expect(metadata).toEqual({ __symbolic: 'module', metadata: { @@ -93,7 +78,7 @@ describe('Collector', () => { __symbolic: 'class', decorators: [{ __symbolic: 'call', - expression: {__symbolic: 'reference', name: 'Component', module: 'angular2/core'}, + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, arguments: [{ selector: 'my-app', template: ` @@ -110,22 +95,22 @@ describe('Collector', () => { directives: [ { __symbolic: 'reference', + module: './hero-detail.component', name: 'HeroDetailComponent', - module: './hero-detail.component' }, - {__symbolic: 'reference', name: 'NgFor', module: 'angular2/common'} + {__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'} ], - providers: [{__symbolic: 'reference', name: undefined, module: './hero.service'}], + providers: [{__symbolic: 'reference', module: './hero.service', default: true}], pipes: [ - {__symbolic: 'reference', name: 'LowerCasePipe', module: 'angular2/common'}, - {__symbolic: 'reference', name: 'UpperCasePipe', module: 'angular2/common'} + {__symbolic: 'reference', module: 'angular2/common', name: 'LowerCasePipe'}, + {__symbolic: 'reference', module: 'angular2/common', name: 'UpperCasePipe'} ] }] }], members: { __ctor__: [{ __symbolic: 'constructor', - parameters: [{__symbolic: 'reference', name: undefined, module: './hero.service'}] + parameters: [{__symbolic: 'reference', module: './hero.service', default: true}] }], onSelect: [{__symbolic: 'method'}], ngOnInit: [{__symbolic: 'method'}], @@ -138,7 +123,7 @@ describe('Collector', () => { it('should return the values of exported variables', () => { const sourceFile = program.getSourceFile('/app/mock-heroes.ts'); - const metadata = collector.getMetadata(sourceFile, typeChecker); + const metadata = collector.getMetadata(sourceFile); expect(metadata).toEqual({ __symbolic: 'module', metadata: { @@ -153,11 +138,11 @@ describe('Collector', () => { }); }); - it('should have no data produced for the no data cases', () => { - const sourceFile = program.getSourceFile('/app/cases-no-data.ts'); + it('should return undefined for modules that have no metadata', () => { + const sourceFile = program.getSourceFile('/app/error-cases.ts'); expect(sourceFile).toBeTruthy(sourceFile); - const metadata = collector.getMetadata(sourceFile, typeChecker); - expect(metadata).toBeFalsy(); + const metadata = collector.getMetadata(sourceFile); + expect(metadata).toBeUndefined(); }); let casesFile: ts.SourceFile; @@ -165,14 +150,15 @@ describe('Collector', () => { beforeEach(() => { casesFile = program.getSourceFile('/app/cases-data.ts'); - casesMetadata = collector.getMetadata(casesFile, typeChecker); + casesMetadata = collector.getMetadata(casesFile); }); - it('should provide null for an any ctor pameter type', () => { + it('should provide any reference for an any ctor parameter type', () => { const casesAny = casesMetadata.metadata['CaseAny']; expect(casesAny).toBeTruthy(); const ctorData = casesAny.members['__ctor__']; - expect(ctorData).toEqual([{__symbolic: 'constructor', parameters: [null]}]); + expect(ctorData).toEqual( + [{__symbolic: 'constructor', parameters: [{__symbolic: 'reference', name: 'any'}]}]); }); it('should record annotations on set and get declarations', () => { @@ -193,6 +179,67 @@ describe('Collector', () => { const caseFullProp = casesMetadata.metadata['FullProp']; expect(caseFullProp.members).toEqual(propertyData); }); + + it('should record references to parameterized types', () => { + const casesForIn = casesMetadata.metadata['NgFor']; + expect(casesForIn).toEqual({ + __symbolic: 'class', + decorators: [{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Injectable'} + }], + members: { + __ctor__: [{ + __symbolic: 'constructor', + parameters: [{ + __symbolic: 'reference', + name: 'ClassReference', + arguments: [{__symbolic: 'reference', name: 'NgForRow'}] + }] + }] + } + }); + }); + + it('should report errors for destructured imports', () => { + let unsupported1 = program.getSourceFile('/unsupported-1.ts'); + let metadata = collector.getMetadata(unsupported1); + expect(metadata).toEqual({ + __symbolic: 'module', + metadata: { + a: { + __symbolc: 'error', + message: 'Destructuring declarations cannot be referenced statically' + }, + b: { + __symbolc: 'error', + message: 'Destructuring declarations cannot be referenced statically' + }, + c: { + __symbolc: 'error', + message: 'Destructuring declarations cannot be referenced statically' + }, + d: { + __symbolc: 'error', + message: 'Destructuring declarations cannot be referenced statically' + }, + e: { + __symbolic: 'error', + message: 'Only intialized variables and constants can be referenced statically' + } + } + }); + }); + + it('should report an error for refrences to unexpected types', () => { + let unsupported1 = program.getSourceFile('/unsupported-2.ts'); + let metadata = collector.getMetadata(unsupported1); + let barClass = metadata.metadata['Bar']; + let ctor = barClass.members['__ctor__'][0]; + let parameter = ctor.parameters[0]; + expect(parameter).toEqual( + {__symbolic: 'error', message: 'Reference to non-exported class Foo'}); + }); }); // TODO: Do not use \` in a template literal as it confuses clang-format @@ -344,8 +391,18 @@ const FILES: Directory = { this._name = value; } } + + export class ClassReference { } + export class NgForRow { + + } + + @Injectable() + export class NgFor { + constructor (public ref: ClassReference) {} + } `, - 'cases-no-data.ts': ` + 'error-cases.ts': ` import HeroService from './hero.service'; export class CaseCtor { @@ -377,7 +434,21 @@ const FILES: Directory = { declare var Promise: PromiseConstructor; `, - + 'unsupported-1.ts': ` + export let {a, b} = {a: 1, b: 2}; + export let [c, d] = [1, 2]; + export let e; + `, + 'unsupported-2.ts': ` + import {Injectable} from 'angular2/core'; + + class Foo {} + + @Injectable() + export class Bar { + constructor(private f: Foo) {} + } + `, 'node_modules': { 'angular2': { 'core.d.ts': ` diff --git a/tools/@angular/tsc-wrapped/test/evaluator.spec.ts b/tools/@angular/tsc-wrapped/test/evaluator.spec.ts index 167989a7c9..76f58ac5ae 100644 --- a/tools/@angular/tsc-wrapped/test/evaluator.spec.ts +++ b/tools/@angular/tsc-wrapped/test/evaluator.spec.ts @@ -15,14 +15,15 @@ describe('Evaluator', () => { let evaluator: Evaluator; beforeEach(() => { - host = new Host( - FILES, - ['expressions.ts', 'const_expr.ts', 'forwardRef.ts', 'classes.ts', 'newExpression.ts']); + host = new Host(FILES, [ + 'expressions.ts', 'consts.ts', 'const_expr.ts', 'forwardRef.ts', 'classes.ts', + 'newExpression.ts' + ]); service = ts.createLanguageService(host); program = service.getProgram(); typeChecker = program.getTypeChecker(); - symbols = new Symbols(); - evaluator = new Evaluator(typeChecker, symbols, []); + symbols = new Symbols(null); + evaluator = new Evaluator(symbols); }); it('should not have typescript errors in test data', () => { @@ -59,8 +60,14 @@ describe('Evaluator', () => { it('should be able to evaluate expressions', () => { var expressions = program.getSourceFile('expressions.ts'); + symbols.define('someName', 'some-name'); + symbols.define('someBool', true); + symbols.define('one', 1); + symbols.define('two', 2); expect(evaluator.evaluateNode(findVar(expressions, 'three').initializer)).toBe(3); + symbols.define('three', 3); expect(evaluator.evaluateNode(findVar(expressions, 'four').initializer)).toBe(4); + symbols.define('four', 4); expect(evaluator.evaluateNode(findVar(expressions, 'obj').initializer)) .toEqual({one: 1, two: 2, three: 3, four: 4}); expect(evaluator.evaluateNode(findVar(expressions, 'arr').initializer)).toEqual([1, 2, 3, 4]); @@ -102,9 +109,9 @@ describe('Evaluator', () => { it('should report recursive references as symbolic', () => { var expressions = program.getSourceFile('expressions.ts'); expect(evaluator.evaluateNode(findVar(expressions, 'recursiveA').initializer)) - .toEqual({__symbolic: 'reference', name: 'recursiveB', module: undefined}); + .toEqual({__symbolic: 'reference', name: 'recursiveB'}); expect(evaluator.evaluateNode(findVar(expressions, 'recursiveB').initializer)) - .toEqual({__symbolic: 'reference', name: 'recursiveA', module: undefined}); + .toEqual({__symbolic: 'reference', name: 'recursiveA'}); }); it('should correctly handle special cases for CONST_EXPR', () => { @@ -120,8 +127,8 @@ describe('Evaluator', () => { }); it('should return new expressions', () => { - evaluator = - new Evaluator(typeChecker, symbols, [{from: './classes', namedImports: [{name: 'Value'}]}]); + symbols.define('Value', {__symbolic: 'reference', module: './classes', name: 'Value'}); + evaluator = new Evaluator(symbols); var newExpression = program.getSourceFile('newExpression.ts'); expect(evaluator.evaluateNode(findVar(newExpression, 'someValue').initializer)).toEqual({ __symbolic: 'new', @@ -154,7 +161,10 @@ const FILES: Directory = { export var two = 2; `, 'expressions.ts': ` - import {someName, someBool, one, two} from './consts'; + export var someName = 'some-name'; + export var someBool = true; + export var one = 1; + export var two = 2; export var three = one + two; export var four = two * two; diff --git a/tools/@angular/tsc-wrapped/test/symbols.spec.ts b/tools/@angular/tsc-wrapped/test/symbols.spec.ts index 54f69c4e33..4b6e700c7e 100644 --- a/tools/@angular/tsc-wrapped/test/symbols.spec.ts +++ b/tools/@angular/tsc-wrapped/test/symbols.spec.ts @@ -1,29 +1,125 @@ import * as ts from 'typescript'; + +import {isMetadataGlobalReferenceExpression} from '../src/schema'; import {Symbols} from '../src/symbols'; -import {MockSymbol, MockVariableDeclaration} from './typescript.mocks'; + +import {Directory, Host, expectNoDiagnostics} from './typescript.mocks'; describe('Symbols', () => { let symbols: Symbols; const someValue = 'some-value'; - const someSymbol = MockSymbol.of('some-symbol'); - const aliasSymbol = new MockSymbol('some-symbol', someSymbol.getDeclarations()[0]); - const missingSymbol = MockSymbol.of('some-other-symbol'); - beforeEach(() => symbols = new Symbols()); + beforeEach(() => symbols = new Symbols(null)); - it('should be able to add a symbol', () => symbols.set(someSymbol, someValue)); + it('should be able to add a symbol', () => symbols.define('someSymbol', someValue)); - beforeEach(() => symbols.set(someSymbol, someValue)); + beforeEach(() => symbols.define('someSymbol', someValue)); - it('should be able to `has` a symbol', () => expect(symbols.has(someSymbol)).toBeTruthy()); + it('should be able to `has` a symbol', () => expect(symbols.has('someSymbol')).toBeTruthy()); it('should be able to `get` a symbol value', - () => expect(symbols.get(someSymbol)).toBe(someValue)); - it('should be able to `has` an alias symbol', - () => expect(symbols.has(aliasSymbol)).toBeTruthy()); + () => expect(symbols.resolve('someSymbol')).toBe(someValue)); it('should be able to `get` a symbol value', - () => expect(symbols.get(aliasSymbol)).toBe(someValue)); + () => expect(symbols.resolve('someSymbol')).toBe(someValue)); it('should be able to determine symbol is missing', - () => expect(symbols.has(missingSymbol)).toBeFalsy()); + () => expect(symbols.has('missingSymbol')).toBeFalsy()); it('should return undefined from `get` for a missing symbol', - () => expect(symbols.get(missingSymbol)).toBeUndefined()); -}); \ No newline at end of file + () => expect(symbols.resolve('missingSymbol')).toBeUndefined()); + + let host: ts.LanguageServiceHost; + let service: ts.LanguageService; + let program: ts.Program; + let expressions: ts.SourceFile; + let imports: ts.SourceFile; + + beforeEach(() => { + host = new Host(FILES, ['consts.ts', 'expressions.ts', 'imports.ts']); + service = ts.createLanguageService(host); + program = service.getProgram(); + expressions = program.getSourceFile('expressions.ts'); + imports = program.getSourceFile('imports.ts'); + }); + + it('should not have syntax errors in the test sources', () => { + expectNoDiagnostics(service.getCompilerOptionsDiagnostics()); + for (const sourceFile of program.getSourceFiles()) { + expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName)); + } + }); + + it('should be able to find the source files', () => { + expect(expressions).toBeDefined(); + expect(imports).toBeDefined(); + }) + + it('should be able to create symbols for a source file', () => { + let symbols = new Symbols(expressions); + expect(symbols).toBeDefined(); + }); + + + it('should be able to find symbols in expression', () => { + let symbols = new Symbols(expressions); + expect(symbols.has('someName')).toBeTruthy(); + expect(symbols.resolve('someName')) + .toEqual({__symbolic: 'reference', module: './consts', name: 'someName'}); + expect(symbols.has('someBool')).toBeTruthy(); + expect(symbols.resolve('someBool')) + .toEqual({__symbolic: 'reference', module: './consts', name: 'someBool'}); + }); + + it('should be able to detect a * import', () => { + let symbols = new Symbols(imports); + expect(symbols.resolve('b')).toEqual({__symbolic: 'reference', module: 'b'}); + }); + + it('should be able to detect importing a default export', () => { + let symbols = new Symbols(imports); + expect(symbols.resolve('d')).toEqual({__symbolic: 'reference', module: 'd', default: true}); + }); + + it('should be able to import a renamed symbol', () => { + let symbols = new Symbols(imports); + expect(symbols.resolve('g')).toEqual({__symbolic: 'reference', name: 'f', module: 'f'}); + }); + + it('should be able to resolve any symbol in core global scope', () => { + let core = program.getSourceFiles().find(source => source.fileName.endsWith('lib.d.ts')); + expect(core).toBeDefined(); + let visit = (node: ts.Node): boolean => { + switch (node.kind) { + case ts.SyntaxKind.VariableStatement: + case ts.SyntaxKind.VariableDeclarationList: + return ts.forEachChild(node, visit); + case ts.SyntaxKind.VariableDeclaration: + const variableDeclaration = node; + const nameNode = variableDeclaration.name; + const name = nameNode.text; + const result = symbols.resolve(name); + expect(isMetadataGlobalReferenceExpression(result) && result.name).toEqual(name); + + // Ignore everything after Float64Array as it is IE specific. + return name === 'Float64Array'; + } + return false; + }; + ts.forEachChild(core, visit); + }); +}); + +const FILES: Directory = { + 'consts.ts': ` + export var someName = 'some-name'; + export var someBool = true; + export var one = 1; + export var two = 2; + `, + 'expressions.ts': ` + import {someName, someBool, one, two} from './consts'; + `, + 'imports.ts': ` + import * as b from 'b'; + import 'c'; + import d from 'd'; + import {f as g} from 'f'; + ` +}; \ No newline at end of file diff --git a/tools/broccoli/broccoli-typescript.ts b/tools/broccoli/broccoli-typescript.ts index 811a7e5d07..50e53c053a 100644 --- a/tools/broccoli/broccoli-typescript.ts +++ b/tools/broccoli/broccoli-typescript.ts @@ -121,7 +121,6 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin { this.doFullBuild(); } else { let program = this.tsService.getProgram(); - let typeChecker = program.getTypeChecker(); tsEmitInternal = false; pathsToEmit.forEach((tsFilePath) => { let output = this.tsService.getEmitOutput(tsFilePath); @@ -139,7 +138,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin { fs.writeFileSync(o.name, this.fixSourceMapSources(o.text), FS_OPTS); if (endsWith(o.name, '.d.ts')) { const sourceFile = program.getSourceFile(tsFilePath); - this.emitMetadata(o.name, sourceFile, typeChecker); + this.emitMetadata(o.name, sourceFile); } }); } @@ -210,7 +209,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin { const originalFile = absoluteFilePath.replace(this.tsOpts.outDir, this.tsOpts.rootDir) .replace(/\.d\.ts$/, '.ts'); const sourceFile = program.getSourceFile(originalFile); - this.emitMetadata(absoluteFilePath, sourceFile, typeChecker); + this.emitMetadata(absoluteFilePath, sourceFile); } }); @@ -256,10 +255,9 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin { * Emit a .metadata.json file to correspond to the .d.ts file if the module contains classes that * use decorators or exported constants. */ - private emitMetadata( - dtsFileName: string, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { + private emitMetadata(dtsFileName: string, sourceFile: ts.SourceFile) { if (sourceFile) { - const metadata = this.metadataCollector.getMetadata(sourceFile, typeChecker); + const metadata = this.metadataCollector.getMetadata(sourceFile); if (metadata && metadata.metadata) { const metadataText = JSON.stringify(metadata); const metadataFileName = dtsFileName.replace(/\.d.ts$/, '.metadata.json');