From e643a53ae765a25af73f7fcf724c8606f6075bcd Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Sat, 20 Jun 2026 08:23:49 -0400 Subject: [PATCH] fix: case-sensitive resolution of same-spelling variables (issue #434) When multiple variables share the same spelling but differ only in case (e.g. TEST vs test), the helpers dictionary's case-insensitive lookup could return a LateBindHelperDescriptor cached for a differently-cased path. This caused {{test}} to resolve to the value of TEST. Fix detects this mismatch in PathBinder and creates a fresh LateBindHelperDescriptor carrying the exact casing of the current expression. Co-Authored-By: Claude Sonnet 4.6 --- .../Handlebars.Test/Issues/Issue434Tests.cs | 22 +++++++++++++++++++ .../Translation/Expression/PathBinder.cs | 10 +++++++++ 2 files changed, 32 insertions(+) create mode 100644 source/Handlebars.Test/Issues/Issue434Tests.cs diff --git a/source/Handlebars.Test/Issues/Issue434Tests.cs b/source/Handlebars.Test/Issues/Issue434Tests.cs new file mode 100644 index 00000000..c1844278 --- /dev/null +++ b/source/Handlebars.Test/Issues/Issue434Tests.cs @@ -0,0 +1,22 @@ +using System.Dynamic; +using Xunit; + +namespace HandlebarsDotNet.Test +{ + public class Issue434Tests + { + [Fact] + public void Issue434_CaseSensitiveLookupWithSameSpellingVariables() + { + var h = Handlebars.Create(); + var template = h.Compile("{{TEST}} {{test}}"); + dynamic data = new ExpandoObject(); + data.TEST = "Upper"; + data.test = "Lower"; + var result = template(data); + var parts = result.Split(' '); + Assert.Equal("Upper", parts[0]); + Assert.Equal("Lower", parts[1]); + } + } +} diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 33d1b232..d3d47c10 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -46,6 +46,16 @@ protected override Expression VisitPathExpression(PathExpression pex) helper = new Ref>(lateBindHelperDescriptor); configuration.Helpers.AddOrReplace(pathInfoLight, helper); } + else if (helper.Value is LateBindHelperDescriptor existingLateBindHelper + && !string.Equals(existingLateBindHelper.Name.Path, pathInfo.Path, System.StringComparison.Ordinal)) + { + // The helpers dictionary uses case-insensitive keys, so "TEST" and "test" map to the + // same slot. If the cached LateBindHelperDescriptor was stored for a differently-cased + // path, create a new one that carries the exact casing of the current expression so + // that runtime property lookup resolves to the right member. + var lateBindHelperDescriptor = new LateBindHelperDescriptor(pathInfo); + helper = new Ref>(lateBindHelperDescriptor); + } else if (configuration.Compatibility.RelaxedHelperNaming) { pathInfoLight = pathInfoLight.TagComparer();