• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 363877

10 Aug 2023 08:52PM UTC coverage: 79.092% (+0.1%) from 78.979%
363877

Pull #6655

CI-PR build

web-flow
Merge 94ad1d11f into fdaed8b69
Pull Request #6655: Implementation of Teams batch APIs

26094 of 32992 relevant lines covered (79.09%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.95
/libraries/AdaptiveExpressions/parser/ExpressionParser.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Generic;
6
using System.Data;
7
using System.Globalization;
8
using System.Linq;
9
using System.Text.RegularExpressions;
10
using Antlr4.Runtime;
11
using Antlr4.Runtime.Misc;
12
using Antlr4.Runtime.Tree;
13

14
namespace AdaptiveExpressions
15
{
16
    /// <summary>
17
    /// Parser to turn strings into an <see cref="Expression"/>.
18
    /// </summary>
19
    public class ExpressionParser : IExpressionParser
20
    {
21
        private static LRUCache<string, IParseTree> expressionDict = new LRUCache<string, IParseTree>();
1✔
22

23
        /// <summary>
24
        /// Initializes a new instance of the <see cref="ExpressionParser"/> class.
25
        /// Constructor.
26
        /// </summary>
27
        /// <param name="lookup">Delegate to lookup evaluation information from type string.</param>
28
        public ExpressionParser(EvaluatorLookup lookup = null)
1✔
29
        {
30
            EvaluatorLookup = lookup ?? Expression.Lookup;
1✔
31
        }
1✔
32

33
        /// <summary>
34
        /// Gets the elegate to lookup function information from the type.
35
        /// </summary>
36
        /// <value>
37
        /// The elegate to lookup function information from the type.
38
        /// </value>
39
        public EvaluatorLookup EvaluatorLookup { get; }
1✔
40

41
        /// <summary>
42
        /// Parse the input into an expression.
43
        /// </summary>
44
        /// <param name="expression">Expression to parse.</param>
45
        /// <returns>Expression tree.</returns>
46
        public Expression Parse(string expression)
47
        {
48
            if (string.IsNullOrEmpty(expression))
1✔
49
            {
50
                return Expression.ConstantExpression(string.Empty);
1✔
51
            }
52
            else
53
            {
54
                return new ExpressionTransformer(EvaluatorLookup).Transform(AntlrParse(expression));
1✔
55
            }
56
        }
57

58
        /// <summary>
59
        /// Parse the expression to ANTLR lexer and parser.
60
        /// </summary>
61
        /// <param name="expression">The input string expression.</param>
62
        /// <returns>A ParseTree.</returns>
63
        protected static IParseTree AntlrParse(string expression)
64
        {
65
            if (expressionDict.TryGet(expression, out var expressionParseTree))
1✔
66
            {
67
                return expressionParseTree;
1✔
68
            }
69

70
            var inputStream = new AntlrInputStream(expression);
1✔
71
            var lexer = new ExpressionAntlrLexer(inputStream);
1✔
72
            lexer.RemoveErrorListeners();
1✔
73
            var tokenStream = new CommonTokenStream(lexer);
1✔
74
            var parser = new ExpressionAntlrParser(tokenStream);
1✔
75
            parser.RemoveErrorListeners();
1✔
76
            parser.AddErrorListener(ParserErrorListener.Instance);
1✔
77
            parser.BuildParseTree = true;
1✔
78
            var expressionContext = parser.file()?.expression();
×
79
            expressionDict.Set(expression, expressionContext);
1✔
80
            return expressionContext;
1✔
81
        }
82

83
        private class ExpressionTransformer : ExpressionAntlrParserBaseVisitor<Expression>
84
        {
85
            private readonly Regex _escapeRegex = new Regex(@"\\[^\r\n]?");
1✔
86
            private readonly EvaluatorLookup _lookupFunction;
87

88
            public ExpressionTransformer(EvaluatorLookup lookup)
1✔
89
            {
90
                _lookupFunction = lookup;
1✔
91
            }
1✔
92

93
            public Expression Transform(IParseTree context) => Visit(context);
1✔
94

95
            public override Expression VisitUnaryOpExp([NotNull] ExpressionAntlrParser.UnaryOpExpContext context)
96
            {
97
                var unaryOperationName = context.GetChild(0).GetText();
1✔
98
                var operand = Visit(context.expression());
1✔
99
                if (unaryOperationName == ExpressionType.Subtract
1✔
100
                    || unaryOperationName == ExpressionType.Add)
1✔
101
                {
102
                    return MakeExpression(unaryOperationName, new Constant(0), operand);
1✔
103
                }
104

105
                return MakeExpression(unaryOperationName, operand);
1✔
106
            }
107

108
            public override Expression VisitBinaryOpExp([NotNull] ExpressionAntlrParser.BinaryOpExpContext context)
109
            {
110
                var binaryOperationName = context.GetChild(1).GetText();
1✔
111
                var left = Visit(context.expression(0));
1✔
112
                var right = Visit(context.expression(1));
1✔
113
                return MakeExpression(binaryOperationName, left, right);
1✔
114
            }
115

116
            public override Expression VisitTripleOpExp([NotNull] ExpressionAntlrParser.TripleOpExpContext context)
117
            {
118
                var conditionalExpression = Visit(context.expression(0));
1✔
119
                var left = Visit(context.expression(1));
1✔
120
                var right = Visit(context.expression(2));
1✔
121
                return MakeExpression(ExpressionType.If, conditionalExpression, left, right);
1✔
122
            }
123

124
            public override Expression VisitFuncInvokeExp([NotNull] ExpressionAntlrParser.FuncInvokeExpContext context)
125
            {
126
                var parameters = ProcessArgsList(context.argsList());
1✔
127

128
                // Remove the check to check primaryExpression is just an IDENTIFIER to support "." in template name
129
                var functionName = context.primaryExpression().GetText();
1✔
130
                if (context.NON() != null)
1✔
131
                {
132
                    functionName += context.NON().GetText();
1✔
133
                }
134

135
                return MakeExpression(functionName, parameters.ToArray());
1✔
136
            }
137

138
            public override Expression VisitIdAtom([NotNull] ExpressionAntlrParser.IdAtomContext context)
139
            {
140
                Expression result;
141
                var symbol = context.GetText();
1✔
142
                var normalized = symbol.ToLowerInvariant();
1✔
143
                if (normalized == "false")
1✔
144
                {
145
                    result = Expression.ConstantExpression(false);
1✔
146
                }
147
                else if (normalized == "true")
1✔
148
                {
149
                    result = Expression.ConstantExpression(true);
1✔
150
                }
151
                else if (normalized == "null")
1✔
152
                {
153
                    result = Expression.ConstantExpression(null);
1✔
154
                }
155
                else
156
                {
157
                    result = MakeExpression(ExpressionType.Accessor, Expression.ConstantExpression(symbol));
1✔
158
                }
159

160
                return result;
1✔
161
            }
162

163
            public override Expression VisitIndexAccessExp([NotNull] ExpressionAntlrParser.IndexAccessExpContext context)
164
            {
165
                Expression instance;
166
                var property = Visit(context.expression());
1✔
167

168
                instance = Visit(context.primaryExpression());
1✔
169
                return MakeExpression(ExpressionType.Element, instance, property);
1✔
170
            }
171

172
            public override Expression VisitMemberAccessExp([NotNull] ExpressionAntlrParser.MemberAccessExpContext context)
173
            {
174
                var property = context.IDENTIFIER().GetText();
1✔
175
                var instance = Visit(context.primaryExpression());
1✔
176

177
                return MakeExpression(ExpressionType.Accessor, Expression.ConstantExpression(property), instance);
1✔
178
            }
179

180
            public override Expression VisitNumericAtom([NotNull] ExpressionAntlrParser.NumericAtomContext context)
181
            {
182
                var contextText = context.GetText();
1✔
183

184
                if (int.TryParse(contextText, out var intValue))
1✔
185
                {
186
                    return Expression.ConstantExpression(intValue);
1✔
187
                }
188

189
                if (long.TryParse(contextText, out var longValue))
1✔
190
                {
191
                    return Expression.ConstantExpression(longValue);
1✔
192
                }
193

194
                if (double.TryParse(contextText, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue))
1✔
195
                {
196
                    return Expression.ConstantExpression(doubleValue);
1✔
197
                }
198

199
                throw new ArgumentException($"{contextText} is not a number in expression \"{contextText}\"");
×
200
            }
201

202
            public override Expression VisitParenthesisExp([NotNull] ExpressionAntlrParser.ParenthesisExpContext context) => Visit(context.expression());
1✔
203

204
            public override Expression VisitArrayCreationExp([NotNull] ExpressionAntlrParser.ArrayCreationExpContext context)
205
            {
206
                var parameters = ProcessArgsList(context.argsList());
1✔
207
                return MakeExpression(ExpressionType.CreateArray, parameters.ToArray());
1✔
208
            }
209

210
            public override Expression VisitStringAtom([NotNull] ExpressionAntlrParser.StringAtomContext context)
211
            {
212
                var text = context.GetText();
1✔
213
                if (text.StartsWith("'", StringComparison.Ordinal) && text.EndsWith("'", StringComparison.Ordinal))
1✔
214
                {
215
                    text = text.Substring(1, text.Length - 2).Replace("\\'", "'");
1✔
216
                }
217
                else if (text.StartsWith("\"", StringComparison.Ordinal) && text.EndsWith("\"", StringComparison.Ordinal))
1✔
218
                {
219
                    text = text.Substring(1, text.Length - 2).Replace("\\\"", "\"");
1✔
220
                }
221
                else
222
                {
223
                    throw new ArgumentException($"Invalid string {text}");
×
224
                }
225

226
                return Expression.ConstantExpression(EvalEscape(text));
1✔
227
            }
228

229
            public override Expression VisitJsonCreationExp([NotNull] ExpressionAntlrParser.JsonCreationExpContext context)
230
            {
231
                var expr = this.MakeExpression(ExpressionType.Json, new Constant("{}"));
1✔
232
                if (context.keyValuePairList() != null)
1✔
233
                {
234
                    foreach (var kvPair in context.keyValuePairList().keyValuePair())
1✔
235
                    {
236
                        var key = string.Empty;
1✔
237
                        var keyNode = kvPair.key().children[0];
1✔
238
                        if (keyNode is ITerminalNode node)
1✔
239
                        {
240
                            if (node.Symbol.Type == ExpressionAntlrParser.IDENTIFIER)
1✔
241
                            {
242
                                key = node.GetText();
1✔
243
                            }
244
                            else
245
                            {
246
                                key = node.GetText().Substring(1, node.GetText().Length - 2);
1✔
247
                            }
248
                        }
249

250
                        expr = this.MakeExpression(ExpressionType.SetProperty, expr, new Constant(key), this.Visit(kvPair.expression()));
1✔
251
                    }
252
                }
253

254
                return expr;
1✔
255
            }
256

257
            public override Expression VisitStringInterpolationAtom([NotNull] ExpressionAntlrParser.StringInterpolationAtomContext context)
258
            {
259
                var children = new List<Expression>() { string.Empty };
1✔
260
                foreach (var child in context.stringInterpolation().children)
1✔
261
                {
262
                    if (child is ITerminalNode node)
1✔
263
                    {
264
                        switch (node.Symbol.Type)
1✔
265
                        {
266
                            case ExpressionAntlrParser.TEMPLATE:
267
                                var expressionString = TrimExpression(node.GetText());
1✔
268
                                children.Add(Expression.Parse(expressionString, _lookupFunction));
1✔
269
                                break;
1✔
270
                            case ExpressionAntlrParser.ESCAPE_CHARACTER:
271
                                children.Add(Expression.ConstantExpression(node.GetText().Replace("\\`", "`").Replace("\\$", "$")));
1✔
272
                                break;
1✔
273
                            default:
274
                                break;
275
                        }
276
                    }
277
                    else
278
                    {
279
                        // text content
280
                        children.Add(Expression.ConstantExpression(child.GetText()));
1✔
281
                    }
282
                }
283

284
                return MakeExpression(ExpressionType.Concat, children.ToArray());
1✔
285
            }
286

287
            private Expression MakeExpression(string functionType, params Expression[] children)
288
                => Expression.MakeExpression(_lookupFunction(functionType) ?? throw new SyntaxErrorException($"{functionType} does not have an evaluator, it's not a built-in function or a custom function."), children);
1✔
289

290
            private IList<Expression> ProcessArgsList(ExpressionAntlrParser.ArgsListContext context)
291
            {
292
                var result = new List<Expression>();
1✔
293
                if (context == null)
1✔
294
                {
295
                    return result;
1✔
296
                }
297

298
                foreach (var child in context.children)
1✔
299
                {
300
                    if (child is ExpressionAntlrParser.LambdaContext lambda)
1✔
301
                    {
302
                        var evalParam = MakeExpression(ExpressionType.Accessor, Expression.ConstantExpression(lambda.IDENTIFIER().GetText()));
1✔
303
                        var evalFun = Visit(lambda.expression());
1✔
304
                        result.Add(evalParam);
1✔
305
                        result.Add(evalFun);
1✔
306
                    }
307
                    else if (child is ExpressionAntlrParser.ExpressionContext expression)
1✔
308
                    {
309
                        result.Add(Visit(expression));
1✔
310
                    }
311
                }
312

313
                return result;
1✔
314
            }
315

316
            private string EvalEscape(string text)
317
            {
318
                if (text == null)
1✔
319
                {
320
                    return string.Empty;
×
321
                }
322

323
                return _escapeRegex.Replace(text, new MatchEvaluator(m =>
1✔
324
                {
1✔
325
                    var value = m.Value;
1✔
326
                    var commonEscapes = new List<string>() { "\\r", "\\n", "\\t", "\\\\" };
1✔
327
                    if (commonEscapes.Contains(value))
1✔
328
                    {
1✔
329
                        return Regex.Unescape(value);
1✔
330
                    }
1✔
331

1✔
332
                    return value;
1✔
333
                }));
1✔
334
            }
335

336
            private string TrimExpression(string expression)
337
            {
338
                var result = expression.Trim().TrimStart('$').Trim();
1✔
339

340
                if (result.StartsWith("{", StringComparison.Ordinal) && result.EndsWith("}", StringComparison.Ordinal))
1✔
341
                {
342
                    result = result.Substring(1, result.Length - 2);
1✔
343
                }
344

345
                return result;
1✔
346
            }
347
        }
348
    }
349
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc