From 01dd7dde24d0812fe6785821780fe16c39c4cbce Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Tue, 31 May 2016 11:00:39 -0700 Subject: [PATCH] chore(tools): Remove use of TypeChecker from metadata collector. The metadata collector was modified to look up references in the import list instead of resolving the symbol using the TypeChecker making the use of the TypeChecker vestigial. This change removes all uses of the TypeChecker. Modified the schema to be able to record global and local (non-module specific references). Added error messages to the schema and errors are recorded in the metadata file allowing the static reflector to throw errors if an unsupported construct is referenced by metadata. Closes #8966 Fixes #8893 Fixes #8894 --- .../integrationtest/test/basic_spec.ts | 2 +- modules/@angular/compiler_cli/src/codegen.ts | 12 +- .../compiler_cli/src/reflector_host.ts | 2 +- .../compiler_cli/src/static_reflector.ts | 2 + tools/@angular/tsc-wrapped/src/collector.ts | 203 +++++++------ .../@angular/tsc-wrapped/src/compiler_host.ts | 3 +- tools/@angular/tsc-wrapped/src/evaluator.ts | 285 +++++++++--------- tools/@angular/tsc-wrapped/src/schema.ts | 62 +++- tools/@angular/tsc-wrapped/src/symbols.ts | 117 +++++-- .../tsc-wrapped/test/collector.spec.ts | 155 +++++++--- .../tsc-wrapped/test/evaluator.spec.ts | 30 +- .../@angular/tsc-wrapped/test/symbols.spec.ts | 126 +++++++- tools/broccoli/broccoli-typescript.ts | 10 +- 13 files changed, 656 insertions(+), 353 deletions(-) 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');