feat(tsc-wrapped): allow values to be substituted by collector clients

Also reenabled tests that were unintentionally disabled when they were
moved from tools/@angular.
This commit is contained in:
Chuck Jazdzewski 2017-07-26 14:58:07 -07:00 committed by Alex Rickabaugh
parent 381471d338
commit 67dff7bd5d
7 changed files with 113 additions and 9 deletions

View File

@ -26,7 +26,7 @@ const isStatic = (ts as any).ModifierFlags ?
/** /**
* A set of collector options to use when collecting metadata. * A set of collector options to use when collecting metadata.
*/ */
export class CollectorOptions { export interface CollectorOptions {
/** /**
* Version of the metadata to collect. * Version of the metadata to collect.
*/ */
@ -42,6 +42,11 @@ export class CollectorOptions {
* Do not simplify invalid expressions. * Do not simplify invalid expressions.
*/ */
verboseInvalidExpression?: boolean; verboseInvalidExpression?: boolean;
/**
* An expression substitution callback.
*/
substituteExpression?: (value: MetadataValue, node: ts.Node) => MetadataValue;
} }
/** /**
@ -54,12 +59,25 @@ export class MetadataCollector {
* Returns a JSON.stringify friendly form describing the decorators of the exported classes from * 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. * the source file that is expected to correspond to a module.
*/ */
public getMetadata(sourceFile: ts.SourceFile, strict: boolean = false): ModuleMetadata|undefined { public getMetadata(
sourceFile: ts.SourceFile, strict: boolean = false,
substituteExpression?: (value: MetadataValue, node: ts.Node) => MetadataValue): ModuleMetadata
|undefined {
const locals = new Symbols(sourceFile); const locals = new Symbols(sourceFile);
const nodeMap = const nodeMap =
new Map<MetadataValue|ClassMetadata|InterfaceMetadata|FunctionMetadata, ts.Node>(); new Map<MetadataValue|ClassMetadata|InterfaceMetadata|FunctionMetadata, ts.Node>();
const evaluator = new Evaluator(locals, nodeMap, this.options); const composedSubstituter = substituteExpression && this.options.substituteExpression ?
(value: MetadataValue, node: ts.Node) =>
this.options.substituteExpression !(substituteExpression(value, node), node) :
substituteExpression;
const evaluatorOptions = substituteExpression ?
{...this.options, substituteExpression: composedSubstituter} :
this.options;
let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined; let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined;
const evaluator = new Evaluator(locals, nodeMap, evaluatorOptions, (name, value) => {
if (!metadata) metadata = {};
metadata[name] = value;
});
let exports: ModuleExportMetadata[]|undefined = undefined; let exports: ModuleExportMetadata[]|undefined = undefined;
function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression {

View File

@ -9,9 +9,10 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CollectorOptions} from './collector'; import {CollectorOptions} from './collector';
import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema'; import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema';
import {Symbols} from './symbols'; import {Symbols} from './symbols';
// In TypeScript 2.1 the spread element kind was renamed. // In TypeScript 2.1 the spread element kind was renamed.
const spreadElementSyntaxKind: ts.SyntaxKind = const spreadElementSyntaxKind: ts.SyntaxKind =
(ts.SyntaxKind as any).SpreadElement || (ts.SyntaxKind as any).SpreadElementExpression; (ts.SyntaxKind as any).SpreadElement || (ts.SyntaxKind as any).SpreadElementExpression;
@ -104,7 +105,8 @@ export function errorSymbol(
export class Evaluator { export class Evaluator {
constructor( constructor(
private symbols: Symbols, private nodeMap: Map<MetadataEntry, ts.Node>, private symbols: Symbols, private nodeMap: Map<MetadataEntry, ts.Node>,
private options: CollectorOptions = {}) {} private options: CollectorOptions = {},
private recordExport?: (name: string, value: MetadataValue) => void) {}
nameOf(node: ts.Node|undefined): string|MetadataError { nameOf(node: ts.Node|undefined): string|MetadataError {
if (node && node.kind == ts.SyntaxKind.Identifier) { if (node && node.kind == ts.SyntaxKind.Identifier) {
@ -232,7 +234,14 @@ export class Evaluator {
const t = this; const t = this;
let error: MetadataError|undefined; let error: MetadataError|undefined;
function recordEntry<T extends MetadataEntry>(entry: T, node: ts.Node): T { function recordEntry(entry: MetadataValue, node: ts.Node): MetadataValue {
if (t.options.substituteExpression) {
const newEntry = t.options.substituteExpression(entry, node);
if (t.recordExport && newEntry != entry && isMetadataGlobalReferenceExpression(newEntry)) {
t.recordExport(newEntry.name, entry);
}
entry = newEntry;
}
t.nodeMap.set(entry, node); t.nodeMap.set(entry, node);
return entry; return entry;
} }
@ -283,7 +292,7 @@ export class Evaluator {
if (this.options.quotedNames && quoted.length) { if (this.options.quotedNames && quoted.length) {
obj['$quoted$'] = quoted; obj['$quoted$'] = quoted;
} }
return obj; return recordEntry(obj, node);
case ts.SyntaxKind.ArrayLiteralExpression: case ts.SyntaxKind.ArrayLiteralExpression:
let arr: MetadataValue[] = []; let arr: MetadataValue[] = [];
ts.forEachChild(node, child => { ts.forEachChild(node, child => {
@ -308,7 +317,7 @@ export class Evaluator {
arr.push(value); arr.push(value);
}); });
if (error) return error; if (error) return error;
return arr; return recordEntry(arr, node);
case spreadElementSyntaxKind: case spreadElementSyntaxKind:
let spreadExpression = this.evaluateNode((node as any).expression); let spreadExpression = this.evaluateNode((node as any).expression);
return recordEntry({__symbolic: 'spread', expression: spreadExpression}, node); return recordEntry({__symbolic: 'spread', expression: spreadExpression}, node);

View File

@ -9,7 +9,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {MetadataCollector} from '../src/collector'; import {MetadataCollector} from '../src/collector';
import {ClassMetadata, ConstructorMetadata, MetadataEntry, ModuleMetadata, isClassMetadata} from '../src/schema'; import {ClassMetadata, ConstructorMetadata, MetadataEntry, ModuleMetadata, isClassMetadata, isMetadataGlobalReferenceExpression} from '../src/schema';
import {Directory, Host, expectValidSources} from './typescript.mocks'; import {Directory, Host, expectValidSources} from './typescript.mocks';
@ -939,6 +939,44 @@ describe('Collector', () => {
}); });
}); });
describe('substitutions', () => {
const lambdaTemp = 'lambdaTemp';
it('should be able to substitute a lambda', () => {
const source = createSource(`
const b = 1;
export const a = () => b;
`);
const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => {
if (node.kind === ts.SyntaxKind.ArrowFunction) {
return {__symbolic: 'reference', name: lambdaTemp};
}
return value;
});
expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp});
});
it('should compose substitution functions', () => {
const collector = new MetadataCollector({
substituteExpression: (value, node) => isMetadataGlobalReferenceExpression(value) &&
value.name == lambdaTemp ?
{__symbolic: 'reference', name: value.name + '2'} :
value
});
const source = createSource(`
const b = 1;
export const a = () => b;
`);
const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => {
if (node.kind === ts.SyntaxKind.ArrowFunction) {
return {__symbolic: 'reference', name: lambdaTemp};
}
return value;
});
expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp + '2'});
});
});
function override(fileName: string, content: string) { function override(fileName: string, content: string) {
host.overrideFile(fileName, content); host.overrideFile(fileName, content);
host.addFile(fileName); host.addFile(fileName);

View File

@ -223,6 +223,45 @@ describe('Evaluator', () => {
expect(evaluator.evaluateNode(expr.initializer !)) expect(evaluator.evaluateNode(expr.initializer !))
.toEqual({__symbolic: 'new', expression: {__symbolic: 'reference', name: 'f'}}); .toEqual({__symbolic: 'new', expression: {__symbolic: 'reference', name: 'f'}});
}); });
describe('with substitution', () => {
let evaluator: Evaluator;
const lambdaTemp = 'lambdaTemp';
beforeEach(() => {
evaluator = new Evaluator(symbols, new Map(), {
substituteExpression: (value, node) => {
if (node.kind == ts.SyntaxKind.ArrowFunction) {
return {__symbolic: 'reference', name: lambdaTemp};
}
return value;
}
});
});
it('should be able to substitute a lambda with a reference', () => {
const source = sourceFileOf(`
var b = 1;
export var a = () => b;
`);
const expr = findVar(source, 'a');
expect(evaluator.evaluateNode(expr !.initializer !))
.toEqual({__symbolic: 'reference', name: lambdaTemp});
});
it('should be able to substitute a lambda in an expression', () => {
const source = sourceFileOf(`
var b = 1;
export var a = [
{ provide: 'someValue': useFactory: () => b }
];
`);
const expr = findVar(source, 'a');
expect(evaluator.evaluateNode(expr !.initializer !)).toEqual([
{provide: 'someValue', useFactory: {__symbolic: 'reference', name: lambdaTemp}}
])
});
})
}); });
function sourceFileOf(text: string): ts.SourceFile { function sourceFileOf(text: string): ts.SourceFile {