From aa493b6356fa659605d8bbaa6d20e475eff0add1 Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Wed, 5 Aug 2026 21:35:22 -0400 Subject: [PATCH 1/2] fix: resolve SonarCloud reliability regression from nullable-references PR PR #642 touched two public readonly array field declarations (adding `?` annotations) without changing their design, which caused SonarCloud to flag S3887 ("non-private readonly field exposes mutable array") as new Bug-type issues on both lines, dropping the reliability rating from A to B. - Closure.A: made internal, since Closure's constructor is already internal and the field is unreachable by external consumers. Added InternalsVisibleTo("Handlebars.Test") for the one test that reads it directly, and pass BindingFlags.NonPublic to the reflection GetField call that resolves this field for compiled closure expressions. - PathInfo.Segments: left public with a NOSONAR suppression instead, since PathInfo instances do reach public surfaces (HelperOptions, PathExpression, etc.) and the field has been public since 2020 - flipping it to internal would be a real breaking change. --- source/Handlebars/Compiler/ClosureBuilder.cs | 4 ++-- source/Handlebars/PathStructure/PathInfo.cs | 2 +- source/Handlebars/Properties/AssemblyInfo.cs | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 source/Handlebars/Properties/AssemblyInfo.cs diff --git a/source/Handlebars/Compiler/ClosureBuilder.cs b/source/Handlebars/Compiler/ClosureBuilder.cs index 808258bc..43b30155 100644 --- a/source/Handlebars/Compiler/ClosureBuilder.cs +++ b/source/Handlebars/Compiler/ClosureBuilder.cs @@ -81,7 +81,7 @@ public KeyValuePair> Bui BuildKnownValuesExpressions(closureExpression, mapping, _blockDecorators, "BDD", 4); BuildKnownValuesExpressions(closureExpression, mapping, _decoratorDelegates, "DDD", 4); - var arrayField = closureType.GetField("A"); + var arrayField = closureType.GetField("A", BindingFlags.NonPublic | BindingFlags.Instance); var array = Expression.Field(closureExpression, arrayField!); for (int index = 0; index < _other.Count; index++) { @@ -160,7 +160,7 @@ public sealed class Closure public readonly ChainSegment[]? BP0; public readonly ChainSegment[][]? BPA; - public readonly object?[] A; + internal readonly object?[] A; internal Closure( List> pathInfos, diff --git a/source/Handlebars/PathStructure/PathInfo.cs b/source/Handlebars/PathStructure/PathInfo.cs index 57b6b90e..e5b97c40 100644 --- a/source/Handlebars/PathStructure/PathInfo.cs +++ b/source/Handlebars/PathStructure/PathInfo.cs @@ -105,7 +105,7 @@ private PathInfo( /// // [MemberNotNullWhen(true, nameof(Segments), nameof(TrimmedPath))] public readonly bool IsVariable; - public readonly PathSegment[]? Segments; + public readonly PathSegment[]? Segments; // NOSONAR: public since v1.0 (2020); staying public avoids a breaking API change public readonly string Path; public readonly string? TrimmedPath; diff --git a/source/Handlebars/Properties/AssemblyInfo.cs b/source/Handlebars/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..8241d321 --- /dev/null +++ b/source/Handlebars/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Handlebars.Test")] From ebcac9f773db4b7d543f737d9a596397344ad61e Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Wed, 5 Aug 2026 21:41:03 -0400 Subject: [PATCH 2/2] fix: revert Closure.A to public to avoid new S3011 maintainability smell Making Closure.A internal required BindingFlags.NonPublic on the reflection GetField("A") lookup used to build compiled closure expressions. SonarCloud flagged that explicit accessibility bypass as S3011 (MAJOR), which dropped Maintainability Rating on New Code to C and failed the Quality Gate. Closure's constructor is already internal, so A was never reachable by external consumers anyway - reverting to public (with NOSONAR, matching the treatment already given to PathInfo.Segments) removes the smell without any real encapsulation loss, and drops the now-unnecessary InternalsVisibleTo attribute. --- source/Handlebars/Compiler/ClosureBuilder.cs | 4 ++-- source/Handlebars/Properties/AssemblyInfo.cs | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 source/Handlebars/Properties/AssemblyInfo.cs diff --git a/source/Handlebars/Compiler/ClosureBuilder.cs b/source/Handlebars/Compiler/ClosureBuilder.cs index 43b30155..9af4f4c9 100644 --- a/source/Handlebars/Compiler/ClosureBuilder.cs +++ b/source/Handlebars/Compiler/ClosureBuilder.cs @@ -81,7 +81,7 @@ public KeyValuePair> Bui BuildKnownValuesExpressions(closureExpression, mapping, _blockDecorators, "BDD", 4); BuildKnownValuesExpressions(closureExpression, mapping, _decoratorDelegates, "DDD", 4); - var arrayField = closureType.GetField("A", BindingFlags.NonPublic | BindingFlags.Instance); + var arrayField = closureType.GetField("A"); var array = Expression.Field(closureExpression, arrayField!); for (int index = 0; index < _other.Count; index++) { @@ -160,7 +160,7 @@ public sealed class Closure public readonly ChainSegment[]? BP0; public readonly ChainSegment[][]? BPA; - internal readonly object?[] A; + public readonly object?[] A; // NOSONAR: public since 2020; reflection-based codegen in this file needs public GetField lookup internal Closure( List> pathInfos, diff --git a/source/Handlebars/Properties/AssemblyInfo.cs b/source/Handlebars/Properties/AssemblyInfo.cs deleted file mode 100644 index 8241d321..00000000 --- a/source/Handlebars/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Handlebars.Test")]