Skip to content

Commit 81bcc69

Browse files
authored
Merge pull request #26 from nblumhardt/wildcard-assoc-fix
Fix wildcard semantics; fix like clause anchoring
2 parents e10eae6 + 8d7c1a0 commit 81bcc69

14 files changed

+142
-35
lines changed

src/Serilog.Expressions/Expressions/Ast/Expression.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
{
33
abstract class Expression
44
{
5+
// Used only as an enabler for testing and debugging.
6+
public abstract override string ToString();
57
}
68
}

src/Serilog.Expressions/Expressions/Ast/IndexOfMatchExpression.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@ public IndexOfMatchExpression(Expression corpus, Regex regex)
1313
Corpus = corpus ?? throw new ArgumentNullException(nameof(corpus));
1414
Regex = regex ?? throw new ArgumentNullException(nameof(regex));
1515
}
16+
17+
public override string ToString()
18+
{
19+
return $"_Internal_IndexOfMatch({Corpus}, '{Regex.ToString().Replace("'", "''")}')";
20+
}
1621
}
1722
}

src/Serilog.Expressions/Expressions/Compilation/ExpressionCompiler.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Serilog.Expressions.Compilation
1010
{
1111
static class ExpressionCompiler
1212
{
13-
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
13+
public static Expression Translate(Expression expression)
1414
{
1515
var actual = expression;
1616
actual = VariadicCallRewriter.Rewrite(actual);
@@ -19,7 +19,12 @@ public static CompiledExpression Compile(Expression expression, NameResolver nam
1919
actual = PropertiesObjectAccessorTransformer.Rewrite(actual);
2020
actual = ConstantArrayEvaluator.Evaluate(actual);
2121
actual = WildcardComprehensionTransformer.Expand(actual);
22-
22+
return actual;
23+
}
24+
25+
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
26+
{
27+
var actual = Translate(expression);
2328
return LinqExpressionCompiler.Compile(actual, nameResolver);
2429
}
2530
}

src/Serilog.Expressions/Expressions/Compilation/Text/LikeSyntaxTransformer.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ cx.Constant is ScalarValue scalar &&
5454

5555
static string LikeToRegex(string like)
5656
{
57+
var begin = "^";
5758
var regex = "";
59+
var end = "$";
60+
5861
for (var i = 0; i < like.Length; ++i)
5962
{
6063
var ch = like[i];
@@ -68,7 +71,17 @@ static string LikeToRegex(string like)
6871
}
6972
else
7073
{
71-
regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline
74+
if (i == 0)
75+
begin = "";
76+
77+
if (i == like.Length - 1)
78+
end = "";
79+
80+
if (i == 0 && i == like.Length - 1)
81+
regex += ".*";
82+
83+
if (i != 0 && i != like.Length - 1)
84+
regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline
7285
}
7386
}
7487
else if (ch == '_')
@@ -87,7 +100,7 @@ static string LikeToRegex(string like)
87100
regex += Regex.Escape(ch.ToString());
88101
}
89102

90-
return regex;
103+
return begin + regex + end;
91104
}
92105
}
93106
}

src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ public static Expression Expand(Expression root)
1414
return wc.Transform(root);
1515
}
1616

