Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move deepEquals and deferences from extensions to Expression class #1927

Merged
merged 5 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions libraries/adaptive-expressions/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ export class Constant extends Expression {
this.value = value;
}


public deepEquals(other: Expression): boolean {
let eq: boolean;
if (!other || other.type !== this.type) {
eq = false;
} else {
let otherVal = (other as Constant).value;
eq = this.value === otherVal;
}

return eq;
}

public toString(): string {

if (this.value === undefined) {
Expand Down
136 changes: 136 additions & 0 deletions libraries/adaptive-expressions/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,142 @@ export class Expression {
}
}

/**
* Do a deep equality between expressions.
* @param other Other expression.
* @returns True if expressions are the same.
*/
public deepEquals(other: Expression): boolean {
let eq = false;
if (!other) {
eq = this.type === other.type;
if (eq) {
eq = this.children.length === other.children.length;
if (this.type === ExpressionType.And || this.type === ExpressionType.Or) {
// And/Or do not depand on order
for(let i = 0; eq && i< this.children.length; i++) {
const primary = this.children[0];
let found = false;
for (var j = 0; j < this.children.length; j++) {
if (primary.deepEquals(other.children[j])) {
found = true;
break;
}
}

eq = found;
}
} else {
for (let i = 0; eq && i< this.children.length; i++) {
eq = this.children[i].deepEquals(other.children[i]);
}
}
}
}
return eq;
}

/**
* Return the static reference paths to memory.
* Return all static paths to memory. If there is a computed element index, then the path is terminated there,
* but you might get other paths from the computed part as well.
* @param expression Expression to get references from.
* @returns List of the static reference paths.
*/
public references(): string[] {
const {path, refs} = this.referenceWalk(this);
if (path !== undefined) {
refs.add(path);
}
return Array.from(refs);
}

/**
* Walking function for identifying static memory references in an expression.
* @param expression Expression to analyze.
* @param references Tracking for references found.
* @param extension If present, called to override lookup for things like template expansion.
* @returns Accessor path of expression.
*/
public referenceWalk(expression: Expression,
extension?: (arg0: Expression) => boolean): {path: string; refs: Set<string>} {
let path: string;
let refs = new Set<string>();
if (extension === undefined || !extension(expression)) {
const children: Expression[] = expression.children;
if (expression.type === ExpressionType.Accessor) {
const prop: string = (children[0] as Constant).value as string;

if (children.length === 1) {
path = prop;
}

if (children.length === 2) {
({path, refs} = this.referenceWalk(children[1], extension));
if (path !== undefined) {
path = path.concat('.', prop);
}
// if path is null we still keep it null, won't append prop
// because for example, first(items).x should not return x as refs
}
} else if (expression.type === ExpressionType.Element) {
({path, refs} = this.referenceWalk(children[0], extension));
if (path !== undefined) {
if (children[1] instanceof Constant) {
const cnst: Constant = children[1] as Constant;
if (cnst.returnType === ReturnType.String) {
path += `.${ cnst.value }`;
} else {
path += `[${ cnst.value }]`;
}
} else {
refs.add(path);
}
}
const result = this.referenceWalk(children[1], extension);
const idxPath = result.path;
const refs1 = result.refs;
refs = new Set([...refs, ...refs1]);
if (idxPath !== undefined) {
refs.add(idxPath);
}
} else if (expression.type === ExpressionType.Foreach ||
expression.type === ExpressionType.Where ||
expression.type === ExpressionType.Select ) {
let result = this.referenceWalk(children[0], extension);
const child0Path = result.path;
const refs0 = result.refs;
if (child0Path !== undefined) {
refs0.add(child0Path);
}

result = this.referenceWalk(children[2], extension);
const child2Path = result.path;
const refs2 = result.refs;
if (child2Path !== undefined) {
refs2.add(child2Path);
}

const iteratorName = (children[1].children[0] as Constant).value as string;
var nonLocalRefs2 = Array.from(refs2).filter((x): boolean => !(x === iteratorName || x.startsWith(iteratorName + '.') || x.startsWith(iteratorName + '[')));
refs = new Set([...refs, ...refs0, ...nonLocalRefs2]);

} else {
for (const child of expression.children) {
const result = this.referenceWalk(child, extension);
const childPath = result.path;
const refs0 = result.refs;
refs = new Set([...refs, ...refs0]);
if (childPath !== undefined) {
refs.add(childPath);
}
}
}
}

return {path, refs};
}

public static parse(expression: string, lookup?: EvaluatorLookup): Expression {
return new ExpressionParser(lookup || Expression.lookup).parse(expression);
}
Expand Down
90 changes: 85 additions & 5 deletions libraries/adaptive-expressions/src/expressionFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,86 @@ export class ExpressionFunctions {
(expr: Expression): void => ExpressionFunctions.validateArityAndAnyType(expr, 2, 3, ReturnType.String, ReturnType.Number));
}

