Skip to content

Commit 6fb4987

Browse files
committed
Use method argument names and optionality when binding function calls; default rest() to use shallow template inspection
1 parent e81c8c5 commit 6fb4987

File tree

8 files changed

+111
-67
lines changed

8 files changed

+111
-67
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ The built-in properties mirror those available in the CLEF format.
147147
| Array | An array of values, in square brackets | `[1, 'two', null]` |
148148
| Object | A mapping of string keys to values; keys that are valid identifiers do not need to be quoted | `{a: 1, 'b c': 2, d}` |
149149

150-
Array and object literals support the spread operator: `[1, 2, ..rest]`, `{a: 1, ..other}`. Specifying an undefined
150+
Array and object literals support the spread operator: `[1, 2, ..others]`, `{a: 1, ..others}`. Specifying an undefined
151151
property in an object literal will remove it from the result: `{..User, Email: Undefined()}`
152152

153153
### Operators and conditionals
@@ -196,7 +196,7 @@ calling a function will be undefined if:
196196
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
197197
| `Length(x)` | Returns the length of a string or array. |
198198
| `Now()` | Returns `DateTimeOffset.Now`. |
199-
| `Rest()` | In an `ExpressionTemplate`, returns an object containing the first-class event properties not otherwise referenced in the template or the event's message. |
199+
| `Rest([deep])` | In an `ExpressionTemplate`, returns an object containing the first-class event properties not otherwise referenced in the template. If `deep` is `true`, also excludes properties referenced in the event's message template. |
200200
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
201201
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
202202
| `Substring(s, start, [length])` | Return the substring of string `s` from `start` to the end of the string, or of `length` characters, if this argument is supplied. |

src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using System.Linq;
1818
using System.Linq.Expressions;
1919
using System.Reflection;
20+
using System.Runtime.InteropServices;
21+
using System.Text;
2022
using Serilog.Events;
2123
using Serilog.Expressions.Ast;
2224
using Serilog.Expressions.Compilation.Transformations;
@@ -27,6 +29,7 @@
2729
using ParameterExpression = System.Linq.Expressions.ParameterExpression;
2830
using LX = System.Linq.Expressions.Expression;
2931
using ExpressionBody = System.Linq.Expressions.Expression;
32+
// ReSharper disable UseIndexFromEndExpression
3033

