diff --git a/libraries/botframework-expressions/src/builtInFunction.ts b/libraries/botframework-expressions/src/builtInFunction.ts index 863e08fc4b..25194b491b 100644 --- a/libraries/botframework-expressions/src/builtInFunction.ts +++ b/libraries/botframework-expressions/src/builtInFunction.ts @@ -303,6 +303,15 @@ export class BuiltInFunctions { return error; } + public static verifyStringOrNull(value: any, expression: Expression, _: number): string { + let error: string; + if (typeof value !== 'string' && value !== undefined) { + error = `${expression} is neither a string nor a null object.`; + } + + return error; + } + /** * Verify value is a number or string. * @param value alue to check. @@ -549,7 +558,7 @@ export class BuiltInFunctions { * @param func Function to apply. */ public static stringTransform(type: string, func: (arg0: ReadonlyArray) => any): ExpressionEvaluator { - return new ExpressionEvaluator(type, BuiltInFunctions.apply(func, BuiltInFunctions.verifyString), + return new ExpressionEvaluator(type, BuiltInFunctions.apply(func, BuiltInFunctions.verifyStringOrNull), ReturnType.String, BuiltInFunctions.validateUnaryString); } @@ -677,6 +686,14 @@ export class BuiltInFunctions { }); } + private static parseStringOrNull(input: string | undefined): string { + if (typeof input === "string") { + return input; + } else { + return ""; + } + } + private static validateAccessor(expression: Expression): void { const children: Expression[] = expression.children; if (children.length === 0 @@ -1160,8 +1177,10 @@ export class BuiltInFunctions { result = str.substr(start, length); } } + } else if (str === undefined) { + result = ""; } else { - error = `${expression.children[0]} is not a string.`; + error = `${expression.children[0]} is neither a string nor a null object.`; } } @@ -1858,27 +1877,55 @@ export class BuiltInFunctions { BuiltInFunctions.validateUnary), new ExpressionEvaluator( ExpressionType.Concat, - BuiltInFunctions.apply((args: ReadonlyArray) => ''.concat(...args), BuiltInFunctions.verifyString), + BuiltInFunctions.apply((args: ReadonlyArray) => ''.concat(...args.map(arg => BuiltInFunctions.parseStringOrNull(arg))), BuiltInFunctions.verifyStringOrNull), ReturnType.String, BuiltInFunctions.validateString), new ExpressionEvaluator( ExpressionType.Length, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].length, BuiltInFunctions.verifyString), + BuiltInFunctions.apply((args: ReadonlyArray) => (BuiltInFunctions.parseStringOrNull(args[0])).length, BuiltInFunctions.verifyStringOrNull), ReturnType.Number, BuiltInFunctions.validateUnaryString), new ExpressionEvaluator( ExpressionType.Replace, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].split(args[1]).join(args[2]), BuiltInFunctions.verifyString), + BuiltInFunctions.applyWithError(( + args: ReadonlyArray) => + { + let error = undefined;1 + let result = undefined; + if (BuiltInFunctions.parseStringOrNull(args[1]).length === 0) { + error = `${args[1]} should be a string with length at least 1`; + } + + if (error === undefined) { + result = BuiltInFunctions.parseStringOrNull(args[0]).split(BuiltInFunctions.parseStringOrNull(args[1])).join(BuiltInFunctions.parseStringOrNull(args[2])) + } + + return {value: result, error}; + }, BuiltInFunctions.verifyStringOrNull), ReturnType.String, (expression: Expression): void => BuiltInFunctions.validateArityAndAnyType(expression, 3, 3, ReturnType.String)), new ExpressionEvaluator( ExpressionType.ReplaceIgnoreCase, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].replace(new RegExp(args[1], 'gi'), args[2]), BuiltInFunctions.verifyString), + BuiltInFunctions.applyWithError(( + args: ReadonlyArray) => + { + let error = undefined; + let result = undefined; + if (BuiltInFunctions.parseStringOrNull(args[1]).length === 0) { + error = `${args[1]} should be a string with length at least 1`; + } + + if (error === undefined) { + result = BuiltInFunctions.parseStringOrNull(args[0]).replace(new RegExp(BuiltInFunctions.parseStringOrNull(args[1]), 'gi'), BuiltInFunctions.parseStringOrNull(args[2])); + } + + return {value: result, error}; + }, BuiltInFunctions.verifyStringOrNull), ReturnType.String, (expression: Expression): void => BuiltInFunctions.validateArityAndAnyType(expression, 3, 3, ReturnType.String)), new ExpressionEvaluator( ExpressionType.Split, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].split(args[1]), BuiltInFunctions.verifyString), + BuiltInFunctions.apply((args: ReadonlyArray) => BuiltInFunctions.parseStringOrNull(args[0]).split(BuiltInFunctions.parseStringOrNull(args[1])), BuiltInFunctions.verifyStringOrNull), ReturnType.Object, (expression: Expression): void => BuiltInFunctions.validateArityAndAnyType(expression, 2, 2, ReturnType.String)), new ExpressionEvaluator( @@ -1886,24 +1933,24 @@ export class BuiltInFunctions { BuiltInFunctions.substring, ReturnType.String, (expression: Expression): void => BuiltInFunctions.validateOrder(expression, [ReturnType.Number], ReturnType.String, ReturnType.Number)), - BuiltInFunctions.stringTransform(ExpressionType.ToLower, (args: ReadonlyArray) => String(args[0]).toLowerCase()), - BuiltInFunctions.stringTransform(ExpressionType.ToUpper, (args: ReadonlyArray) => String(args[0]).toUpperCase()), - BuiltInFunctions.stringTransform(ExpressionType.Trim, (args: ReadonlyArray) => String(args[0]).trim()), + BuiltInFunctions.stringTransform(ExpressionType.ToLower, (args: ReadonlyArray) => String(BuiltInFunctions.parseStringOrNull(args[0])).toLowerCase()), + BuiltInFunctions.stringTransform(ExpressionType.ToUpper, (args: ReadonlyArray) => String(BuiltInFunctions.parseStringOrNull(args[0])).toUpperCase()), + BuiltInFunctions.stringTransform(ExpressionType.Trim, (args: ReadonlyArray) => String(BuiltInFunctions.parseStringOrNull(args[0])).trim()), new ExpressionEvaluator( ExpressionType.StartsWith, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].startsWith(args[1]), BuiltInFunctions.verifyString), + BuiltInFunctions.apply((args: ReadonlyArray) => BuiltInFunctions.parseStringOrNull(args[0]).startsWith(BuiltInFunctions.parseStringOrNull(args[1])), BuiltInFunctions.verifyStringOrNull), ReturnType.Boolean, (expression: Expression): void => BuiltInFunctions.validateArityAndAnyType(expression, 2, 2, ReturnType.String) ), new ExpressionEvaluator( ExpressionType.EndsWith, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].endsWith(args[1]), BuiltInFunctions.verifyString), + BuiltInFunctions.apply((args: ReadonlyArray) => BuiltInFunctions.parseStringOrNull(args[0]).endsWith(BuiltInFunctions.parseStringOrNull(args[1])), BuiltInFunctions.verifyStringOrNull), ReturnType.Boolean, (expression: Expression): void => BuiltInFunctions.validateArityAndAnyType(expression, 2, 2, ReturnType.String) ), new ExpressionEvaluator( ExpressionType.CountWord, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].trim().split(/\s+/).length, BuiltInFunctions.verifyString), + BuiltInFunctions.apply((args: ReadonlyArray) => BuiltInFunctions.parseStringOrNull(args[0]).trim().split(/\s+/).length, BuiltInFunctions.verifyStringOrNull), ReturnType.Number, BuiltInFunctions.validateUnaryString ), @@ -1921,13 +1968,13 @@ export class BuiltInFunctions { ), new ExpressionEvaluator( ExpressionType.IndexOf, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].indexOf(args[1]), BuiltInFunctions.verifyString), + BuiltInFunctions.apply((args: ReadonlyArray) => BuiltInFunctions.parseStringOrNull(args[0]).indexOf(BuiltInFunctions.parseStringOrNull(args[1])), BuiltInFunctions.verifyStringOrNull), ReturnType.Number, (expression: Expression): void => BuiltInFunctions.validateArityAndAnyType(expression, 2, 2, ReturnType.String) ), new ExpressionEvaluator( ExpressionType.LastIndexOf, - BuiltInFunctions.apply((args: ReadonlyArray) => args[0].lastIndexOf(args[1]), BuiltInFunctions.verifyString), + BuiltInFunctions.apply((args: ReadonlyArray) => BuiltInFunctions.parseStringOrNull(args[0]).lastIndexOf(BuiltInFunctions.parseStringOrNull(args[1])), BuiltInFunctions.verifyStringOrNull), ReturnType.Number, (expression: Expression): void => BuiltInFunctions.validateArityAndAnyType(expression, 2, 2, ReturnType.String) ), diff --git a/libraries/botframework-expressions/tests/badExpression.test.js b/libraries/botframework-expressions/tests/badExpression.test.js index e123ccc84b..969012079e 100644 --- a/libraries/botframework-expressions/tests/badExpression.test.js +++ b/libraries/botframework-expressions/tests/badExpression.test.js @@ -40,7 +40,9 @@ const badExpressions = "replace(one, 'l', 'k')", // replace only accept string parameter "replace('hi', 1, 'k')", // replace only accept string parameter "replace('hi', 'l', 1)", // replace only accept string parameter + "replace('hi', nullObj, 'k')", // replace oldValue must string length not less than 1 "replaceIgnoreCase(hello)", // replaceIgnoreCase need three parameters + "replaceIgnoreCase('HI', nullObj, 'k')", // replaceIgnoreCase oldValue must string length not less than 1 "replaceIgnoreCase(one, 'l', 'k')", // replaceIgnoreCase only accept string parameter "replaceIgnoreCase('hi', 1, 'k')", // replaceIgnoreCase only accept string parameter "replaceIgnoreCase('hi', 'l', 1)", // replaceIgnoreCase only accept string parameter diff --git a/libraries/botframework-expressions/tests/expression.test.js b/libraries/botframework-expressions/tests/expression.test.js index 5da0020e5b..0dcc38163e 100644 --- a/libraries/botframework-expressions/tests/expression.test.js +++ b/libraries/botframework-expressions/tests/expression.test.js @@ -69,9 +69,11 @@ const dataSource = [ // String functions tests ["concat(hello,world)", "helloworld"], + ["concat(hello,nullObj)", "hello"], ["concat('hello','world')", "helloworld"], ["concat(\"hello\",\"world\")", "helloworld"], ["length('hello')", 5], + ["length(nullObj)", 0], ["length(\"hello\")", 5], ["length(concat(hello,world))", 10], ["count('hello')", 5], @@ -79,29 +81,47 @@ const dataSource = [ ["count(concat(hello,world))", 10], ["replace('hello', 'l', 'k')", "hekko"], ["replace('hello', 'L', 'k')", "hello"], + ["replace(nullObj, 'L', 'k')", ""], + ["replace('hello', 'L', nullObj)", "hello"], ["replace(\"hello'\", \"'\", '\"')", "hello\""], ["replace('hello\"', '\"', \"'\")", "hello'"], ["replace('hello\"', '\"', '\n')", "hello\n"], ["replace('hello\n', '\n', '\\\\')", "hello\\"], ["replace('hello\\\\', '\\\\', '\\\\\\\\')", "hello\\\\"], ["replaceIgnoreCase('hello', 'L', 'k')", "hekko"], + ["replaceIgnoreCase(nullObj, 'L', 'k')", ""], + ["replaceIgnoreCase('hello', 'L', nullObj)", "heo"], ["split('hello','e')", ["h", "llo"]], + ["split(nullObj,'e')", [""]], + ["split('hello',nullObj)", ["h", "e", "l", "l", "o"]], + ["split(nullObj,nullObj)", []], ["substring('hello', 0, 5)", "hello"], ["substring('hello', 0, 3)", "hel"], ["substring('hello', 3)", "lo"], + ["substring(nullObj, 3)", ""], + ["substring(nullObj, 0, 3)", ""], ["substring('hello', 0, bag.index)", "hel"], ["toLower('UpCase')", "upcase"], + ["toLower(nullObj)", ""], ["toUpper('lowercase')", "LOWERCASE"], + ["toUpper(nullObj)", ""], ["toLower(toUpper('lowercase'))", "lowercase"], ["trim(' hello ')", "hello"], + ["trim(nullObj)", ""], ["trim(' hello')", "hello"], ["trim('hello')", "hello"], ["endsWith('hello','o')", true], ["endsWith('hello','a')", false], + ["endsWith(nullObj,'a')", false], + ["endsWith(nullObj, nullObj)", true], + ["endsWith('hello',nullObj)", true], ["endsWith(hello,'o')", true], ["endsWith(hello,'a')", false], ["startsWith('hello','h')", true], ["startsWith('hello','a')", false], + ["startsWith(nullObj,'a')", false], + ["startsWith(nullObj, nullObj)", true], + ["startsWith('hello',nullObj)", true], ["countWord(hello)", 1], ["countWord(concat(hello, ' ', world))", 2], ["addOrdinal(11)", "11th"], @@ -116,6 +136,10 @@ const dataSource = [ ["indexOf(newGuid(), '-')", 8], ["indexOf(newGuid(), '-')", 8], ["indexOf(hello, '-')", -1], + ["indexOf(nullObj, '-')", -1], + ["indexOf(hello, nullObj)", 0], + ["lastIndexOf(nullObj, '-')", -1], + ["lastIndexOf(hello, nullObj)", 5], ["lastIndexOf(newGuid(), '-')", 23], ["lastIndexOf(newGuid(), '-')", 23], ["lastIndexOf(hello, '-')", -1],