/**
* Lookup a property in IDictionary, JObject or through reflection.
* @param instance Instance with property.
* @param property Property to lookup.
* @returns Value and error information if any.
*/
public static accessProperty(instance: any, property: string): { value: any; error: string } {
// NOTE: This returns null rather than an error if property is not present
if (!instance) {
return { value: undefined, error: undefined };
}

let value: any;
let error: string;
// todo, Is there a better way to access value, or any case is not listed below?
if (instance instanceof Map && instance as Map<string, any>!== undefined) {
const instanceMap: Map<string, any> = instance as Map<string, any>;
value = instanceMap.get(property);
if (value === undefined) {
const prop: string = Array.from(instanceMap.keys()).find((k: string): boolean => k.toLowerCase() === property.toLowerCase());
if (prop !== undefined) {
value = instanceMap.get(prop);
}
}
} else {
const prop: string = Object.keys(instance).find((k: string): boolean => k.toLowerCase() === property.toLowerCase());
if (prop !== undefined) {
value = instance[prop];
}
}

return { value, error };
}

/**
* Set a property in Map or Object.
* @param instance Instance to set.
* @param property Property to set.
* @param value Value to set.
* @returns set value.
*/
public static setProperty(instance: any, property: string, value: any): { value: any; error: string } {
const result: any = value;
if (instance instanceof Map) {
instance.set(property, value);
} else {
instance[property] = value;
}

return {value: result, error: undefined};
}

/**
* Lookup a property in IDictionary, JObject or through reflection.
* @param instance Instance with property.
* @param property Property to lookup.
* @returns Value and error information if any.
*/
public static accessIndex(instance: any, index: number): { value: any; error: string } {
// NOTE: This returns null rather than an error if property is not present
if (instance === null || instance === undefined) {
return { value: undefined, error: undefined };
}

let value: any;
let error: string;

if (Array.isArray(instance)) {
if (index >= 0 && index < instance.length) {
value = instance[index];
} else {
error = `${ index } is out of range for ${ instance }`;
}
} else {
error = `${ instance } is not a collection.`;
}

return { value, error };
}

private static parseTimestamp(timeStamp: string, transform?: (arg0: moment.Moment) => any): { value: any; error: string } {
let value: any;
const error: string = this.verifyISOTimestamp(timeStamp);
Expand Down Expand Up @@ -937,9 +1017,9 @@ export class ExpressionFunctions {
({ value: idxValue, error } = index.tryEvaluate(state));
if (!error) {
if (Number.isInteger(idxValue)) {
({ value, error } = Extensions.accessIndex(inst, Number(idxValue)));
({ value, error } = ExpressionFunctions.accessIndex(inst, Number(idxValue)));
} else if (typeof idxValue === 'string') {
({ value, error } = Extensions.accessProperty(inst, idxValue.toString()));
({ value, error } = ExpressionFunctions.accessProperty(inst, idxValue.toString()));
} else {
error = `Could not coerce ${ index } to an int or string.`;
}
Expand Down Expand Up @@ -2074,7 +2154,7 @@ export class ExpressionFunctions {
found = (args[0] as Map<string, any>).get(args[1]) !== undefined;
} else if (typeof args[1] === 'string') {
let value: any;
({ value, error } = Extensions.accessProperty(args[0], args[1]));
({ value, error } = ExpressionFunctions.accessProperty(args[0], args[1]));
found = !error && value !== undefined;
}
}
Expand Down Expand Up @@ -2874,7 +2954,7 @@ export class ExpressionFunctions {
}

if (Array.isArray(args[0]) && args[0].length > 0) {
first = Extensions.accessIndex(args[0], 0).value;
first = ExpressionFunctions.accessIndex(args[0], 0).value;
}

return first;
Expand All @@ -2891,7 +2971,7 @@ export class ExpressionFunctions {
}

if (Array.isArray(args[0]) && args[0].length > 0) {
last = Extensions.accessIndex(args[0], args[0].length - 1).value;
last = ExpressionFunctions.accessIndex(args[0], args[0].length - 1).value;
}

return last;
Expand Down
Loading