17+
// This matches expression fragments such as `A[?] = 'test'` and
18+
// transforms them into `any(A, |p| p = 'test)`.
19+
//
20+
// As the comparand in such expressions can be complex, e.g.
21+
// `Substring(A[?], 0, 4) = 'test')`, the search for `?` and `*` wildcards
22+
// is deep, but, it terminates upon reaching any other wildcard-compatible
23+
// comparison. Thus `(A[?] = 'test') = true` will result in `any(A, |p| p = 'test') = true` and
24+
// not `any(A, |p| (p = 'test') = true)`, which is important because short-circuiting when the first
25+
// argument to `any()` is undefined will change the semantics of the resulting expression, otherwise.
1726
protected override Expression Transform(CallExpression lx)
1827
{
1928
if (!Operators.WildcardComparators.Contains(lx.OperatorName))
@@ -24,7 +33,7 @@ protected override Expression Transform(CallExpression lx)
2433
var indexerOperand = -1;
2534
for (var i = 0; i < lx.Operands.Length; ++i)
2635
{
27-
indexer = WildcardSearch.FindElementAtWildcard(lx.Operands[i]);
36+
indexer = WildcardSearch.FindWildcardIndexer(lx.Operands[i]);
2837
if (indexer != null)
2938
{
3039
indexerOperand = i;
@@ -52,5 +61,19 @@ protected override Expression Transform(CallExpression lx)
5261
var call = new CallExpression(false, op, coll, lambda);
5362
return Transform(call);
5463
}
64+
65+
// Detects and transforms standalone `A[?]` fragments that are not part of a comparision; these
66+
// are effectively Boolean tests.
67+
protected override Expression Transform(IndexerExpression ix)
68+
{
69+
if (!(ix.Index is IndexerWildcardExpression wx))
70+
return base.Transform(ix);
71+
72+
var px = new ParameterExpression("p" + _nextParameter++);
73+
var coll = Transform(ix.Receiver);
74+
var lambda = new LambdaExpression(new[] { px }, px);
75+
var op = Operators.ToRuntimeWildcardOperator(wx.Wildcard);
76+
return new CallExpression(false, op, coll, lambda);
77+
}
5578
}
5679
}

src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardSearch.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class WildcardSearch : SerilogExpressionTransformer<IndexerExpression?>
88
{
99
static readonly WildcardSearch Instance = new WildcardSearch();
1010

11-
public static IndexerExpression? FindElementAtWildcard(Expression fx)
11+
public static IndexerExpression? FindWildcardIndexer(Expression fx)
1212
{
1313
return Instance.Transform(fx);
1414
}
@@ -59,6 +59,11 @@ class WildcardSearch : SerilogExpressionTransformer<IndexerExpression?>
5959

6060
protected override IndexerExpression? Transform(CallExpression lx)
6161
{
62+
// If we hit a wildcard-compatible operation, then any wildcards within its operands "belong" to
63+
// it and can't be the result of this search.
64+
if (Operators.WildcardComparators.Contains(lx.OperatorName))
65+
return null;
66+
6267
return lx.Operands.Select(Transform).FirstOrDefault(e => e != null);
6368
}
6469

src/Serilog.Expressions/Expressions/Parsing/ExpressionParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ static class ExpressionParser
88
{
99
static ExpressionTokenizer Tokenizer { get; } = new ExpressionTokenizer();
1010

11-
public static Expression Parse(string filterExpression)
11+
public static Expression Parse(string expression)
1212
{
13-
if (!TryParse(filterExpression, out var root, out var error))
13+
if (!TryParse(expression, out var root, out var error))
1414
throw new ArgumentException(error);
1515

1616
return root;

src/Serilog.Expressions/Expressions/SerilogExpression.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
// limitations under the License.
1414

1515
using System;
16-
using System.Collections.Generic;
1716
using System.Diagnostics.CodeAnalysis;
1817
using System.Linq;
1918
using Serilog.Expressions.Compilation;

test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,7 @@ undefined() = undefined() ci ⇶ undefined()
267267
'test' like '%s_' ⇶ true
268268
'test' like '%' ⇶ true
269269
'test' like 't%s%' ⇶ true
270+
'test' like 'es' ⇶ false
271+
'test' like '%' ⇶ true
272+
'test' like '' ⇶ false
273+
'' like '' ⇶ true
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Like
2+
A like 'a' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a$'), -1)
3+
A like 'a%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a'), -1)
4+
A like '%a%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a'), -1)
5+
A like '%a' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a$'), -1)
6+
A like '%a_b%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a.b'), -1)
7+
A like 'a%b%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a(?:.|\r|\n)*b'), -1)
8+
A like '%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '.*'), -1)
9+
10+
// Root properties
11+
@p['Test'] ⇶ Test
12+
@p[Test] ⇶ @p[Test]
13+
14+
// Variadics
15+
coalesce(A, B, C, D) ⇶ coalesce(A, coalesce(B, coalesce(C, D)))
16+
17+
// Wildcards!
18+
A[?] ⇶ _Internal_Any(A, |$$p0| {$$p0})
19+
A or B[*] ⇶ _Internal_Or(A, _Internal_All(B, |$$p0| {$$p0}))
20+
not (A is not null) or not (A[?] = 'a') ⇶ _Internal_Or(_Internal_Not(_Internal_IsNotNull(A)), _Internal_Not(_Internal_Any(A, |$$p0| {_Internal_Equal($$p0, 'a')})))

