Skip to content

Commit 46e38e6

Browse files
Fix StrictVariables exception to show property name instead of type name (#899)
* Initial plan * Fix StrictVariables exception to show property name instead of type name Co-authored-by: sebastienros <1165805+sebastienros@users.noreply.github.com> * Add StrictVariables check to async Awaited method Co-authored-by: sebastienros <1165805+sebastienros@users.noreply.github.com> * Remove unnecessary IdentifierSegment assertions from tests Co-authored-by: sebastienros <1165805+sebastienros@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sebastienros <1165805+sebastienros@users.noreply.github.com>
1 parent d70a5e6 commit 46e38e6

File tree

6 files changed

+63
-1
lines changed

6 files changed

+63
-1
lines changed

Fluid.Tests/StrictVariableTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,41 @@ public async Task UndefinedDelegate_CanReturnCustomValue()
494494
Assert.Equal("[missing not found] [another not found]", result);
495495
}
496496

497+
[Fact]
498+
public async Task StrictVariables_ExceptionMessage_ContainsPropertyName()
499+
{
500+
// This test verifies that the exception message contains the actual property name
501+
// rather than the type name like "Fluid.Ast.IdentifierSegment"
502+
_parser.TryParse("{{ event.userId }}", out var template, out var _);
503+
var options = new TemplateOptions { StrictVariables = true };
504+
var context = new TemplateContext(options);
505+
506+
// Set event but without userId property
507+
context.SetValue("event", new { email = "test@example.com" });
508+
509+
var exception = await Assert.ThrowsAsync<FluidException>(() => template.RenderAsync(context).AsTask());
510+
511+
// The exception message should contain "userId"
512+
Assert.Contains("userId", exception.Message);
513+
}
514+
515+
[Fact]
516+
public async Task StrictVariables_ExceptionMessage_WithNestedProperty()
517+
{
518+
// Test that nested property access also shows the correct property name
519+
_parser.TryParse("{{ user.profile.avatar }}", out var template, out var _);
520+
var options = new TemplateOptions { StrictVariables = true };
521+
var context = new TemplateContext(options);
522+
523+
// Set user with profile but without avatar property
524+
context.SetValue("user", new { profile = new { name = "John" } });
525+
526+
var exception = await Assert.ThrowsAsync<FluidException>(() => template.RenderAsync(context).AsTask());
527+
528+
// The exception message should contain "avatar"
529+
Assert.Contains("avatar", exception.Message);
530+
}
531+
497532
private (TemplateOptions, List<string>) CreateStrictOptions()
498533
{
499534
var missingVariables = new List<string>();

Fluid/Ast/FunctionCallSegment.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,11 @@ public override async ValueTask<FluidValue> ResolveAsync(FluidValue value, Templ
5353

5454
return await value.InvokeAsync(arguments, context);
5555
}
56+
57+
public override string GetSegmentName()
58+
{
59+
// For function call segments, return a generic representation
60+
return "()";
61+
}
5662
}
5763
}

Fluid/Ast/IdentifierSegment.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,10 @@ public override ValueTask<FluidValue> ResolveAsync(FluidValue value, TemplateCon
1717
{
1818
return value.GetValueAsync(Identifier, context);
1919
}
20+
21+
public override string GetSegmentName()
22+
{
23+
return Identifier;
24+
}
2025
}
2126
}

Fluid/Ast/IndexerSegment.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,12 @@ public override async ValueTask<FluidValue> ResolveAsync(FluidValue value, Templ
1616
var index = await Expression.EvaluateAsync(context);
1717
return await value.GetIndexAsync(index, context);
1818
}
19+
20+
public override string GetSegmentName()
21+
{
22+
// For indexer segments, return a representation like [index]
23+
// Since we don't have the evaluated value here, we return a generic representation
24+
return "[index]";
25+
}
1926
}
2027
}

Fluid/Ast/MemberExpression.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public override ValueTask<FluidValue> EvaluateAsync(TemplateContext context)
8181
{
8282
if (context.Options.StrictVariables)
8383
{
84-
throw new FluidException($"Undefined variable '{_segments[i]}'");
84+
throw new FluidException($"Undefined variable '{s.GetSegmentName()}'");
8585
}
8686
return value;
8787
}
@@ -105,6 +105,10 @@ private static async ValueTask<FluidValue> Awaited(
105105
// Stop processing as soon as a member returns nothing
106106
if (value.IsNil())
107107
{
108+
if (context.Options.StrictVariables)
109+
{
110+
throw new FluidException($"Undefined variable '{s.GetSegmentName()}'");
111+
}
108112
return value;
109113
}
110114
}

Fluid/Ast/MemberSegment.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,10 @@ public abstract class MemberSegment
88
/// Resolves the member of a <see cref="FluidValue"/> instance.
99
/// </summary>
1010
public abstract ValueTask<FluidValue> ResolveAsync(FluidValue value, TemplateContext context);
11+
12+
/// <summary>
13+
/// Gets a string representation of this segment for use in error messages.
14+
/// </summary>
15+
public abstract string GetSegmentName();
1116
}
1217
}

0 commit comments

Comments
 (0)