diff --git a/libraries/adaptive-expressions/src/constant.ts b/libraries/adaptive-expressions/src/constant.ts index 3a73fa7d7d..190c17d8da 100644 --- a/libraries/adaptive-expressions/src/constant.ts +++ b/libraries/adaptive-expressions/src/constant.ts @@ -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) { diff --git a/libraries/adaptive-expressions/src/expression.ts b/libraries/adaptive-expressions/src/expression.ts index 7a94b1fde6..f63468b39e 100644 --- a/libraries/adaptive-expressions/src/expression.ts +++ b/libraries/adaptive-expressions/src/expression.ts @@ -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} { + let path: string; + let refs = new Set(); + 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); } diff --git a/libraries/adaptive-expressions/src/expressionFunctions.ts b/libraries/adaptive-expressions/src/expressionFunctions.ts index 298f9f5659..15141c3255 100644 --- a/libraries/adaptive-expressions/src/expressionFunctions.ts +++ b/libraries/adaptive-expressions/src/expressionFunctions.ts @@ -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!== undefined) { + const instanceMap: Map = instance as Map; + 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); @@ -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.`; } @@ -2074,7 +2154,7 @@ export class ExpressionFunctions { found = (args[0] as Map).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; } } @@ -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; @@ -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; diff --git a/libraries/adaptive-expressions/src/extensions.ts b/libraries/adaptive-expressions/src/extensions.ts index bbd9e90f3e..f8683a0523 100644 --- a/libraries/adaptive-expressions/src/extensions.ts +++ b/libraries/adaptive-expressions/src/extensions.ts @@ -5,30 +5,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Constant } from './constant'; -import { Expression, ReturnType } from './expression'; -import { ExpressionType } from './expressionType'; /** * Some util and extension functions */ export class Extensions { - - /** - * 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 static references(expression: Expression): string[] { - const {path, refs} = this.referenceWalk(expression); - if (path !== undefined) { - refs.add(path); - } - return Array.from(refs); - } - /** * Patch method * TODO: is there any better solution? @@ -46,170 +27,4 @@ export class Extensions { return 'getValue' in obj && 'setValue' in obj && 'version' in obj && typeof obj.getValue === 'function' && typeof obj.setValue === 'function' && typeof obj.version === 'function'; } - - /** - * 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 static referenceWalk(expression: Expression, - extension?: (arg0: Expression) => boolean): {path: string; refs: Set} { - let path: string; - let refs = new Set(); - 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} = 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, refs} = Extensions.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 = Extensions.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 = 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): boolean => !(x === iteratorName || x.startsWith(iteratorName + '.') || x.startsWith(iteratorName + '['))); - refs = new Set([...refs, ...refs0, ...nonLocalRefs2]); - - } else { - for (const child of expression.children) { - const result = Extensions.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}; - } - - /** - * 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!== undefined) { - const instanceMap: Map = instance as Map; - 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 }; - } } diff --git a/libraries/adaptive-expressions/src/memory/simpleObjectMemory.ts b/libraries/adaptive-expressions/src/memory/simpleObjectMemory.ts index f20d7e9979..fcb3ae1903 100644 --- a/libraries/adaptive-expressions/src/memory/simpleObjectMemory.ts +++ b/libraries/adaptive-expressions/src/memory/simpleObjectMemory.ts @@ -1,5 +1,6 @@ import { MemoryInterface } from './memoryInterface'; import { Extensions } from '../extensions'; +import { ExpressionFunctions } from '../expressionFunctions'; /** * @module adaptive-expressions @@ -55,9 +56,9 @@ export class SimpleObjectMemory implements MemoryInterface { let error: string; const idx = parseInt(part); if(!isNaN(idx) && Array.isArray(curScope)) { - ({value, error} = Extensions.accessIndex(curScope, idx)); + ({value, error} = ExpressionFunctions.accessIndex(curScope, idx)); } else { - ({value, error} = Extensions.accessProperty(curScope, part)); + ({value, error} = ExpressionFunctions.accessProperty(curScope, part)); } if (error) { @@ -100,10 +101,10 @@ export class SimpleObjectMemory implements MemoryInterface { const idx = parseInt(parts[i]); if(!isNaN(idx) && Array.isArray(curScope)) { curPath = `[${ parts[i] }]`; - ({value: curScope, error} = Extensions.accessIndex(curScope, idx)); + ({value: curScope, error} = ExpressionFunctions.accessIndex(curScope, idx)); } else { curPath = `.${ parts[i] }`; - ({value: curScope, error} = Extensions.accessProperty(curScope, parts[i])); + ({value: curScope, error} = ExpressionFunctions.accessProperty(curScope, parts[i])); } if (error) { @@ -135,7 +136,7 @@ export class SimpleObjectMemory implements MemoryInterface { return; } } else { - error = Extensions.setProperty(curScope,parts[parts.length - 1], input).error; + error = ExpressionFunctions.setProperty(curScope,parts[parts.length - 1], input).error; if (error) { return; } diff --git a/libraries/adaptive-expressions/tests/expressionParser.test.js b/libraries/adaptive-expressions/tests/expressionParser.test.js index 43b130d119..e204861d67 100644 --- a/libraries/adaptive-expressions/tests/expressionParser.test.js +++ b/libraries/adaptive-expressions/tests/expressionParser.test.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable @typescript-eslint/no-var-requires */ -const { Expression, Extensions, SimpleObjectMemory, ExpressionFunctions } = require('../lib'); +const { Expression, SimpleObjectMemory, ExpressionFunctions } = require('../lib'); const assert = require('assert'); const moment = require('moment'); @@ -689,7 +689,7 @@ describe('expression parser functional test', () => { //Assert ExpectedRefs if (data.length === 3) { - const actualRefs = Extensions.references(parsed); + const actualRefs = parsed.references(); assertObjectEquals(actualRefs.sort(), data[2].sort()); } diff --git a/libraries/botbuilder-lg/src/analyzer.ts b/libraries/botbuilder-lg/src/analyzer.ts index 5be3a97f59..aafb5fbd67 100644 --- a/libraries/botbuilder-lg/src/analyzer.ts +++ b/libraries/botbuilder-lg/src/analyzer.ts @@ -201,7 +201,7 @@ export class Analyzer extends AbstractParseTreeVisitor implement exp = TemplateExtensions.trimExpression(exp); const parsed: Expression = this._expressionParser.parse(exp); - const references: readonly string[] = Extensions.references(parsed); + const references: readonly string[] = parsed.references(); result.union(new AnalyzerResult(references.slice(), [])); result.union(this.analyzeExpressionDirectly(parsed)); diff --git a/libraries/botbuilder-lg/src/index.ts b/libraries/botbuilder-lg/src/index.ts index cb4b167944..4a7b019965 100644 --- a/libraries/botbuilder-lg/src/index.ts +++ b/libraries/botbuilder-lg/src/index.ts @@ -7,7 +7,7 @@ */ export * from './templates'; export * from './evaluator'; -export * from './templateParser'; +export * from './templatesParser'; export * from './generated'; export * from './staticChecker'; export * from './analyzer'; diff --git a/libraries/botbuilder-lg/src/templates.ts b/libraries/botbuilder-lg/src/templates.ts index 9e8b9cca0e..dfed0bf2fa 100644 --- a/libraries/botbuilder-lg/src/templates.ts +++ b/libraries/botbuilder-lg/src/templates.ts @@ -10,11 +10,11 @@ import { Template } from './template'; import { TemplateImport } from './templateImport'; import { Diagnostic, DiagnosticSeverity } from './diagnostic'; import { ExpressionParser } from 'adaptive-expressions'; -import { ImportResolverDelegate } from './templateParser'; +import { ImportResolverDelegate } from './templatesParser'; import { Evaluator } from './evaluator'; import { Expander } from './expander'; import { Analyzer } from './analyzer'; -import { TemplateParser } from './templateParser'; +import { TemplatesParser } from './templatesParser'; import { AnalyzerResult } from './analyzerResult'; import { TemplateErrors } from './templateErrors'; import { TemplateExtensions } from './templateExtensions'; @@ -155,7 +155,7 @@ export class Templates implements Iterable