Skip to content
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
60 changes: 11 additions & 49 deletions libraries/botframework-expressions/src/builtInFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ExpressionType } from './expressionType';
import { Extensions } from './extensions';
import { TimeZoneConverter } from './timeZoneConverter';
import { convertCSharpDateTimeToMomentJS } from './formatConverter';
import { MemoryInterface, SimpleObjectMemory, ComposedMemory } from './memory';
import { MemoryInterface, SimpleObjectMemory, ComposedMemory, StackedMemory } from './memory';

/**
* Verify the result of an expression is of the appropriate type and return a string if not.
Expand Down Expand Up @@ -950,22 +950,20 @@ export class BuiltInFunctions {

if (!error) {
// 2nd parameter has been rewrite to $local.item
const iteratorName: string = (expression.children[1].children[0] as Constant).value as string;
if (!Array.isArray(collection)) {
error = `${ expression.children[0] } is not a collection to run foreach`;
} else {
const iteratorName = (expression.children[1].children[0] as Constant).value as string;
const stackedMemory = StackedMemory.wrap(state);
result = [];
for (const item of collection) {
const local: Map<string, any> = new Map<string, any>([
[iteratorName, item]
]);

const newMemory: Map<string, any> = new Map<string, MemoryInterface>([
['$global', state],
['$local', new SimpleObjectMemory(local)]
]);

const { value: r, error: e } = expression.children[2].tryEvaluate(new ComposedMemory(newMemory));
stackedMemory.push(SimpleObjectMemory.wrap(local));
const { value: r, error: e } = expression.children[2].tryEvaluate(stackedMemory);
stackedMemory.pop();
if (e !== undefined) {
return { value: undefined, error: e };
}
Expand All @@ -989,18 +987,17 @@ export class BuiltInFunctions {
if (!Array.isArray(collection)) {
error = `${ expression.children[0] } is not a collection to run where`;
} else {
const iteratorName = (expression.children[1].children[0] as Constant).value as string;
const stackedMemory = StackedMemory.wrap(state);
result = [];
for (const item of collection) {
const local: Map<string, any> = new Map<string, any>([
[iteratorName, item]
]);

const newMemory: Map<string, MemoryInterface> = new Map<string, MemoryInterface>([
['$global', state],
['$local', new SimpleObjectMemory(local)]
]);

const { value: r, error: e } = expression.children[2].tryEvaluate(new ComposedMemory(newMemory));
stackedMemory.push(SimpleObjectMemory.wrap(local));
const { value: r, error: e } = expression.children[2].tryEvaluate(stackedMemory);
stackedMemory.pop();
if (e !== undefined) {
return { value: undefined, error: e };
}
Expand Down Expand Up @@ -1028,12 +1025,6 @@ export class BuiltInFunctions {
if (!(second.type === ExpressionType.Accessor && second.children.length === 1)) {
throw new Error(`Second parameter of foreach is not an identifier : ${ second }`);
}

const iteratorName: string = second.toString();

// rewrite the 2nd, 3rd paramater
expression.children[1] = BuiltInFunctions.rewriteAccessor(expression.children[1], iteratorName);
expression.children[2] = BuiltInFunctions.rewriteAccessor(expression.children[2], iteratorName);
}

private static validateIsMatch(expression: Expression): void {
Expand All @@ -1046,35 +1037,6 @@ export class BuiltInFunctions {
}
}

private static rewriteAccessor(expression: Expression, localVarName: string): Expression {
if (expression.type === ExpressionType.Accessor) {
if (expression.children.length === 2) {
expression.children[1] = BuiltInFunctions.rewriteAccessor(expression.children[1], localVarName);
} else {
const str: string = expression.toString();
let prefix = '$global';
if (str === localVarName || str.startsWith(localVarName.concat('.'))) {
prefix = '$local';
}

expression.children = [
expression.children[0],
Expression.makeExpression(ExpressionType.Accessor, undefined, new Constant(prefix))
];
}

return expression;
} else {
// rewite children if have any
for (let idx = 0; idx < expression.children.length; idx++) {
expression.children[idx] = BuiltInFunctions.rewriteAccessor(expression.children[idx], localVarName);
}

return expression;
}

}

private static isEmpty(instance: any): boolean {
let result: boolean;
if (instance === undefined) {
Expand Down
67 changes: 41 additions & 26 deletions libraries/botframework-expressions/src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,11 @@ export class Extensions {
* @returns Hash set of the static reference paths.
*/
public static references(expression: Expression): string[] {
let references: Set<string> = new Set<string>();
const path: string = this.referenceWalk(expression, references);
const {path, refs} = this.referenceWalk(expression);
if (path !== undefined) {
references = references.add(path);
refs.add(path);
}

const filteredReferences: Set<string> = new Set<string>();
references.forEach((x: string): void => {
if (!x.startsWith('$local.')) {
if (x.startsWith('$global.')) {
filteredReferences.add(x.substr(8));
} else {
filteredReferences.add(x);
}
}
});

return Array.from(filteredReferences);
return Array.from(refs);
}

