chore(refactor): Refactored metadata collector

Renamed MetadataExtractor to MetadataCollector
Reorganized to split src from tests

Closes #7492
This commit is contained in:
Chuck Jazdzewski 2016-03-22 17:11:42 -07:00
parent 3f57fa6e0e
commit 09f4d6f52d
12 changed files with 896 additions and 309 deletions

View File

@ -5,7 +5,7 @@ import fse = require('fs-extra');
import path = require('path');
import * as ts from 'typescript';
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
import {MetadataExtractor} from '../metadata/extractor';
import {MetadataCollector} from '../metadata';
type FileRegistry = ts.Map<{version: number}>;
@ -50,7 +50,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
private rootFilePaths: string[];
private tsServiceHost: ts.LanguageServiceHost;
private tsService: ts.LanguageService;
private metadataExtractor: MetadataExtractor;
private metadataCollector: MetadataCollector;
private firstRun: boolean = true;
private previousRunFailed: boolean = false;
// Whether to generate the @internal typing files (they are only generated when `stripInternal` is
@ -93,7 +93,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
this.tsServiceHost = new CustomLanguageServiceHost(this.tsOpts, this.rootFilePaths,
this.fileRegistry, this.inputPath);
this.tsService = ts.createLanguageService(this.tsServiceHost, ts.createDocumentRegistry());
this.metadataExtractor = new MetadataExtractor(this.tsService);
this.metadataCollector = new MetadataCollector(this.tsService);
}
@ -265,7 +265,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
private emitMetadata(dtsFileName: string, sourceFile: ts.SourceFile,
typeChecker: ts.TypeChecker) {
if (sourceFile) {
const metadata = this.metadataExtractor.getMetadata(sourceFile, typeChecker);
const metadata = this.metadataCollector.getMetadata(sourceFile, typeChecker);
if (metadata && metadata.metadata) {
const metadataText = JSON.stringify(metadata);
const metadataFileName = dtsFileName.replace(/\.d.ts$/, '.metadata.json');

View File

@ -1,138 +0,0 @@
var mockfs = require('mock-fs');
import * as ts from 'typescript';
import * as fs from 'fs';
import {MockHost, expectNoDiagnostics, findClass} from './typescript.mock';
import {MetadataExtractor} from './extractor';
describe('MetadataExtractor', () => {
// Read the lib.d.ts before mocking fs.
let libTs: string = fs.readFileSync(ts.getDefaultLibFilePath({}), 'utf8');
beforeEach(() => files['lib.d.ts'] = libTs);
beforeEach(() => mockfs(files));
afterEach(() => mockfs.restore());
let host: ts.LanguageServiceHost;
let service: ts.LanguageService;
let program: ts.Program;
let typeChecker: ts.TypeChecker;
let extractor: MetadataExtractor;
beforeEach(() => {
host = new MockHost(['A.ts', 'B.ts', 'C.ts'], /*currentDirectory*/ undefined, 'lib.d.ts');
service = ts.createLanguageService(host);
program = service.getProgram();
typeChecker = program.getTypeChecker();
extractor = new MetadataExtractor(service);
});
it('should not have typescript errors in test data', () => {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (const sourceFile of program.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName));
}
});
it('should be able to extract metadata when defined by literals', () => {
const sourceFile = program.getSourceFile('A.ts');
const metadata = extractor.getMetadata(sourceFile, typeChecker);
expect(metadata).toEqual({
__symbolic: 'module',
module: './A',
metadata: {
A: {
__symbolic: 'class',
decorators: [
{
__symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Pipe', module: './directives'},
arguments: [{name: 'A', pure: false}]
}
]
}
}
});
});
it('should be able to extract metadata from metadata defined using vars', () => {
const sourceFile = program.getSourceFile('B.ts');
const metadata = extractor.getMetadata(sourceFile, typeChecker);
expect(metadata).toEqual({
__symbolic: 'module',
module: './B',
metadata: {
B: {
__symbolic: 'class',
decorators: [
{
__symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Pipe', module: './directives'},
arguments: [{name: 'some-name', pure: true}]
}
]
}
}
});
});
it('souce be able to extract metadata that uses external references', () => {
const sourceFile = program.getSourceFile('C.ts');
const metadata = extractor.getMetadata(sourceFile, typeChecker);
expect(metadata).toEqual({
__symbolic: 'module',
module: './C',
metadata: {
B: {
__symbolic: 'class',
decorators: [
{
__symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Pipe', module: './directives'},
arguments: [
{
name: {__symbolic: "reference", module: "./external", name: "externalName"},
pure:
{__symbolic: "reference", module: "./external", name: "externalBool"}
}
]
}
]
}
}
});
});
});
const files = {
'directives.ts': `
export function Pipe(options: { name?: string, pure?: boolean}) {
return function(fn: Function) { }
}
`,
'consts.ts': `
export var someName = 'some-name';
export var someBool = true;
`,
'external.d.ts': `
export const externalName: string;
export const externalBool: boolean;
`,
'A.ts': `
import {Pipe} from './directives';
@Pipe({name: 'A', pure: false})
export class A {}`,
'B.ts': `
import {Pipe} from './directives';
import {someName, someBool} from './consts';
@Pipe({name: someName, pure: someBool})
export class B {}`,
'C.ts': `
import {Pipe} from './directives';
import {externalName, externalBool} from './external';
@Pipe({name: externalName, pure: externalBool})
export class B {}`
}

View File

@ -1,92 +0,0 @@
import * as ts from 'typescript';
import {Evaluator} from './evaluator';
import {Symbols} from './symbols';
import * as path from 'path';
const EXT_REGEX = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const NODE_MODULES = '/node_modules/';
const NODE_MODULES_PREFIX = 'node_modules/';
function pathTo(from: string, to: string): string {
var result = path.relative(path.dirname(from), to);
if (path.dirname(result) === '.') {
result = '.' + path.sep + result;
}
return result;
}
function moduleNameFromBaseName(moduleFileName: string, baseFileName: string): string {
// Remove the extension
moduleFileName = moduleFileName.replace(EXT_REGEX, '');
// Check for node_modules
const nodeModulesIndex = moduleFileName.lastIndexOf(NODE_MODULES);
if (nodeModulesIndex >= 0) {
return moduleFileName.substr(nodeModulesIndex + NODE_MODULES.length);
}
if (moduleFileName.lastIndexOf(NODE_MODULES_PREFIX, NODE_MODULES_PREFIX.length) !== -1) {
return moduleFileName.substr(NODE_MODULES_PREFIX.length);
}
// Construct a simplified path from the file to the module
return pathTo(baseFileName, moduleFileName);
}
// TODO: Support cross-module folding
export class MetadataExtractor {
constructor(private service: ts.LanguageService) {}
/**
* 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): any {
const locals = new Symbols();
const moduleNameOf = (fileName: string) =>
moduleNameFromBaseName(fileName, sourceFile.fileName);
const evaluator = new Evaluator(this.service, typeChecker, locals, moduleNameOf);
function objFromDecorator(decoratorNode: ts.Decorator): any {
return evaluator.evaluateNode(decoratorNode.expression);
}
function classWithDecorators(classDeclaration: ts.ClassDeclaration): any {
return {
__symbolic: "class",
decorators: classDeclaration.decorators.map(decorator => objFromDecorator(decorator))
};
}
let metadata: any;
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;
if (classDeclaration.decorators) {
if (!metadata) metadata = {};
metadata[classDeclaration.name.text] = classWithDecorators(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);
}
}
break;
}
}
}
return metadata && {__symbolic: "module", module: moduleNameOf(sourceFile.fileName), metadata};
}
}

2
tools/metadata/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './src/collector';
export * from './src/schema';

View File

@ -0,0 +1,199 @@
import * as ts from 'typescript';
import {Evaluator} from './evaluator';
import {Symbols} from './symbols';
import {
ClassMetadata,
ConstructorMetadata,
ModuleMetadata,
MemberMetadata,
MetadataMap,
MetadataSymbolicExpression,
MetadataSymbolicReferenceExpression,
MetadataValue,
MethodMetadata
} from './schema';
import * as path from 'path';
const EXT_REGEX = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const NODE_MODULES = '/node_modules/';
const NODE_MODULES_PREFIX = 'node_modules/';
function pathTo(from: string, to: string): string {
var result = path.relative(path.dirname(from), to);
if (path.dirname(result) === '.') {
result = '.' + path.sep + result;
}
return result;
}
function moduleNameFromBaseName(moduleFileName: string, baseFileName: string): string {
// Remove the extension
moduleFileName = moduleFileName.replace(EXT_REGEX, '');
// Check for node_modules
const nodeModulesIndex = moduleFileName.lastIndexOf(NODE_MODULES);
if (nodeModulesIndex >= 0) {
return moduleFileName.substr(nodeModulesIndex + NODE_MODULES.length);
}
if (moduleFileName.lastIndexOf(NODE_MODULES_PREFIX, NODE_MODULES_PREFIX.length) !== -1) {
return moduleFileName.substr(NODE_MODULES_PREFIX.length);
}
// Construct a simplified path from the file to the module
return pathTo(baseFileName, moduleFileName);
}
/**
* Collect decorator metadata from a TypeScript module.
*/
export class MetadataCollector {
constructor(private service: ts.LanguageService) {}
/**
* 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 moduleNameOf = (fileName: string) =>
moduleNameFromBaseName(fileName, sourceFile.fileName);
const evaluator = new Evaluator(this.service, typeChecker, locals, moduleNameOf);
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) {
if (symbol.flags & ts.SymbolFlags.Alias) {
symbol = typeChecker.getAliasedSymbol(symbol);
}
if (symbol.declarations.length) {
const declaration = symbol.declarations[0];
const sourceFile = declaration.getSourceFile();
return {
__symbolic: "reference",
module: moduleNameOf(sourceFile.fileName),
name: symbol.name
};
}
}
}
}
function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata {
let result: ClassMetadata =
{ __symbolic: "class" }
function getDecorators(decorators: ts.Decorator[]):
MetadataSymbolicExpression[] {
if (decorators && decorators.length)
return decorators.map(decorator => objFromDecorator(decorator));
return undefined;
}
// Add class decorators
if (classDeclaration.decorators) {
result.decorators = getDecorators(classDeclaration.decorators);
}
// member decorators
let members: MetadataMap = null;
function recordMember(name: string, metadata: MemberMetadata) {
if (!members) members = {};
let data = members.hasOwnProperty(name) ? members[name] : [];
data.push(metadata);
members[name] = data;
}
for (const member of classDeclaration.members) {
let isConstructor = false;
switch (member.kind) {
case ts.SyntaxKind.Constructor:
isConstructor = true;
// fallthrough
case ts.SyntaxKind.MethodDeclaration:
const method = <ts.MethodDeclaration | ts.ConstructorDeclaration>member;
const methodDecorators = getDecorators(method.decorators);
const parameters = method.parameters;
const parameterDecoratorData: MetadataSymbolicExpression[][] = [];
const parametersData: MetadataSymbolicReferenceExpression[] = [];
let hasDecoratorData: boolean = false;
let hasParameterData: boolean = false;
for (const parameter of parameters) {
const parameterData = getDecorators(parameter.decorators);
parameterDecoratorData.push(parameterData);
hasDecoratorData = hasDecoratorData || !!parameterData;
if (isConstructor) {
const parameterType = typeChecker.getTypeAtLocation(parameter);
parametersData.push(referenceFromType(parameterType) || null);
hasParameterData = true;
}
}
if (methodDecorators || hasDecoratorData || hasParameterData) {
const data: MethodMetadata = {__symbolic: isConstructor ? "constructor" : "method"};
const name = isConstructor ? "__ctor__" : evaluator.nameOf(member.name);
if (methodDecorators) {
data.decorators = methodDecorators;
}
if (hasDecoratorData) {
data.parameterDecorators = parameterDecoratorData;
}
if (hasParameterData) {
(<ConstructorMetadata>data).parameters = parametersData;
}
recordMember(name, data);
}
break;
case ts.SyntaxKind.PropertyDeclaration:
const property = <ts.PropertyDeclaration>member;
const propertyDecorators = getDecorators(property.decorators);
if (propertyDecorators) {
recordMember(evaluator.nameOf(property.name),
{__symbolic: 'property', decorators: propertyDecorators});
}
break;
}
}
if (members) {
result.members = members;
}
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;
if (classDeclaration.decorators) {
if (!metadata) metadata = {};
metadata[classDeclaration.name.text] = 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);
}
}
break;
}
}
}
return metadata && {__symbolic: "module", module: moduleNameOf(sourceFile.fileName), metadata};
}
}

View File

@ -1,6 +1,13 @@
import * as ts from 'typescript';
import {Symbols} from './symbols';
import {
MetadataValue,
MetadataObject,
MetadataSymbolicCallExpression,
MetadataSymbolicReferenceExpression
} from './schema';
// TOOD: Remove when tools directory is upgraded to support es6 target
interface Map<K, V> {
has(k: K): boolean;
@ -43,12 +50,6 @@ function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) {
return !ts.forEachChild(node, node => !cb(node));
}
export interface SymbolReference {
__symbolic: string; // TODO: Change this to type "reference" when we move to TypeScript 1.8
name: string;
module: string;
}
function isPrimitive(value: any): boolean {
return Object(value) !== value;
}
@ -82,7 +83,7 @@ export class Evaluator {
return undefined;
}
private symbolReference(symbol: ts.Symbol): SymbolReference {
private symbolReference(symbol: ts.Symbol): MetadataSymbolicReferenceExpression {
if (symbol) {
const name = symbol.name;
const module = this.moduleNameOf(this.symbolFileName(symbol));
@ -90,7 +91,7 @@ export class Evaluator {
}
}
private nodeSymbolReference(node: ts.Node): SymbolReference {
private nodeSymbolReference(node: ts.Node): MetadataSymbolicReferenceExpression {
return this.symbolReference(this.typeChecker.getSymbolAtLocation(node));
}
@ -98,7 +99,7 @@ export class Evaluator {
if (node.kind == ts.SyntaxKind.Identifier) {
return (<ts.Identifier>node).text;
}
return this.evaluateNode(node);
return <string>this.evaluateNode(node);
}
/**
@ -213,10 +214,10 @@ export class Evaluator {
* Produce a JSON serialiable object representing `node`. The foldable values in the expression
* tree are folded. For example, a node representing `1 + 2` is folded into `3`.
*/
public evaluateNode(node: ts.Node): any {
public evaluateNode(node: ts.Node): MetadataValue {
switch (node.kind) {
case ts.SyntaxKind.ObjectLiteralExpression:
let obj = {};
let obj: MetadataValue = {};
let allPropertiesDefined = true;
ts.forEachChild(node, child => {
switch (child.kind) {
@ -245,7 +246,7 @@ export class Evaluator {
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
if (this.isFoldable(callExpression)) {
if (isMethodCallOf(callExpression, "concat")) {
const arrayValue = this.evaluateNode(
const arrayValue = <MetadataValue[]>this.evaluateNode(
(<ts.PropertyAccessExpression>callExpression.expression).expression);
return arrayValue.concat(args[0]);
}
@ -256,11 +257,14 @@ export class Evaluator {
}
const expression = this.evaluateNode(callExpression.expression);
if (isDefined(expression) && args.every(isDefined)) {
return {
const result: MetadataSymbolicCallExpression = {
__symbolic: "call",
expression: this.evaluateNode(callExpression.expression),
arguments: args
expression: this.evaluateNode(callExpression.expression)
};
if (args && args.length) {
result.arguments = args;
}
return result;
}
break;
case ts.SyntaxKind.PropertyAccessExpression: {
@ -279,13 +283,9 @@ export class Evaluator {
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
if (this.isFoldable(elementAccessExpression.expression) &&
this.isFoldable(elementAccessExpression.argumentExpression))
return expression[index];
return expression[<string | number>index];
if (isDefined(expression) && isDefined(index)) {
return {
__symbolic: "index",
expression,
index: this.evaluateNode(elementAccessExpression.argumentExpression)
};
return {__symbolic: "index", expression, index};
}
break;
}
@ -317,6 +317,9 @@ export class Evaluator {
case ts.SyntaxKind.ParenthesizedExpression:
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
return this.evaluateNode(parenthesizedExpression.expression);
case ts.SyntaxKind.TypeAssertionExpression:
const typeAssertion = <ts.TypeAssertion>node;
return this.evaluateNode(typeAssertion.expression);
case ts.SyntaxKind.PrefixUnaryExpression:
const prefixUnaryExpression = <ts.PrefixUnaryExpression>node;
const operand = this.evaluateNode(prefixUnaryExpression.operand);
@ -357,20 +360,48 @@ export class Evaluator {
if (isDefined(left) && isDefined(right)) {
if (isPrimitive(left) && isPrimitive(right))
switch (binaryExpression.operatorToken.kind) {
case ts.SyntaxKind.PlusToken:
return left + right;
case ts.SyntaxKind.MinusToken:
return left - right;
case ts.SyntaxKind.AsteriskToken:
return left * right;
case ts.SyntaxKind.SlashToken:
return left / right;
case ts.SyntaxKind.PercentToken:
return left % right;
case ts.SyntaxKind.AmpersandAmpersandToken:
return left && right;
case ts.SyntaxKind.BarBarToken:
return left || right;
return <any>left || <any>right;
case ts.SyntaxKind.AmpersandAmpersandToken:
return <any>left && <any>right;
case ts.SyntaxKind.AmpersandToken:
return <any>left & <any>right;
case ts.SyntaxKind.BarToken:
return <any>left | <any>right;
case ts.SyntaxKind.CaretToken:
return <any>left ^ <any>right;
case ts.SyntaxKind.EqualsEqualsToken:
return <any>left == <any>right;
case ts.SyntaxKind.ExclamationEqualsToken:
return <any>left != <any>right;
case ts.SyntaxKind.EqualsEqualsEqualsToken:
return <any>left === <any>right;
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
return <any>left !== <any>right;
case ts.SyntaxKind.LessThanToken:
return <any>left < <any>right;
case ts.SyntaxKind.GreaterThanToken:
return <any>left > <any>right;
case ts.SyntaxKind.LessThanEqualsToken:
return <any>left <= <any>right;
case ts.SyntaxKind.GreaterThanEqualsToken:
return <any>left >= <any>right;
case ts.SyntaxKind.LessThanLessThanToken:
return (<any>left) << (<any>right);
case ts.SyntaxKind.GreaterThanGreaterThanToken:
return <any>left >> <any>right;
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
return <any>left >>> <any>right;
case ts.SyntaxKind.PlusToken:
return <any>left + <any>right;
case ts.SyntaxKind.MinusToken:
return <any>left - <any>right;
case ts.SyntaxKind.AsteriskToken:
return <any>left * <any>right;
case ts.SyntaxKind.SlashToken:
return <any>left / <any>right;
case ts.SyntaxKind.PercentToken:
return <any>left % <any>right;
}
return {
__symbolic: "binop",

View File

@ -0,0 +1,141 @@
// TODO: fix typings for __symbolic once angular moves to 1.8
export interface ModuleMetadata {
__symbolic: string; // "module";
module: string;
metadata: {[name: string]: (ClassMetadata | MetadataValue)};
}
export function isModuleMetadata(value: any): value is ModuleMetadata {
return value && value.__symbolic === "module";
}
export interface ClassMetadata {
__symbolic: string; // "class";
decorators?: MetadataSymbolicExpression[];
members?: MetadataMap;
}
export function isClassMetadata(value: any): value is ClassMetadata {
return value && value.__symbolic === "class";
}
export interface MetadataMap { [name: string]: MemberMetadata[]; }
export interface MemberMetadata {
__symbolic: string; // "constructor" | "method" | "property";
decorators?: MetadataSymbolicExpression[];
}
export function isMemberMetadata(value: any): value is MemberMetadata {
if (value) {
switch (value.__symbolic) {
case "constructor":
case "method":
case "property":
return true;
}
}
return false;
}
export interface MethodMetadata extends MemberMetadata {
// __symbolic: "constructor" | "method";
parameterDecorators?: MetadataSymbolicExpression[][];
}
export function isMethodMetadata(value: any): value is MemberMetadata {
return value && (value.__symbolic === "constructor" || value.__symbolic === "method");
}
export interface ConstructorMetadata extends MethodMetadata {
// __symbolic: "constructor";
parameters?: MetadataSymbolicExpression[];
}
export function isConstructorMetadata(value: any): value is ConstructorMetadata {
return value && value.__symbolic === "constructor";
}
export type MetadataValue =
string | number | boolean | MetadataObject | MetadataArray | MetadataSymbolicExpression;
export interface MetadataObject { [name: string]: MetadataValue; }
export interface MetadataArray { [name: number]: MetadataValue; }
export interface MetadataSymbolicExpression {
__symbolic: string; // "binary" | "call" | "index" | "pre" | "reference" | "select"
}
export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression {
if (value) {
switch (value.__symbolic) {
case "binary":
case "call":
case "index":
case "pre":
case "reference":
case "select":
return true;
}
}
return false;
}
export interface MetadataSymbolicBinaryExpression extends MetadataSymbolicExpression {
// __symbolic: "binary";
operator: string; // "&&" | "||" | "|" | "^" | "&" | "==" | "!=" | "===" | "!==" | "<" | ">" |
// "<=" | ">=" | "instanceof" | "in" | "as" | "<<" | ">>" | ">>>" | "+" | "-" |
// "*" | "/" | "%" | "**";
left: MetadataValue;
right: MetadataValue;
}
export function isMetadataSymbolicBinaryExpression(
value: any): value is MetadataSymbolicBinaryExpression {
return value && value.__symbolic === "binary";
}
export interface MetadataSymbolicIndexExpression extends MetadataSymbolicExpression {
// __symbolic: "index";
expression: MetadataValue;
index: MetadataValue;
}
export function isMetadataSymbolicIndexExpression(
value: any): value is MetadataSymbolicIndexExpression {
return value && value.__symbolic === "index";
}
export interface MetadataSymbolicCallExpression extends MetadataSymbolicExpression {
// __symbolic: "call";
expression: MetadataValue;
arguments?: MetadataValue[];
}
export function isMetadataSymbolicCallExpression(
value: any): value is MetadataSymbolicCallExpression {
return value && value.__symbolic === "call";
}
export interface MetadataSymbolicPrefixExpression extends MetadataSymbolicExpression {
// __symbolic: "pre";
operator: string; // "+" | "-" | "~" | "!";
operand: MetadataValue;
}
export function isMetadataSymbolicPrefixExpression(
value: any): value is MetadataSymbolicPrefixExpression {
return value && value.__symbolic === "pre";
}
export interface MetadataSymbolicReferenceExpression extends MetadataSymbolicExpression {
// __symbolic: "reference";
name: string;
module: string;
}
export function isMetadataSymbolicReferenceExpression(
value: any): value is MetadataSymbolicReferenceExpression {
return value && value.__symbolic === "reference";
}
export interface MetadataSymbolicSelectExpression extends MetadataSymbolicExpression {
// __symbolic: "select";
expression: MetadataValue;
name: string;
}
export function isMetadataSymbolicSelectExpression(
value: any): value is MetadataSymbolicSelectExpression {
return value && value.__symbolic === "select";
}

View File

@ -0,0 +1,394 @@
import * as ts from 'typescript';
import {MetadataCollector} from '../src/collector';
import {ClassMetadata} from '../src/schema';
import {Directory, expectValidSources, Host} from './typescript.mocks';
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']);
service = ts.createLanguageService(host);
program = service.getProgram();
typeChecker = program.getTypeChecker();
collector = new MetadataCollector(service);
});
it('should not have errors in test data', () => { expectValidSources(service, program); });
it('should return undefined for modules that have no metadata', () => {
const sourceFile = program.getSourceFile('app/hero.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker);
expect(metadata).toBeUndefined();
});
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);
expect(metadata).toEqual({
__symbolic: 'module',
module: './hero-detail.component',
metadata: {
HeroDetailComponent: {
__symbolic: 'class',
decorators: [
{
__symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Component', module: 'angular2/core'},
arguments: [
{
selector: 'my-hero-detail',
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
`
}
]
}
],
members: {
hero: [
{
__symbolic: 'property',
decorators: [
{
__symbolic: 'call',
expression:
{__symbolic: 'reference', name: 'Input', module: 'angular2/core'}
}
]
}
]
}
}
}
});
});
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);
expect(metadata).toEqual({
__symbolic: 'module',
module: './app.component',
metadata: {
AppComponent: {
__symbolic: 'class',
decorators: [
{
__symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Component', module: 'angular2/core'},
arguments: [
{
selector: 'my-app',
template: `
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
(click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id | lowercase}}</span> {{hero.name | uppercase}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
directives: [
{
__symbolic: 'reference',
name: 'HeroDetailComponent',
module: './hero-detail.component'
},
{__symbolic: 'reference', name: 'NgFor', module: 'angular2/common'}
],
providers:
[{__symbolic: 'reference', name: 'HeroService', module: './hero.service'}],
pipes: [
{__symbolic: 'reference', name: 'LowerCasePipe', module: 'angular2/common'},
{
__symbolic: 'reference',
name: 'UpperCasePipe',
module: 'angular2/common'
}
]
}
]
}
],
members: {
__ctor__: [
{
__symbolic: 'constructor',
parameters: [
{__symbolic: 'reference', module: './hero.service', name: 'HeroService'}
]
}
]
}
}
}
});
});
it('should return the values of exported variables', () => {
const sourceFile = program.getSourceFile('/app/mock-heroes.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker);
expect(metadata).toEqual({
__symbolic: 'module',
module: './mock-heroes',
metadata: {
HEROES: [
{"id": 11, "name": "Mr. Nice"},
{"id": 12, "name": "Narco"},
{"id": 13, "name": "Bombasto"},
{"id": 14, "name": "Celeritas"},
{"id": 15, "name": "Magneta"},
{"id": 16, "name": "RubberMan"},
{"id": 17, "name": "Dynama"},
{"id": 18, "name": "Dr IQ"},
{"id": 19, "name": "Magma"},
{"id": 20, "name": "Tornado"}
]
}
});
});
it('should have no data produced for the no data cases', () => {
const sourceFile = program.getSourceFile('/app/cases-no-data.ts');
expect(sourceFile).toBeTruthy(sourceFile);
const metadata = collector.getMetadata(sourceFile, typeChecker);
expect(metadata).toBeFalsy();
});
it('should provide null for an any ctor pameter type', () => {
const sourceFile = program.getSourceFile('/app/cases-data.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker);
expect(metadata).toBeTruthy();
const casesAny = <ClassMetadata>metadata.metadata['CaseAny'];
expect(casesAny).toBeTruthy();
const ctorData = casesAny.members['__ctor__'];
expect(ctorData).toEqual([{__symbolic: 'constructor', parameters: [null]}]);
});
});
// TODO: Do not use \` in a template literal as it confuses clang-format
const FILES: Directory = {
'app': {
'app.component.ts': `
import {Component, OnInit} from 'angular2/core';
import {NgFor, LowerCasePipe, UpperCasePipe} from 'angular2/common';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
import {HeroService} from './hero.service';
@Component({
selector: 'my-app',
template:` + "`" + `
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
(click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id | lowercase}}</span> {{hero.name | uppercase}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
` +
"`" + `,
directives: [HeroDetailComponent, NgFor],
providers: [HeroService],
pipes: [LowerCasePipe, UpperCasePipe]
})
export class AppComponent implements OnInit {
public title = 'Tour of Heroes';
public heroes: Hero[];
public selectedHero: Hero;
constructor(private _heroService: HeroService) { }
onSelect(hero: Hero) { this.selectedHero = hero; }
ngOnInit() {
this.getHeroes()
}
getHeroes() {
this._heroService.getHeroesSlowly().then(heros => this.heroes = heros);
}
}`,
'hero.ts': `
export interface Hero {
id: number;
name: string;
}`,
'hero-detail.component.ts': `
import {Component, Input} from 'angular2/core';
import {Hero} from './hero';
@Component({
selector: 'my-hero-detail',
template: ` + "`" + `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
` + "`" + `,
})
export class HeroDetailComponent {
@Input() public hero: Hero;
}`,
'mock-heroes.ts': `
import {Hero} from './hero';
export const HEROES: Hero[] = [
{"id": 11, "name": "Mr. Nice"},
{"id": 12, "name": "Narco"},
{"id": 13, "name": "Bombasto"},
{"id": 14, "name": "Celeritas"},
{"id": 15, "name": "Magneta"},
{"id": 16, "name": "RubberMan"},
{"id": 17, "name": "Dynama"},
{"id": 18, "name": "Dr IQ"},
{"id": 19, "name": "Magma"},
{"id": 20, "name": "Tornado"}
];`,
'hero.service.ts': `
import {Injectable} from 'angular2/core';
import {HEROES} from './mock-heroes';
import {Hero} from './hero';
@Injectable()
export class HeroService {
getHeros() {
return Promise.resolve(HEROES);
}
getHeroesSlowly() {
return new Promise<Hero[]>(resolve =>
setTimeout(()=>resolve(HEROES), 2000)); // 2 seconds
}
}`,
'cases-data.ts': `
import {Injectable} from 'angular2/core';
@Injectable()
export class CaseAny {
constructor(param: any) {}
}
`,
'cases-no-data.ts': `
import {HeroService} from './hero.service';
export class CaseCtor {
constructor(private _heroService: HeroService) { }
}
`
},
'promise.ts': `
interface PromiseLike<T> {
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): PromiseLike<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): PromiseLike<TResult>;
}
interface Promise<T> {
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): Promise<TResult>;
catch(onrejected?: (reason: any) => T | PromiseLike<T>): Promise<T>;
catch(onrejected?: (reason: any) => void): Promise<T>;
}
interface PromiseConstructor {
prototype: Promise<any>;
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
reject(reason: any): Promise<void>;
reject<T>(reason: any): Promise<T>;
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
resolve(): Promise<void>;
}
declare var Promise: PromiseConstructor;
`,
'node_modules': {
'angular2': {
'core.d.ts': `
export interface Type extends Function { }
export interface TypeDecorator {
<T extends Type>(type: T): T;
(target: Object, propertyKey?: string | symbol, parameterIndex?: number): void;
annotations: any[];
}
export interface ComponentDecorator extends TypeDecorator { }
export interface ComponentFactory {
(obj: {
selector?: string;
inputs?: string[];
outputs?: string[];
properties?: string[];
events?: string[];
host?: {
[key: string]: string;
};
bindings?: any[];
providers?: any[];
exportAs?: string;
moduleId?: string;
queries?: {
[key: string]: any;
};
viewBindings?: any[];
viewProviders?: any[];
templateUrl?: string;
template?: string;
styleUrls?: string[];
styles?: string[];
directives?: Array<Type | any[]>;
pipes?: Array<Type | any[]>;
}): ComponentDecorator;
}
export declare var Component: ComponentFactory;
export interface InputFactory {
(bindingPropertyName?: string): any;
new (bindingPropertyName?: string): any;
}
export declare var Input: InputFactory;
export interface InjectableFactory {
(): any;
}
export declare var Injectable: InjectableFactory;
export interface OnInit {
ngOnInit(): any;
}
`,
'common.d.ts': `
export declare class NgFor {
ngForOf: any;
ngForTemplate: any;
ngDoCheck(): void;
}
export declare class LowerCasePipe {
transform(value: string, args?: any[]): string;
}
export declare class UpperCasePipe {
transform(value: string, args?: any[]): string;
}
`
}
}
};

View File

@ -1,19 +1,10 @@
var mockfs = require('mock-fs');
import * as ts from 'typescript';
import * as fs from 'fs';
import {MockHost, expectNoDiagnostics, findVar} from './typescript.mock';
import {Evaluator} from './evaluator';
import {Symbols} from './symbols';
import {Directory, Host, expectNoDiagnostics, findVar} from './typescript.mocks';
import {Evaluator} from '../src/evaluator';
import {Symbols} from '../src/symbols';
describe('Evaluator', () => {
// Read the lib.d.ts before mocking fs.
let libTs: string = fs.readFileSync(ts.getDefaultLibFilePath({}), 'utf8');
beforeEach(() => files['lib.d.ts'] = libTs);
beforeEach(() => mockfs(files));
afterEach(() => mockfs.restore());
let host: ts.LanguageServiceHost;
let service: ts.LanguageService;
let program: ts.Program;
@ -22,7 +13,7 @@ describe('Evaluator', () => {
let evaluator: Evaluator;
beforeEach(() => {
host = new MockHost(['expressions.ts'], /*currentDirectory*/ undefined, 'lib.d.ts');
host = new Host(FILES, ['expressions.ts']);
service = ts.createLanguageService(host);
program = service.getProgram();
typeChecker = program.getTypeChecker();
@ -74,6 +65,33 @@ describe('Evaluator', () => {
expect(evaluator.evaluateNode(findVar(expressions, 'bOr').initializer)).toEqual(true);
expect(evaluator.evaluateNode(findVar(expressions, 'nDiv').initializer)).toEqual(2);
expect(evaluator.evaluateNode(findVar(expressions, 'nMod').initializer)).toEqual(1);
expect(evaluator.evaluateNode(findVar(expressions, 'bLOr').initializer)).toEqual(false || true);
expect(evaluator.evaluateNode(findVar(expressions, 'bLAnd').initializer)).toEqual(true && true);
expect(evaluator.evaluateNode(findVar(expressions, 'bBOr').initializer)).toEqual(0x11 | 0x22);
expect(evaluator.evaluateNode(findVar(expressions, 'bBAnd').initializer)).toEqual(0x11 & 0x03);
expect(evaluator.evaluateNode(findVar(expressions, 'bXor').initializer)).toEqual(0x11 ^ 0x21);
expect(evaluator.evaluateNode(findVar(expressions, 'bEqual').initializer))
.toEqual(1 == <any>"1");
expect(evaluator.evaluateNode(findVar(expressions, 'bNotEqual').initializer))
.toEqual(1 != <any>"1");
expect(evaluator.evaluateNode(findVar(expressions, 'bIdentical').initializer))
.toEqual(1 === <any>"1");
expect(evaluator.evaluateNode(findVar(expressions, 'bNotIdentical').initializer))
.toEqual(1 !== <any>"1");
expect(evaluator.evaluateNode(findVar(expressions, 'bLessThan').initializer)).toEqual(1 < 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bGreaterThan').initializer)).toEqual(1 > 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bLessThanEqual').initializer))
.toEqual(1 <= 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bGreaterThanEqual').initializer))
.toEqual(1 >= 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bShiftLeft').initializer)).toEqual(1 << 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bShiftRight').initializer))
.toEqual(-1 >> 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bShiftRightU').initializer))
.toEqual(-1 >>> 2);
});
it('should report recursive references as symbolic', () => {
@ -85,7 +103,7 @@ describe('Evaluator', () => {
});
});
const files = {
const FILES: Directory = {
'directives.ts': `
export function Pipe(options: { name?: string, pure?: boolean}) {
return function(fn: Function) { }
@ -111,6 +129,23 @@ const files = {
export var nDiv = four / two;
export var nMod = (four + one) % two;
export var bLOr = false || true; // true
export var bLAnd = true && true; // true
export var bBOr = 0x11 | 0x22; // 0x33
export var bBAnd = 0x11 & 0x03; // 0x01
export var bXor = 0x11 ^ 0x21; // 0x20
export var bEqual = 1 == <any>"1"; // true
export var bNotEqual = 1 != <any>"1"; // false
export var bIdentical = 1 === <any>"1"; // false
export var bNotIdentical = 1 !== <any>"1"; // true
export var bLessThan = 1 < 2; // true
export var bGreaterThan = 1 > 2; // false
export var bLessThanEqual = 1 <= 2; // true
export var bGreaterThanEqual = 1 >= 2; // false
export var bShiftLeft = 1 << 2; // 0x04
export var bShiftRight = -1 >> 2; // -1
export var bShiftRightU = -1 >>> 2; // 0x3fffffff
export var recursiveA = recursiveB;
export var recursiveB = recursiveA;
`,

View File

@ -1,9 +1,6 @@
/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../typings/jasmine/jasmine.d.ts" />
import * as ts from 'typescript';
import {Symbols} from './symbols';
import {MockSymbol, MockVariableDeclaration} from './typescript.mock';
import {Symbols} from '../src/symbols';
import {MockSymbol, MockVariableDeclaration} from './typescript.mocks';
describe('Symbols', () => {
let symbols: Symbols;

View File

@ -1,35 +1,45 @@
import * as ts from 'typescript';
import * as path from 'path';
import * as fs from 'fs';
import * as ts from 'typescript';
/**
* A mock language service host that assumes mock-fs is used for the file system.
*/
export class MockHost implements ts.LanguageServiceHost {
constructor(private fileNames: string[], private currentDirectory: string = process.cwd(),
private libName?: string) {}
export interface Directory { [name: string]: (Directory | string); }
export class Host implements ts.LanguageServiceHost {
constructor(private directory: Directory, private scripts: string[]) {}
getCompilationSettings(): ts.CompilerOptions {
return {
experimentalDecorators: true,
modules: ts.ModuleKind.CommonJS,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5
};
}
getScriptFileNames(): string[] { return this.fileNames; }
getScriptFileNames(): string[] { return this.scripts; }
getScriptVersion(fileName: string): string { return "1"; }
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
if (fs.existsSync(fileName)) {
return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName, 'utf8'))
}
let content = this.getFileContent(fileName);
if (content) return ts.ScriptSnapshot.fromString(content);
}
getCurrentDirectory(): string { return this.currentDirectory; }
getCurrentDirectory(): string { return '/'; }
getDefaultLibFileName(options: ts.CompilerOptions): string {
return this.libName || ts.getDefaultLibFilePath(options);
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
private getFileContent(fileName: string): string {
const names = fileName.split(path.sep);
if (names[names.length - 1] === 'lib.d.ts') {
return fs.readFileSync(ts.getDefaultLibFilePath(this.getCompilationSettings()), 'utf8');
}
let current: Directory | string = this.directory;
if (names.length && names[0] === '') names.shift();
for (const name of names) {
if (!current || typeof current === 'string') return undefined;
current = current[name];
}
if (typeof current === 'string') return current;
}
}
@ -102,6 +112,14 @@ export function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
expect(diagnostics.length).toBe(0);
}
export function expectValidSources(service: ts.LanguageService, program: ts.Program) {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (const sourceFile of program.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName));
expectNoDiagnostics(service.getSemanticDiagnostics(sourceFile.fileName));
}
}
export function allChildren<T>(node: ts.Node, cb: (node: ts.Node) => T) {
return ts.forEachChild(node, child => {
const result = cb(node);
@ -112,18 +130,14 @@ export function allChildren<T>(node: ts.Node, cb: (node: ts.Node) => T) {
})
}
export function findVar(sourceFile: ts.SourceFile, name: string): ts.VariableDeclaration {
return allChildren(sourceFile,
node => isVar(node) && isNamed(node.name, name) ? node : undefined);
}
export function findClass(sourceFile: ts.SourceFile, name: string): ts.ClassDeclaration {
return ts.forEachChild(sourceFile,
node => isClass(node) && isNamed(node.name, name) ? node : undefined);
}
export function isVar(node: ts.Node): node is ts.VariableDeclaration {
return node.kind === ts.SyntaxKind.VariableDeclaration;
export function findVar(sourceFile: ts.SourceFile, name: string): ts.VariableDeclaration {
return allChildren(sourceFile,
node => isVar(node) && isNamed(node.name, name) ? node : undefined);
}
export function isClass(node: ts.Node): node is ts.ClassDeclaration {
@ -133,3 +147,7 @@ export function isClass(node: ts.Node): node is ts.ClassDeclaration {
export function isNamed(node: ts.Node, name: string): node is ts.Identifier {
return node.kind === ts.SyntaxKind.Identifier && (<ts.Identifier>node).text === name;
}
export function isVar(node: ts.Node): node is ts.VariableDeclaration {
return node.kind === ts.SyntaxKind.VariableDeclaration;
}