test/Serilog.Expressions.Tests/ExpressionEvaluationTests.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,8 @@ namespace Serilog.Expressions.Tests
1111
{
1212
public class ExpressionEvaluationTests
1313
{
14-
static readonly string CasesPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, "Cases");
15-
16-
static IEnumerable<object[]> ReadCases(string filename)
17-
{
18-
foreach (var line in File.ReadLines(Path.Combine(CasesPath, filename)))
19-
{
20-
var cols = line.Split("⇶", StringSplitOptions.RemoveEmptyEntries);
21-
if (cols.Length == 2)
22-
yield return cols.Select(c => c.Trim()).ToArray<object>();
23-
}
24-
}
25-
2614
public static IEnumerable<object[]> ExpressionEvaluationCases =>
27-
ReadCases("expression-evaluation-cases.asv");
15+
AsvCases.ReadCases("expression-evaluation-cases.asv");
2816

2917
[Theory]
3018
[MemberData(nameof(ExpressionEvaluationCases))]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using Serilog.Events;
6+
using Serilog.Expressions.Compilation;
7+
using Serilog.Expressions.Parsing;
8+
using Serilog.Expressions.Runtime;
9+
using Serilog.Expressions.Tests.Support;
10+
using Xunit;
11+
12+
namespace Serilog.Expressions.Tests
13+
{
14+
public class ExpressionTranslationTests
15+
{
16+
public static IEnumerable<object[]> ExpressionEvaluationCases =>
17+
AsvCases.ReadCases("translation-cases.asv");
18+
19+
[Theory]
20+
[MemberData(nameof(ExpressionEvaluationCases))]
21+
public void ExpressionsAreCorrectlyTranslated(string expr, string expected)
22+
{
23+
var parsed = ExpressionParser.Parse(expr);
24+
var translated = ExpressionCompiler.Translate(parsed);
25+
var actual = translated.ToString();
26+
Assert.Equal(expected, actual);
27+
}
28+
}
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
6+
namespace Serilog.Expressions.Tests.Support
7+
{
8+
// "Arrow-separated values ;-) ... convenient because the Unicode `⇶` character doesn't appear in
9+
// any of the cases themselves, and so we ignore any need for special character escaping (which is
10+
// troublesome when the language the cases are written in uses just about all special characters somehow
11+
// or other!).
12+
//
13+
// The ASV format informally supports `//` comment lines, as long as they don't contain the arrow character.
14+
static class AsvCases
15+
{
16+
static readonly string CasesPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, "Cases");
17+
18+
public static IEnumerable<object[]> ReadCases(string filename)
19+
{
20+
return from line in File.ReadLines(Path.Combine(CasesPath, filename))
21+
select line.Split("⇶", StringSplitOptions.RemoveEmptyEntries) into cols
22+
where cols.Length == 2
23+
select cols.Select(c => c.Trim()).ToArray<object>();
24+
}
25+
}
26+
}

test/Serilog.Expressions.Tests/TemplateEvaluationTests.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,8 @@ namespace Serilog.Expressions.Tests
1010
{
1111
public class TemplateEvaluationTests
1212
{
13-
static readonly string CasesPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, "Cases");
14-
15-
static IEnumerable<object[]> ReadCases(string filename)
16-
{
17-
foreach (var line in File.ReadLines(Path.Combine(CasesPath, filename)))
18-
{
19-
var cols = line.Split("⇶", StringSplitOptions.RemoveEmptyEntries);
20-
if (cols.Length == 2)
21-
yield return cols.Select(c => c.Trim()).ToArray<object>();
22-
}
23-
}
24-
2513
public static IEnumerable<object[]> TemplateEvaluationCases =>
26-
ReadCases("template-evaluation-cases.asv");
14+
AsvCases.ReadCases("template-evaluation-cases.asv");
2715

2816
[Theory]
2917
[MemberData(nameof(TemplateEvaluationCases))]

0 commit comments

Comments
 (0)