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
This commit is contained in:
Chuck Jazdzewski 2016-05-31 11:00:39 -07:00
parent 13c39a52c6
commit 01dd7dde24
13 changed files with 656 additions and 353 deletions

View File

@ -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", () => {

View File

@ -62,17 +62,21 @@ export class CodeGenerator {
private readComponents(absSourcePath: string) {
const result: Promise<compiler.CompileDirectiveMetadata>[] = [];
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(<any>staticType);

View File

@ -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;
}

View File

@ -359,6 +359,8 @@ export class StaticReflector implements ReflectorReader {
} else {
return context;
}
case "error":
throw new Error(expression['message']);
}
return null;
}

View File

@ -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 = <ts.ImportDeclaration>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) {
(<any>newImport)['defaultName'] = importDecl.importClause.name.text;
}
const bindings = importDecl.importClause.namedBindings;
if (bindings) {
switch (bindings.kind) {
case ts.SyntaxKind.NamedImports:
const namedImports: ImportSpecifierMetadata[] = [];
(<ts.NamedImports>bindings).elements.forEach(i => {
const namedImport = {name: i.name.text};
if (i.propertyName) {
(<any>namedImport)['propertyName'] = i.propertyName.text;
}
namedImports.push(namedImport);
});
(<any>newImport)['namedImports'] = namedImports;
break;
case ts.SyntaxKind.NamespaceImport:
(<any>newImport)['namespace'] = (<ts.NamespaceImport>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 <MetadataSymbolicExpression>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 = <ts.MethodDeclaration|ts.ConstructorDeclaration>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) {
(<ConstructorMetadata>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 = <ts.PropertyDeclaration>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 = <ts.ClassDeclaration>declaration;
// Predeclare classes
ts.forEachChild(sourceFile, node => {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration:
const classDeclaration = <ts.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 = <ts.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 = <ts.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 = <ts.VariableStatement>node;
for (let variableDeclaration of variableStatement.declarationList.declarations) {
if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier) {
let nameNode = <ts.Identifier>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 {<identifier>[, <identifer>]+} = <expression>;
// or
// var [<identifier>[, <identifier}+] = <expression>;
// 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 = <ts.Identifier>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 = <ts.BindingElement>nameNode;
report(bindingElement.name);
break;
case ts.SyntaxKind.ObjectBindingPattern:
case ts.SyntaxKind.ArrayBindingPattern:
const bindings = <ts.BindingPattern>nameNode;
bindings.elements.forEach(report);
break;
}
};
report(variableDeclaration.name);
}
break;
}
}
break;
}
}
});
return metadata && {__symbolic: 'module', metadata};
}

View File

@ -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'});

View File

@ -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<K, V> {
has(k: K): boolean;
set(k: K, v: V): void;
get(k: K): V;
delete (k: K): void;
}
interface MapConstructor {
new<K, V>(): Map<K, V>;
}
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 = (<ts.PropertyAccessExpression>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 === (<ts.Identifier>lhs).text) {
return eachImport;
}
}
}
}
}
private nodeSymbolReference(node: ts.Node): MetadataSymbolicReferenceExpression {
const importNamespace = this.findImportNamespace(node);
if (importNamespace) {
const result = this.symbolReference(
this.typeChecker.getSymbolAtLocation((<ts.PropertyAccessExpression>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 (<ts.Identifier>node).text;
}
return <string>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 = <ts.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 = <ts.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 = <ts.PropertyAssignment>child;
const propertyName = this.nameOf(assignment.name);
if (isMetadataError(propertyName)) {
return propertyName;
}
const propertyValue = this.evaluateNode(assignment.initializer);
(<any>obj)[propertyName] = propertyValue;
allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined;
if (isMetadataError(propertyValue)) {
error = propertyValue;
return true; // Stop the forEachChild.
} else {
obj[<string>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 = <ts.CallExpression>node;
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
if (this.isFoldable(callExpression)) {
if (isMethodCallOf(callExpression, 'concat')) {
const arrayValue = <MetadataValue[]>this.evaluateNode(
(<ts.PropertyAccessExpression>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 = <MetadataValue[]>this.evaluateNode(
(<ts.PropertyAccessExpression>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 = <ts.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 = <ts.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 (<any>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 (<any>expression)[<string>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 = <ts.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 (<any>expression)[<string|number>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 = <ts.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 = <ts.VariableDeclaration>(
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
return this.evaluateNode(variableDeclaration.initializer);
return reference;
case ts.SyntaxKind.TypeReference:
const typeReferenceNode = <ts.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 = <ts.Identifier>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 (<ts.LiteralExpression>node).text;
case ts.SyntaxKind.StringLiteral:
return (<ts.StringLiteral>node).text;
case ts.SyntaxKind.NumericLiteral:
return parseFloat((<ts.LiteralExpression>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 = <ts.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'};
}
}

View File

@ -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';
}

View File

@ -1,36 +1,99 @@
import * as ts from 'typescript';
// TOOD: Remove when tools directory is upgraded to support es6 target
interface Map<K, V> {
has(v: V): boolean;
set(k: K, v: V): void;
get(k: K): V;
}
interface MapConstructor {
new<K, V>(): Map<K, V>;
}
declare var Map: MapConstructor;
import {MetadataValue} from './schema';
var a: Array<number>;
/**
* 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<ts.Node, any>();
private _symbols: Map<string, MetadataValue>;
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<string, MetadataValue> {
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 = <ts.ImportEqualsDeclaration>node;
if (importEqualsDeclaration.moduleReference.kind ===
ts.SyntaxKind.ExternalModuleReference) {
const externalReference =
<ts.ExternalModuleReference>importEqualsDeclaration.moduleReference;
// An `import <identifier> = require(<module-specifier>);
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 = <ts.ImportDeclaration>node;
if (!importDecl.importClause) {
// An `import <module-specifier>` clause which does not bring symbols into scope.
break;
}
const from = stripQuotes(importDecl.moduleSpecifier.getText());
if (importDecl.importClause.name) {
// An `import <identifier> form <module-specifier>` 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 { [<identifier> [, <identifier>] } from <module-specifier>` clause
for (let binding of (<ts.NamedImports>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 <identifier> from <module-specifier>` clause.
symbols.set(
(<ts.NamespaceImport>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<string, MetadataValue>) {
// 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}));
}

View File

@ -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 = <ClassMetadata>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 = <ClassMetadata>casesMetadata.metadata['FullProp'];
expect(caseFullProp.members).toEqual(propertyData);
});
it('should record references to parameterized types', () => {
const casesForIn = <ClassMetadata>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 = <ClassMetadata>metadata.metadata['Bar'];
let ctor = <ConstructorMetadata>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<T> { }
export class NgForRow {
}
@Injectable()
export class NgFor {
constructor (public ref: ClassReference<NgForRow>) {}
}
`,
'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': `

View File

@ -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;

View File

@ -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());
() => 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 = <ts.VariableDeclaration>node;
const nameNode = <ts.Identifier>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';
`
};

View File

@ -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');