/**
Expand Down Expand Up @@ -67,9 +54,10 @@ export class Extensions {
* @param extension If present, called to override lookup for things like template expansion.
* @returns Accessor path of expression.
*/
public static referenceWalk(expression: Expression, references: Set<string>,
extension?: (arg0: Expression) => boolean): string {
public static 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) {
Expand All @@ -80,15 +68,15 @@ export class Extensions {
}

if (children.length === 2) {
path = Extensions.referenceWalk(children[1], references, extension);
({path, refs} = Extensions.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 = Extensions.referenceWalk(children[0], references, extension);
({path, refs} = Extensions.referenceWalk(children[0], extension));
if (path !== undefined) {
if (children[1] instanceof Constant) {
const cnst: Constant = children[1] as Constant;
Expand All @@ -98,24 +86,51 @@ export class Extensions {
path += `[${ cnst.value }]`;
}
} else {
references.add(path);
refs.add(path);
}
}
const idxPath: string = Extensions.referenceWalk(children[1], references, extension);
const result = Extensions.referenceWalk(children[1], extension);
const idxPath = result.path;
const refs1 = result.refs;
refs = new Set([...refs, ...refs1]);
if (idxPath !== undefined) {
references.add(idxPath);
refs.add(idxPath);
}
} else if (expression.type === ExpressionType.Foreach ||
expression.type === ExpressionType.Where ||
expression.type === ExpressionType.Select ) {
let result = Extensions.referenceWalk(children[0], extension);
const child0Path = result.path;
const refs0 = result.refs;
if (child0Path !== undefined) {
refs0.add(child0Path);
}

result = Extensions.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 => !(x === iteratorName || x.startsWith(iteratorName + '.') || x.startsWith(iteratorName + '[')));
refs = new Set([...refs, ...refs0, ...nonLocalRefs2]);

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

return path;
return {path, refs}
}

/**
Expand Down
1 change: 1 addition & 0 deletions libraries/botframework-expressions/src/memory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
export * from './memoryInterface';
export * from './composedMemory';
export * from './simpleObjectMemory';
export * from './stackedMemory';
44 changes: 44 additions & 0 deletions libraries/botframework-expressions/src/memory/stackedMemory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { MemoryInterface } from './memoryInterface';

/**
* @module botframework-expressions
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

export class StackedMemory extends Array<MemoryInterface> implements MemoryInterface {
public static wrap(memory: MemoryInterface): StackedMemory {
if (memory instanceof StackedMemory) {
return memory;
} else {
const stackedMemory = new StackedMemory();
stackedMemory.push(memory);
return stackedMemory;
}
}

public getValue(path: string): any {
if (this.length === 0) {
return undefined;
} else {
for (const memory of Array.from(this).reverse()) {
if (memory.getValue(path) !== undefined) {
return memory.getValue(path);
}
}

return undefined;
}
}

public setValue(_path: string, _value: any): void {
throw new Error(`Can't set value to ${ _path }, stacked memory is read-only`);
}

public version(): string {
return '0';
}
}
10 changes: 9 additions & 1 deletion libraries/botframework-expressions/tests/expression.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ const dataSource = [
['join(foreach(items, item, item), \',\')', 'zero,one,two'],
['join(foreach(nestedItems, i, i.x + first(nestedItems).x), \',\')', '2,3,4', ['nestedItems']],
['join(foreach(items, item, concat(item, string(count(items)))), \',\')', 'zero3,one3,two3', ['items']],
['join(foreach(doubleNestedItems, items, join(foreach(items, item, item.x), ",")), ",")', '1,2,3'],
['join(foreach(doubleNestedItems, items, join(foreach(items, item, concat(y, string(item.x))), ",")), ",")', 'y1,y2,y3'],
['count(where(doubleNestedItems, items, count(where(items, item, item.x == 1)) == 1))', 1],
['count(where(doubleNestedItems, items, count(where(items, item, count(items) == 1)) == 1))', 1],
['join(select(items, item, item), \',\')', 'zero,one,two'],
['join(select(nestedItems, i, i.x + first(nestedItems).x), \',\')', '2,3,4', ['nestedItems']],
['join(select(items, item, concat(item, string(count(items)))), \',\')', 'zero3,one3,two3', ['items']],
Expand Down Expand Up @@ -551,7 +555,11 @@ const scope = {
options: { xxx: 'options', yyy : ['optionY1', 'optionY2' ] },
title: 'Dialog Title',
subTitle: 'Dialog Sub Title'
}
},
doubleNestedItems: [
[{ x: 1 }, { x: 2 }],
[{ x: 3 }],
],
};

describe('expression functional test', () => {
Expand Down