When testing JIT code, it is useful to be able to access the generated JIT source. Previously this is done by spying on the global `Function` object, to capture the code when it is being evaluated. This is problematic because you can only capture the body of the function, and not the arguments, which messes up line and column positions for source mapping for instance. Now the code that generates and then evaluates JIT code is wrapped in a `JitEvaluator` class, making it possible to provide a mock implementation that can capture the generated source of the function passed to `executeFunction(fn: Function, args: any[])`. PR Close #28055
153 lines
5.7 KiB
TypeScript
153 lines
5.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {identifierName} from '../compile_metadata';
|
|
import {CompileReflector} from '../compile_reflector';
|
|
|
|
import {EmitterVisitorContext} from './abstract_emitter';
|
|
import {AbstractJsEmitterVisitor} from './abstract_js_emitter';
|
|
import * as o from './output_ast';
|
|
|
|
/**
|
|
* A helper class to manage the evaluation of JIT generated code.
|
|
*/
|
|
export class JitEvaluator {
|
|
/**
|
|
*
|
|
* @param sourceUrl The URL of the generated code.
|
|
* @param statements An array of Angular statement AST nodes to be evaluated.
|
|
* @param reflector A helper used when converting the statements to executable code.
|
|
* @param createSourceMaps If true then create a source-map for the generated code and include it
|
|
* inline as a source-map comment.
|
|
* @returns A map of all the variables in the generated code.
|
|
*/
|
|
evaluateStatements(
|
|
sourceUrl: string, statements: o.Statement[], reflector: CompileReflector,
|
|
createSourceMaps: boolean): {[key: string]: any} {
|
|
const converter = new JitEmitterVisitor(reflector);
|
|
const ctx = EmitterVisitorContext.createRoot();
|
|
converter.visitAllStatements(statements, ctx);
|
|
converter.createReturnStmt(ctx);
|
|
return this.evaluateCode(sourceUrl, ctx, converter.getArgs(), createSourceMaps);
|
|
}
|
|
|
|
/**
|
|
* Evaluate a piece of JIT generated code.
|
|
* @param sourceUrl The URL of this generated code.
|
|
* @param ctx A context object that contains an AST of the code to be evaluated.
|
|
* @param vars A map containing the names and values of variables that the evaluated code might
|
|
* reference.
|
|
* @param createSourceMap If true then create a source-map for the generated code and include it
|
|
* inline as a source-map comment.
|
|
* @returns The result of evaluating the code.
|
|
*/
|
|
evaluateCode(
|
|
sourceUrl: string, ctx: EmitterVisitorContext, vars: {[key: string]: any},
|
|
createSourceMap: boolean): any {
|
|
let fnBody = `${ctx.toSource()}\n//# sourceURL=${sourceUrl}`;
|
|
const fnArgNames: string[] = [];
|
|
const fnArgValues: any[] = [];
|
|
for (const argName in vars) {
|
|
fnArgValues.push(vars[argName]);
|
|
fnArgNames.push(argName);
|
|
}
|
|
if (createSourceMap) {
|
|
// using `new Function(...)` generates a header, 1 line of no arguments, 2 lines otherwise
|
|
// E.g. ```
|
|
// function anonymous(a,b,c
|
|
// /**/) { ... }```
|
|
// We don't want to hard code this fact, so we auto detect it via an empty function first.
|
|
const emptyFn = new Function(...fnArgNames.concat('return null;')).toString();
|
|
const headerLines = emptyFn.slice(0, emptyFn.indexOf('return null;')).split('\n').length - 1;
|
|
fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, headerLines).toJsComment()}`;
|
|
}
|
|
const fn = new Function(...fnArgNames.concat(fnBody));
|
|
return this.executeFunction(fn, fnArgValues);
|
|
}
|
|
|
|
/**
|
|
* Execute a JIT generated function by calling it.
|
|
*
|
|
* This method can be overridden in tests to capture the functions that are generated
|
|
* by this `JitEvaluator` class.
|
|
*
|
|
* @param fn A function to execute.
|
|
* @param args The arguments to pass to the function being executed.
|
|
* @returns The return value of the executed function.
|
|
*/
|
|
executeFunction(fn: Function, args: any[]) { return fn(...args); }
|
|
}
|
|
|
|
/**
|
|
* An Angular AST visitor that converts AST nodes into executable JavaScript code.
|
|
*/
|
|
export class JitEmitterVisitor extends AbstractJsEmitterVisitor {
|
|
private _evalArgNames: string[] = [];
|
|
private _evalArgValues: any[] = [];
|
|
private _evalExportedVars: string[] = [];
|
|
|
|
constructor(private reflector: CompileReflector) { super(); }
|
|
|
|
createReturnStmt(ctx: EmitterVisitorContext) {
|
|
const stmt = new o.ReturnStatement(new o.LiteralMapExpr(this._evalExportedVars.map(
|
|
resultVar => new o.LiteralMapEntry(resultVar, o.variable(resultVar), false))));
|
|
stmt.visitStatement(this, ctx);
|
|
}
|
|
|
|
getArgs(): {[key: string]: any} {
|
|
const result: {[key: string]: any} = {};
|
|
for (let i = 0; i < this._evalArgNames.length; i++) {
|
|
result[this._evalArgNames[i]] = this._evalArgValues[i];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
|
|
this._emitReferenceToExternal(ast, this.reflector.resolveExternalReference(ast.value), ctx);
|
|
return null;
|
|
}
|
|
|
|
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): any {
|
|
this._emitReferenceToExternal(ast, ast.node, ctx);
|
|
return null;
|
|
}
|
|
|
|
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any {
|
|
if (stmt.hasModifier(o.StmtModifier.Exported)) {
|
|
this._evalExportedVars.push(stmt.name);
|
|
}
|
|
return super.visitDeclareVarStmt(stmt, ctx);
|
|
}
|
|
|
|
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: EmitterVisitorContext): any {
|
|
if (stmt.hasModifier(o.StmtModifier.Exported)) {
|
|
this._evalExportedVars.push(stmt.name);
|
|
}
|
|
return super.visitDeclareFunctionStmt(stmt, ctx);
|
|
}
|
|
|
|
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any {
|
|
if (stmt.hasModifier(o.StmtModifier.Exported)) {
|
|
this._evalExportedVars.push(stmt.name);
|
|
}
|
|
return super.visitDeclareClassStmt(stmt, ctx);
|
|
}
|
|
|
|
private _emitReferenceToExternal(ast: o.Expression, value: any, ctx: EmitterVisitorContext):
|
|
void {
|
|
let id = this._evalArgValues.indexOf(value);
|
|
if (id === -1) {
|
|
id = this._evalArgValues.length;
|
|
this._evalArgValues.push(value);
|
|
const name = identifierName({reference: value}) || 'val';
|
|
this._evalArgNames.push(`jit_${name}_${id}`);
|
|
}
|
|
ctx.print(ast, this._evalArgNames[id]);
|
|
}
|
|
}
|