3134
namespace Serilog.Expressions.Compilation.Linq
3235
{
@@ -101,11 +104,18 @@ protected override ExpressionBody Transform(CallExpression call)
101104
if (!_nameResolver.TryResolveFunctionName(call.OperatorName, out var m))
102105
throw new ArgumentException($"The function name `{call.OperatorName}` was not recognized.");
103106

104-
var methodParameters = m.GetParameters();
107+
var methodParameters = m.GetParameters()
108+
.Select(info => (pi: info, optional: info.GetCustomAttribute<OptionalAttribute>() != null))
109+
.ToList();
105110

106-
var parameterCount = methodParameters.Count(pi => pi.ParameterType == typeof(LogEventPropertyValue));
107-
if (parameterCount != call.Operands.Length)
108-
throw new ArgumentException($"The function `{call.OperatorName}` requires {parameterCount} arguments.");
111+
var allowedParameters = methodParameters.Where(info => info.pi.ParameterType == typeof(LogEventPropertyValue)).ToList();
112+
var requiredParameterCount = allowedParameters.Count(info => !info.optional);
113+
114+
if (call.Operands.Length < requiredParameterCount || call.Operands.Length > allowedParameters.Count)
115+
{
116+
var requirements = DescribeRequirements(allowedParameters.Select(info => (info.pi.Name!, info.optional)).ToList());
117+
throw new ArgumentException($"The function `{call.OperatorName}` {requirements}.");
118+
}
109119

110120
var operands = new Queue<LX>(call.Operands.Select(Transform));
111121

@@ -116,11 +126,15 @@ protected override ExpressionBody Transform(CallExpression call)
116126
if (Operators.SameOperator(call.OperatorName, Operators.RuntimeOpOr))
117127
return CompileLogical(LX.OrElse, operands.Dequeue(), operands.Dequeue());
118128

119-
var boundParameters = new List<LX>(methodParameters.Length);
120-
foreach (var pi in methodParameters)
129+
var boundParameters = new List<LX>(methodParameters.Count);
130+
foreach (var (pi, optional) in methodParameters)
121131
{
122132
if (pi.ParameterType == typeof(LogEventPropertyValue))
123-
boundParameters.Add(operands.Dequeue());
133+
{
134+
boundParameters.Add(operands.Count > 0
135+
? operands.Dequeue()
136+
: LX.Constant(null, typeof(LogEventPropertyValue)));
137+
}
124138
else if (pi.ParameterType == typeof(StringComparison))
125139
boundParameters.Add(LX.Constant(call.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));
126140
else if (pi.ParameterType == typeof(IFormatProvider))
@@ -129,13 +143,38 @@ protected override ExpressionBody Transform(CallExpression call)
129143
boundParameters.Add(LX.Property(Context, EvaluationContextLogEventProperty));
130144
else if (_nameResolver.TryBindFunctionParameter(pi, out var binding))
131145
boundParameters.Add(LX.Constant(binding, pi.ParameterType));
146+
else if (optional)
147+
boundParameters.Add(LX.Constant(
148+
pi.GetCustomAttribute<DefaultParameterValueAttribute>()?.Value, pi.ParameterType));
132149
else
133-
throw new ArgumentException($"The method `{m.Name}` implementing function `{call.OperatorName}` has parameter `{pi.Name}` which could not be bound.");
150+
throw new ArgumentException($"The method `{m.Name}` implementing function `{call.OperatorName}` has argument `{pi.Name}` which could not be bound.");
134151
}
135152

136153
return LX.Call(m, boundParameters);
137154
}
138155

156+
static string DescribeRequirements(IReadOnlyList<(string name, bool optional)> parameters)
157+
{
158+
static string DescribeArgument((string name, bool optional) p) =>
159+
$"`{p.name}`" + (p.optional ? " (optional)" : "");
160+
161+
if (parameters.Count == 0)
162+
return "accepts no arguments";
163+
164+
if (parameters.Count == 1)
165+
return $"accepts one argument, {DescribeArgument(parameters[0])}";
166+
167+
if (parameters.Count == 2)
168+
return $"accepts two arguments, {DescribeArgument(parameters[0])} and {DescribeArgument(parameters[1])}";
169+
170+
var result = new StringBuilder("accepts arguments");
171+
for (var i = 0; i < parameters.Count - 1; ++i)
172+
result.Append($" {DescribeArgument(parameters[i])},");
173+
174+
result.Append($" and {DescribeArgument(parameters[parameters.Count - 1])}");
175+
return result.ToString();
176+
}
177+
139178
static ExpressionBody CompileLogical(Func<ExpressionBody, ExpressionBody, ExpressionBody> apply, ExpressionBody lhs, ExpressionBody rhs)
140179
{
141180
return LX.Convert(

src/Serilog.Expressions/Expressions/Compilation/Variadics/VariadicCallRewriter.cs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,11 @@ public static Expression Rewrite(Expression expression)
3030

3131
protected override Expression Transform(CallExpression call)
3232
{
33-
if (Operators.SameOperator(call.OperatorName, Operators.OpSubstring) && call.Operands.Length == 2)
34-
{
35-
var operands = call.Operands
36-
.Select(Transform)
37-
.Concat(new[] {CallUndefined()})
38-
.ToArray();
39-
return new CallExpression(call.IgnoreCase, call.OperatorName, operands);
40-
}
41-
4233
if (Operators.SameOperator(call.OperatorName, Operators.OpCoalesce) ||
4334
Operators.SameOperator(call.OperatorName, Operators.OpConcat))
4435
{
4536
if (call.Operands.Length == 0)
46-
return CallUndefined();
37+
return new CallExpression(false, Operators.OpUndefined);
4738
if (call.Operands.Length == 1)
4839
return Transform(call.Operands.Single());
4940
if (call.Operands.Length > 2)
@@ -54,18 +45,7 @@ protected override Expression Transform(CallExpression call)
5445
}
5546
}
5647

57-
if (Operators.SameOperator(call.OperatorName, Operators.OpToString) &&
58-
call.Operands.Length == 1)
59-
{
60-
return new CallExpression(call.IgnoreCase, call.OperatorName, call.Operands[0], CallUndefined());
61-
}
62-
6348
return base.Transform(call);
6449
}
65-
66-
static CallExpression CallUndefined()
67-
{
68-
return new(false, Operators.OpUndefined);
69-
}
7050
}
7151
}

