packages/validation/src/ast-serialization.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import * as AST from '@aurelia/expression-parser';
import { ErrorNames, createMappedError } from './errors';
const astVisit = AST.astVisit;
enum ASTExpressionTypes {
BindingBehaviorExpression = 'BindingBehaviorExpression',
ValueConverterExpression = 'ValueConverterExpression',
AssignExpression = 'AssignExpression',
ConditionalExpression = 'ConditionalExpression',
AccessThisExpression = 'AccessThisExpression',
AccessBoundaryExpression = 'AccessBoundaryExpression',
AccessScopeExpression = 'AccessScopeExpression',
AccessMemberExpression = 'AccessMemberExpression',
AccessKeyedExpression = 'AccessKeyedExpression',
CallScopeExpression = 'CallScopeExpression',
CallMemberExpression = 'CallMemberExpression',
CallFunctionExpression = 'CallFunctionExpression',
BinaryExpression = 'BinaryExpression',
UnaryExpression = 'UnaryExpression',
PrimitiveLiteralExpression = 'PrimitiveLiteralExpression',
ArrayLiteralExpression = 'ArrayLiteralExpression',
ObjectLiteralExpression = 'ObjectLiteralExpression',
TemplateExpression = 'TemplateExpression',
TaggedTemplateExpression = 'TaggedTemplateExpression',
ArrayBindingPattern = 'ArrayBindingPattern',
ObjectBindingPattern = 'ObjectBindingPattern',
BindingIdentifier = 'BindingIdentifier',
ForOfStatement = 'ForOfStatement',
Interpolation = 'Interpolation',
DestructuringAssignment = 'DestructuringAssignment',
DestructuringSingleAssignment = 'DestructuringSingleAssignment',
DestructuringRestAssignment = 'DestructuringRestAssignment',
ArrowFunction = 'ArrowFunction',
Custom = 'Custom',
}
export interface IExpressionHydrator {
hydrate(jsonExpr: any): any;
}
export class Deserializer implements IExpressionHydrator {
public static deserialize(serializedExpr: string): AST.IsExpressionOrStatement {
const deserializer = new Deserializer();
const raw = JSON.parse(serializedExpr);
return deserializer.hydrate(raw);
}
public hydrate(raw: any): any {
switch (raw.$TYPE) {
case ASTExpressionTypes.AccessMemberExpression: {
const expr: Pick<AST.AccessMemberExpression, 'object' | 'name'> = raw;
return new AST.AccessMemberExpression(this.hydrate(expr.object), expr.name);
}
case ASTExpressionTypes.AccessKeyedExpression: {
const expr: Pick<AST.AccessKeyedExpression, 'object' | 'key'> = raw;
return new AST.AccessKeyedExpression(this.hydrate(expr.object), this.hydrate(expr.key));
}
case ASTExpressionTypes.AccessThisExpression: {
const expr: Pick<AST.AccessThisExpression, 'ancestor'> = raw;
return new AST.AccessThisExpression(expr.ancestor);
}
case ASTExpressionTypes.AccessBoundaryExpression: {
return new AST.AccessBoundaryExpression();
}
case ASTExpressionTypes.AccessScopeExpression: {
const expr: Pick<AST.AccessScopeExpression, 'name' | 'ancestor'> = raw;
return new AST.AccessScopeExpression(expr.name, expr.ancestor);
}
case ASTExpressionTypes.ArrayLiteralExpression: {
const expr: Pick<AST.ArrayLiteralExpression, 'elements'> = raw;
return new AST.ArrayLiteralExpression(this.hydrate(expr.elements));
}
case ASTExpressionTypes.ObjectLiteralExpression: {
const expr: Pick<AST.ObjectLiteralExpression, 'keys' | 'values'> = raw;
return new AST.ObjectLiteralExpression(this.hydrate(expr.keys), this.hydrate(expr.values));
}
case ASTExpressionTypes.PrimitiveLiteralExpression: {
const expr: Pick<AST.PrimitiveLiteralExpression, 'value'> = raw;
return new AST.PrimitiveLiteralExpression(this.hydrate(expr.value));
}
case ASTExpressionTypes.CallFunctionExpression: {
const expr: Pick<AST.CallFunctionExpression, 'func' | 'args'> = raw;
return new AST.CallFunctionExpression(this.hydrate(expr.func), this.hydrate(expr.args));
}
case ASTExpressionTypes.CallMemberExpression: {
const expr: Pick<AST.CallMemberExpression, 'object' | 'name' | 'args'> = raw;
return new AST.CallMemberExpression(this.hydrate(expr.object), expr.name, this.hydrate(expr.args));
}
case ASTExpressionTypes.CallScopeExpression: {
const expr: Pick<AST.CallScopeExpression, 'name' | 'args' | 'ancestor'> = raw;
return new AST.CallScopeExpression(expr.name, this.hydrate(expr.args), expr.ancestor);
}
case ASTExpressionTypes.TemplateExpression: {
const expr: Pick<AST.TemplateExpression, 'cooked' | 'expressions'> = raw;
return new AST.TemplateExpression(this.hydrate(expr.cooked), this.hydrate(expr.expressions));
}
case ASTExpressionTypes.TaggedTemplateExpression: {
const expr: Pick<AST.TaggedTemplateExpression, 'cooked' | 'func' | 'expressions'> & {
raw: any;
} = raw;
return new AST.TaggedTemplateExpression(this.hydrate(expr.cooked), this.hydrate(expr.raw), this.hydrate(expr.func), this.hydrate(expr.expressions));
}
case ASTExpressionTypes.UnaryExpression: {
const expr: Pick<AST.UnaryExpression, 'operation' | 'expression'> = raw;
return new AST.UnaryExpression(expr.operation, this.hydrate(expr.expression));
}
case ASTExpressionTypes.BinaryExpression: {
const expr: Pick<AST.BinaryExpression, 'operation' | 'left' | 'right'> = raw;
return new AST.BinaryExpression(expr.operation, this.hydrate(expr.left), this.hydrate(expr.right));
}
case ASTExpressionTypes.ConditionalExpression: {
const expr: Pick<AST.ConditionalExpression, 'condition' | 'yes' | 'no'> = raw;
return new AST.ConditionalExpression(this.hydrate(expr.condition), this.hydrate(expr.yes), this.hydrate(expr.no));
}
case ASTExpressionTypes.AssignExpression: {
const expr: Pick<AST.AssignExpression, 'target' | 'value'> = raw;
return new AST.AssignExpression(this.hydrate(expr.target), this.hydrate(expr.value));
}
case ASTExpressionTypes.ValueConverterExpression: {
const expr: Pick<AST.ValueConverterExpression, 'expression' | 'name' | 'args'> = raw;
return new AST.ValueConverterExpression(this.hydrate(expr.expression), expr.name, this.hydrate(expr.args));
}
case ASTExpressionTypes.BindingBehaviorExpression: {
const expr: Pick<AST.BindingBehaviorExpression, 'expression' | 'name' | 'args'> = raw;
return new AST.BindingBehaviorExpression(this.hydrate(expr.expression), expr.name, this.hydrate(expr.args));
}
case ASTExpressionTypes.ArrayBindingPattern: {
const expr: Pick<AST.ArrayBindingPattern, 'elements'> = raw;
return new AST.ArrayBindingPattern(this.hydrate(expr.elements));
}
case ASTExpressionTypes.ObjectBindingPattern: {
const expr: Pick<AST.ObjectBindingPattern, 'keys' | 'values'> = raw;
return new AST.ObjectBindingPattern(this.hydrate(expr.keys), this.hydrate(expr.values));
}
case ASTExpressionTypes.BindingIdentifier: {
const expr: Pick<AST.BindingIdentifier, 'name'> = raw;
return new AST.BindingIdentifier(expr.name);
}
case ASTExpressionTypes.ForOfStatement: {
const expr: Pick<AST.ForOfStatement, 'declaration' | 'iterable' | 'semiIdx'> = raw;
return new AST.ForOfStatement(this.hydrate(expr.declaration), this.hydrate(expr.iterable), this.hydrate(expr.semiIdx));
}
case ASTExpressionTypes.Interpolation: {
const expr: {
cooked: any;
expressions: any;
} = raw;
return new AST.Interpolation(this.hydrate(expr.cooked), this.hydrate(expr.expressions));
}
case ASTExpressionTypes.DestructuringAssignment: {
return new AST.DestructuringAssignmentExpression(this.hydrate(raw.$kind), this.hydrate(raw.list), this.hydrate(raw.source), this.hydrate(raw.initializer));
}
case ASTExpressionTypes.DestructuringSingleAssignment: {
return new AST.DestructuringAssignmentSingleExpression(this.hydrate(raw.target), this.hydrate(raw.source), this.hydrate(raw.initializer));
}
case ASTExpressionTypes.DestructuringRestAssignment: {
return new AST.DestructuringAssignmentRestExpression(this.hydrate(raw.target), this.hydrate(raw.indexOrProperties));
}
case ASTExpressionTypes.ArrowFunction: {
return new AST.ArrowFunction(this.hydrate(raw.parameters), this.hydrate(raw.body), this.hydrate(raw.rest));
}
default:
if (Array.isArray(raw)) {
if (typeof raw[0] === 'object') {
return this.deserializeExpressions(raw);
} else {
return raw.map(deserializePrimitive);
}
} else if (typeof raw !== 'object') {
return deserializePrimitive(raw);
}
throw createMappedError(ErrorNames.unable_to_deserialize_expression, raw);
}
}
private deserializeExpressions(exprs: unknown[]) {
const expressions: AST.IsExpressionOrStatement[] = [];
for (const expr of exprs) {
expressions.push(this.hydrate(expr));
}
return expressions;
}
}
export class Serializer implements AST.IVisitor<string> {
public static serialize(expr: AST.IsExpressionOrStatement | AST.CustomExpression): string {
const visitor = new Serializer();
if (expr == null) {
return `${expr}`;
}
return astVisit(expr, visitor);
}
public visitAccessMember(expr: AST.AccessMemberExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.AccessMemberExpression}","name":"${expr.name}","object":${astVisit(expr.object, this)}}`;
}
public visitAccessKeyed(expr: AST.AccessKeyedExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.AccessKeyedExpression}","object":${astVisit(expr.object, this)},"key":${astVisit(expr.key, this)}}`;
}
public visitAccessThis(expr: AST.AccessThisExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.AccessThisExpression}","ancestor":${expr.ancestor}}`;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public visitAccessBoundary(expr: AST.AccessBoundaryExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.AccessBoundaryExpression}"}`;
}
public visitAccessScope(expr: AST.AccessScopeExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.AccessScopeExpression}","name":"${expr.name}","ancestor":${expr.ancestor}}`;
}
public visitArrayLiteral(expr: AST.ArrayLiteralExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.ArrayLiteralExpression}","elements":${this.serializeExpressions(expr.elements)}}`;
}
public visitObjectLiteral(expr: AST.ObjectLiteralExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.ObjectLiteralExpression}","keys":${serializePrimitives(expr.keys)},"values":${this.serializeExpressions(expr.values)}}`;
}
public visitPrimitiveLiteral(expr: AST.PrimitiveLiteralExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.PrimitiveLiteralExpression}","value":${serializePrimitive(expr.value)}}`;
}
public visitCallFunction(expr: AST.CallFunctionExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.CallFunctionExpression}","func":${astVisit(expr.func, this)},"args":${this.serializeExpressions(expr.args)}}`;
}
public visitCallMember(expr: AST.CallMemberExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.CallMemberExpression}","name":"${expr.name}","object":${astVisit(expr.object, this)},"args":${this.serializeExpressions(expr.args)}}`;
}
public visitCallScope(expr: AST.CallScopeExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.CallScopeExpression}","name":"${expr.name}","ancestor":${expr.ancestor},"args":${this.serializeExpressions(expr.args)}}`;
}
public visitTemplate(expr: AST.TemplateExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.TemplateExpression}","cooked":${serializePrimitives(expr.cooked)},"expressions":${this.serializeExpressions(expr.expressions)}}`;
}
public visitTaggedTemplate(expr: AST.TaggedTemplateExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.TaggedTemplateExpression}","cooked":${serializePrimitives(expr.cooked)},"raw":${serializePrimitives(expr.cooked.raw as readonly unknown[])},"func":${astVisit(expr.func, this)},"expressions":${this.serializeExpressions(expr.expressions)}}`;
}
public visitUnary(expr: AST.UnaryExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.UnaryExpression}","operation":"${expr.operation}","expression":${astVisit(expr.expression, this)}}`;
}
public visitBinary(expr: AST.BinaryExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.BinaryExpression}","operation":"${expr.operation}","left":${astVisit(expr.left, this)},"right":${astVisit(expr.right, this)}}`;
}
public visitConditional(expr: AST.ConditionalExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.ConditionalExpression}","condition":${astVisit(expr.condition, this)},"yes":${astVisit(expr.yes, this)},"no":${astVisit(expr.no, this)}}`;
}
public visitAssign(expr: AST.AssignExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.AssignExpression}","target":${astVisit(expr.target, this)},"value":${astVisit(expr.value, this)}}`;
}
public visitValueConverter(expr: AST.ValueConverterExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.ValueConverterExpression}","name":"${expr.name}","expression":${astVisit(expr.expression, this)},"args":${this.serializeExpressions(expr.args)}}`;
}
public visitBindingBehavior(expr: AST.BindingBehaviorExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.BindingBehaviorExpression}","name":"${expr.name}","expression":${astVisit(expr.expression, this)},"args":${this.serializeExpressions(expr.args)}}`;
}
public visitArrayBindingPattern(expr: AST.ArrayBindingPattern): string {
return `{"$TYPE":"${ASTExpressionTypes.ArrayBindingPattern}","elements":${this.serializeExpressions(expr.elements)}}`;
}
public visitObjectBindingPattern(expr: AST.ObjectBindingPattern): string {
return `{"$TYPE":"${ASTExpressionTypes.ObjectBindingPattern}","keys":${serializePrimitives(expr.keys)},"values":${this.serializeExpressions(expr.values)}}`;
}
public visitBindingIdentifier(expr: AST.BindingIdentifier): string {
return `{"$TYPE":"${ASTExpressionTypes.BindingIdentifier}","name":"${expr.name}"}`;
}
public visitForOfStatement(expr: AST.ForOfStatement): string {
return `{"$TYPE":"${ASTExpressionTypes.ForOfStatement}","declaration":${astVisit(expr.declaration, this)},"iterable":${astVisit(expr.iterable, this)},"semiIdx":${serializePrimitive(expr.semiIdx)}}`;
}
public visitInterpolation(expr: AST.Interpolation): string {
return `{"$TYPE":"${ASTExpressionTypes.Interpolation}","cooked":${serializePrimitives(expr.parts)},"expressions":${this.serializeExpressions(expr.expressions)}}`;
}
public visitDestructuringAssignmentExpression(expr: AST.DestructuringAssignmentExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.DestructuringAssignment}","$kind":${serializePrimitive(expr.$kind)},"list":${this.serializeExpressions(expr.list)},"source":${expr.source === void 0 ? serializePrimitive(expr.source) : astVisit(expr.source, this)},"initializer":${expr.initializer === void 0 ? serializePrimitive(expr.initializer) : astVisit(expr.initializer, this)}}`;
}
public visitDestructuringAssignmentSingleExpression(expr: AST.DestructuringAssignmentSingleExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.DestructuringSingleAssignment}","source":${astVisit(expr.source, this)},"target":${astVisit(expr.target, this)},"initializer":${expr.initializer === void 0 ? serializePrimitive(expr.initializer) : astVisit(expr.initializer, this)}}`;
}
public visitDestructuringAssignmentRestExpression(expr: AST.DestructuringAssignmentRestExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.DestructuringRestAssignment}","target":${astVisit(expr.target, this)},"indexOrProperties":${Array.isArray(expr.indexOrProperties) ? serializePrimitives(expr.indexOrProperties) : serializePrimitive(expr.indexOrProperties)}}`;
}
public visitArrowFunction(expr: AST.ArrowFunction): string {
return `{"$TYPE":"${ASTExpressionTypes.ArrowFunction}","parameters":${this.serializeExpressions(expr.args)},"body":${astVisit(expr.body, this)},"rest":${serializePrimitive(expr.rest)}}`;
}
public visitCustom(expr: AST.CustomExpression): string {
return `{"$TYPE":"${ASTExpressionTypes.Custom}","body":${expr.value}}`;
}
private serializeExpressions(args: readonly AST.IsExpressionOrStatement[]): string {
let text = '[';
for (let i = 0, ii = args.length; i < ii; ++i) {
if (i !== 0) {
text += ',';
}
text += astVisit(args[i], this);
}
text += ']';
return text;
}
}
export function serializePrimitives(values: readonly unknown[]): string {
let text = '[';
for (let i = 0, ii = values.length; i < ii; ++i) {
if (i !== 0) {
text += ',';
}
text += serializePrimitive(values[i]);
}
text += ']';
return text;
}
export function serializePrimitive(value: unknown): string {
if (typeof value === 'string') {
return `"\\"${escapeString(value)}\\""`;
} else if (value == null) {
return `"${value}"`;
} else {
return `${value}`;
}
}
function escapeString(str: string): string {
let ret = '';
for (let i = 0, ii = str.length; i < ii; ++i) {
ret += escape(str.charAt(i));
}
return ret;
}
function escape(ch: string): string {
switch (ch) {
case '\b': return '\\b';
case '\t': return '\\t';
case '\n': return '\\n';
case '\v': return '\\v';
case '\f': return '\\f';
case '\r': return '\\r';
case '"': return '\\"';
// case '\'': return '\\\''; /* when used in serialization context, escaping `'` (single quote) is not needed as the string is wrapped in a par of `"` (double quote) */
case '\\': return '\\\\';
default: return ch;
}
}
export function deserializePrimitive(value: unknown): any {
if (typeof value === 'string') {
if (value === 'null') { return null; }
if (value === 'undefined') { return undefined; }
return value.substring(1, value.length - 1);
} else {
return value;
}
}