src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,9 @@ static bool UnboxedEqualHelper(StringComparison sc, LogEventPropertyValue? left,
236236
return null;
237237
}
238238

239-
public static LogEventPropertyValue? Round(LogEventPropertyValue? value, LogEventPropertyValue? places)
239+
public static LogEventPropertyValue? Round(LogEventPropertyValue? number, LogEventPropertyValue? places)
240240
{
241-
if (!Coerce.Numeric(value, out var v) ||
241+
if (!Coerce.Numeric(number, out var v) ||
242242
!Coerce.Numeric(places, out var p) ||
243243
p < 0 ||
244244
p > 32) // Check my memory, here :D
@@ -266,57 +266,57 @@ static bool UnboxedEqualHelper(StringComparison sc, LogEventPropertyValue? left,
266266
null;
267267
}
268268

269-
public static LogEventPropertyValue? Contains(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
269+
public static LogEventPropertyValue? Contains(StringComparison sc, LogEventPropertyValue? @string, LogEventPropertyValue? substring)
270270
{
271-
if (!Coerce.String(corpus, out var ctx) ||
272-
!Coerce.String(pattern, out var ptx))
271+
if (!Coerce.String(@string, out var ctx) ||
272+
!Coerce.String(substring, out var ptx))
273273
return null;
274274

275275
return ScalarBoolean(ctx.Contains(ptx, sc));
276276
}
277277

278-
public static LogEventPropertyValue? IndexOf(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
278+
public static LogEventPropertyValue? IndexOf(StringComparison sc, LogEventPropertyValue? @string, LogEventPropertyValue? substring)
279279
{
280-
if (!Coerce.String(corpus, out var ctx) ||
281-
!Coerce.String(pattern, out var ptx))
280+
if (!Coerce.String(@string, out var ctx) ||
281+
!Coerce.String(substring, out var ptx))
282282
return null;
283283

284284
return new ScalarValue(ctx.IndexOf(ptx, sc));
285285
}
286286

287-
public static LogEventPropertyValue? LastIndexOf(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
287+
public static LogEventPropertyValue? LastIndexOf(StringComparison sc, LogEventPropertyValue? @string, LogEventPropertyValue? substring)
288288
{
289-
if (!Coerce.String(corpus, out var ctx) ||
290-
!Coerce.String(pattern, out var ptx))
289+
if (!Coerce.String(@string, out var ctx) ||
290+
!Coerce.String(substring, out var ptx))
291291
return null;
292292

293293
return new ScalarValue(ctx.LastIndexOf(ptx, sc));
294294
}
295295

296-
public static LogEventPropertyValue? Length(LogEventPropertyValue? arg)
296+
public static LogEventPropertyValue? Length(LogEventPropertyValue? value)
297297
{
298-
if (Coerce.String(arg, out var s))
298+
if (Coerce.String(value, out var s))
299299
return new ScalarValue(s.Length);
300300

301-
if (arg is SequenceValue seq)
301+
if (value is SequenceValue seq)
302302
return new ScalarValue(seq.Elements.Count);
303303

304304
return null;
305305
}
306306

307-
public static LogEventPropertyValue? StartsWith(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
307+
public static LogEventPropertyValue? StartsWith(StringComparison sc, LogEventPropertyValue? value, LogEventPropertyValue? substring)
308308
{
309-
if (!Coerce.String(corpus, out var ctx) ||
310-
!Coerce.String(pattern, out var ptx))
309+
if (!Coerce.String(value, out var ctx) ||
310+
!Coerce.String(substring, out var ptx))
311311
return null;
312312

313313
return ScalarBoolean(ctx.StartsWith(ptx, sc));
314314
}
315315

316-
public static LogEventPropertyValue? EndsWith(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
316+
public static LogEventPropertyValue? EndsWith(StringComparison sc, LogEventPropertyValue? value, LogEventPropertyValue? substring)
317317
{
318-
if (!Coerce.String(corpus, out var ctx) ||
319-
!Coerce.String(pattern, out var ptx))
318+
if (!Coerce.String(value, out var ctx) ||
319+
!Coerce.String(substring, out var ptx))
320320
return null;
321321

322322
return ScalarBoolean(ctx.EndsWith(ptx, sc));
@@ -432,17 +432,17 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
432432
}
433433

434434
// Ideally this will be compiled as a short-circuiting intrinsic
435-
public static LogEventPropertyValue? Coalesce(LogEventPropertyValue? v1, LogEventPropertyValue? v2)
435+
public static LogEventPropertyValue? Coalesce(LogEventPropertyValue? value0, LogEventPropertyValue? value1)
436436
{
437-
if (v1 is null or ScalarValue {Value: null})
438-
return v2;
437+
if (value0 is null or ScalarValue {Value: null})
438+
return value1;
439439

440-
return v1;
440+
return value0;
441441
}
442442

443-
public static LogEventPropertyValue? Substring(LogEventPropertyValue? sval, LogEventPropertyValue? startIndex, LogEventPropertyValue? length)
443+
public static LogEventPropertyValue? Substring(LogEventPropertyValue? @string, LogEventPropertyValue? startIndex, LogEventPropertyValue? length = null)
444444
{
445-
if (!Coerce.String(sval, out var str) ||
445+
if (!Coerce.String(@string, out var str) ||
446446
!Coerce.Numeric(startIndex, out var si))
447447
return null;
448448

@@ -461,9 +461,9 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
461461
return new ScalarValue(str.Substring((int)si, (int)len));
462462
}
463463

464-
public static LogEventPropertyValue? Concat(LogEventPropertyValue? first, LogEventPropertyValue? second)
464+
public static LogEventPropertyValue? Concat(LogEventPropertyValue? string0, LogEventPropertyValue? string1)
465465
{
466-
if (Coerce.String(first, out var f) && Coerce.String(second, out var s))
466+
if (Coerce.String(string0, out var f) && Coerce.String(string1, out var s))
467467
{
468468
return new ScalarValue(f + s);
469469
}
@@ -492,7 +492,7 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
492492
return Coerce.IsTrue(condition) ? consequent : alternative;
493493
}
494494

495-
public static LogEventPropertyValue? ToString(IFormatProvider? formatProvider, LogEventPropertyValue? value, LogEventPropertyValue? format)
495+
public static LogEventPropertyValue? ToString(IFormatProvider? formatProvider, LogEventPropertyValue? value, LogEventPropertyValue? format = null)
496496
{
497497
if (value is not ScalarValue sv ||
498498
sv.Value == null ||

src/Serilog.Expressions/Templates/Compilation/UnreferencedProperties/UnreferencedPropertiesFunction.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Reflection;
2020
using Serilog.Events;
2121
using Serilog.Expressions;
22+
using Serilog.Expressions.Runtime;
2223
using Serilog.Parsing;
2324
using Serilog.Templates.Ast;
2425

@@ -74,11 +75,12 @@ public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)]
7475

7576
// By convention, built-in functions accept and return nullable values.
7677
// ReSharper disable once ReturnTypeCanBeNotNullable
77-
public static LogEventPropertyValue? Implementation(UnreferencedPropertiesFunction self, LogEvent logEvent)
78+
public static LogEventPropertyValue? Implementation(UnreferencedPropertiesFunction self, LogEvent logEvent, LogEventPropertyValue? deep = null)
7879
{
80+
var checkMessageTemplate = Coerce.IsTrue(deep);
7981
return new StructureValue(logEvent.Properties
80-
.Where(kvp => !(self._referencedInTemplate.Contains(kvp.Key) ||
81-
TemplateContainsPropertyName(logEvent.MessageTemplate, kvp.Key)))
82+
.Where(kvp => !self._referencedInTemplate.Contains(kvp.Key) &&
83+
(!checkMessageTemplate || !TemplateContainsPropertyName(logEvent.MessageTemplate, kvp.Key)))
8284
.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)));
8385
}
8486

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ A{#if false}B{#else if true}C{#end} ⇶ AC
2727
{#if true}A{#each a in [1]}B{a}{#end}C{#end}D ⇶ AB1CD
2828
{#each a in []}{a}!{#else}none{#end} ⇶ none
2929
Culture-specific {42.34} ⇶ Culture-specific 42,34
30+
{rest()} ⇶ {"Name":"nblumhardt"}
31+
{Name} {rest()} ⇶ nblumhardt {}
32+
{rest(true)} ⇶ {}

test/Serilog.Expressions.Tests/ExpressionCompilerTests.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Serilog.Events;
1+
using System;
2+
using Serilog.Events;
23
using System.Linq;
34
using Serilog.Expressions.Tests.Support;
45
using Xunit;
@@ -134,6 +135,19 @@ public void InExaminesSequenceValues()
134135
Some.InformationEvent());
135136
}
136137

138+
[Theory]
139+
[InlineData("now(1)", "The function `now` accepts no arguments.")]
140+
[InlineData("length()", "The function `length` accepts one argument, `value`.")]
141+
[InlineData("length(1, 2)", "The function `length` accepts one argument, `value`.")]
142+
[InlineData("round()", "The function `round` accepts two arguments, `number` and `places`.")]
143+
[InlineData("substring()", "The function `substring` accepts arguments `string`, `startIndex`, and `length` (optional).")]
144+
public void ReportsArityMismatches(string call, string expectedError)
145+
{
146+
// These will eventually be reported gracefully by `TryCompile()`...
147+
var ex = Assert.Throws<ArgumentException>(() => SerilogExpression.Compile(call));
148+
Assert.Equal(expectedError, ex.Message);
149+
}
150+
137151
static void AssertEvaluation(string expression, LogEvent match, params LogEvent[] noMatches)
138152
{
139153
var sink = new CollectingSink();

test/Serilog.Expressions.Tests/Templates/UnreferencedPropertiesFunctionTests.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using Serilog.Events;
34
using Serilog.Parsing;
45
using Serilog.Templates.Ast;
@@ -20,7 +21,7 @@ public void UnreferencedPropertiesFunctionIsNamedRest()
2021
[Fact]
2122
public void UnreferencedPropertiesExcludeThoseInMessageAndTemplate()
2223
{
23-
Assert.True(new TemplateParser().TryParse("{A + 1}{#if true}{B}{#end}", out var template, out _));
24+
Assert.True(new TemplateParser().TryParse("{@m}{A + 1}{#if true}{B}{#end}", out var template, out _));
2425

2526
var function = new UnreferencedPropertiesFunction(template!);
2627

@@ -37,11 +38,16 @@ public void UnreferencedPropertiesExcludeThoseInMessageAndTemplate()
3738
new LogEventProperty("D", new ScalarValue(null)),
3839
});
3940

40-
var result = UnreferencedPropertiesFunction.Implementation(function, evt);
41+
var deep = UnreferencedPropertiesFunction.Implementation(function, evt, new ScalarValue(true));
4142

42-
var sv = Assert.IsType<StructureValue>(result);
43+
var sv = Assert.IsType<StructureValue>(deep);
4344
var included = Assert.Single(sv.Properties);
4445
Assert.Equal("D", included!.Name);
46+
47+
var shallow = UnreferencedPropertiesFunction.Implementation(function, evt);
48+
sv = Assert.IsType<StructureValue>(shallow);
49+
Assert.Contains(sv.Properties, p => p.Name == "C");
50+
Assert.Contains(sv.Properties, p => p.Name == "D");
4551
}
4652
}
4753
}

0 commit comments

Comments
 (0)