harden(vtag): exclude backslash from passthrough capture + parity tests (A review)
This commit is contained in:
@@ -7,10 +7,17 @@ using System.Text.RegularExpressions;
|
||||
/// which can be evaluated by returning the dependency value directly — no Roslyn compilation.
|
||||
/// Narrow, exact pattern: any near-miss returns false and falls through to the Roslyn path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Physically defined in the Core.Scripting.Abstractions assembly (Roslyn-free, so ControlPlane
|
||||
/// can reference it); the namespace is Core.Scripting to keep consumer using-directives unchanged.
|
||||
/// </remarks>
|
||||
public static partial class PassthroughScript
|
||||
{
|
||||
// ^ \s* return \s+ ctx . GetTag ( "X" ) . Value ; \s* $ (whitespace-tolerant around tokens)
|
||||
[GeneratedRegex(@"^\s*return\s+ctx\s*\.\s*GetTag\s*\(\s*""([^""]+)""\s*\)\s*\.\s*Value\s*;\s*$")]
|
||||
// Tag-name class [^"\\] excludes both the closing quote and backslash: a literal containing a
|
||||
// backslash escape (e.g. "a\\b" → runtime name a\b) won't match, so it correctly falls through
|
||||
// to Roslyn, which interprets the escape and resolves the actual dependency key.
|
||||
[GeneratedRegex(@"^\s*return\s+ctx\s*\.\s*GetTag\s*\(\s*""([^""\\]+)""\s*\)\s*\.\s*Value\s*;\s*$")]
|
||||
private static partial Regex MirrorRegex();
|
||||
|
||||
/// <summary>True if <paramref name="source"/> is the mirror passthrough shape; outputs the referenced tag.</summary>
|
||||
|
||||
@@ -76,4 +76,26 @@ public sealed class PassthroughScriptTests
|
||||
PassthroughScript.TryMatch(source, out var tag).ShouldBeFalse();
|
||||
tag.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A tag literal containing a backslash escape (C# source <c>"a\\b"</c> → runtime name
|
||||
/// <c>a\b</c>) does NOT match the passthrough pattern — it falls through to Roslyn, which
|
||||
/// interprets the escape and resolves the correct dependency key. Capturing the raw source
|
||||
/// text <c>a\\b</c> would produce a wrong-result silent miss against the key <c>a\b</c>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rejects_tag_literal_containing_backslash_escape()
|
||||
{
|
||||
// C# literal "return ctx.GetTag(\"a\\\\b\").Value;" → script source contains: a\\b (two chars: backslash + b)
|
||||
PassthroughScript.TryMatch("return ctx.GetTag(\"a\\\\b\").Value;", out var tag).ShouldBeFalse();
|
||||
tag.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>A plain dotted tag name (no backslash) still matches — the fix is additive only.</summary>
|
||||
[Fact]
|
||||
public void Matches_plain_dotted_tag_after_backslash_fix()
|
||||
{
|
||||
PassthroughScript.TryMatch("return ctx.GetTag(\"Site1.Area.Tag\").Value;", out var tag).ShouldBeTrue();
|
||||
tag.ShouldBe("Site1.Area.Tag");
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -200,6 +200,38 @@ public sealed class RoslynVirtualTagEvaluatorTests
|
||||
result.Reason.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Direct parity test: the EXACT mirror source <c>return ctx.GetTag("missing").Value;</c>
|
||||
/// evaluated against an empty dependency dictionary takes the fast-path and yields
|
||||
/// <c>Ok(null)</c>; a minimally-altered but semantically-identical non-passthrough variant
|
||||
/// <c>return (object?)ctx.GetTag("missing").Value;</c> compiled by Roslyn against the same
|
||||
/// empty deps yields the identical <c>Ok(null)</c> — proving the fast-path is byte-identical
|
||||
/// to the Roslyn path for the missing-dependency case.</summary>
|
||||
[Fact]
|
||||
public void Passthrough_exact_mirror_missing_dep_matches_Roslyn_baseline()
|
||||
{
|
||||
using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance);
|
||||
|
||||
// Fast-path: exact mirror shape → passthrough returns Ok(null) for missing dep.
|
||||
var fastPath = sut.Evaluate(
|
||||
"vt-parity-fast",
|
||||
"return ctx.GetTag(\"missing\").Value;",
|
||||
new Dictionary<string, object?>());
|
||||
|
||||
fastPath.Success.ShouldBeTrue(fastPath.Reason);
|
||||
fastPath.Value.ShouldBeNull();
|
||||
fastPath.Reason.ShouldBeNull();
|
||||
|
||||
// Roslyn path: `(object?)` cast forces compilation but reads the same missing tag.
|
||||
var roslynPath = sut.Evaluate(
|
||||
"vt-parity-roslyn",
|
||||
"return (object?)ctx.GetTag(\"missing\").Value;",
|
||||
new Dictionary<string, object?>());
|
||||
|
||||
roslynPath.Success.ShouldBeTrue(roslynPath.Reason);
|
||||
roslynPath.Value.ShouldBeNull();
|
||||
roslynPath.Reason.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Decisive proof the fast-path skips Roslyn: the compiled-script cache (which every
|
||||
/// Roslyn evaluation populates via <c>GetOrAdd</c>) stays EMPTY after a mirror evaluation, then
|
||||
/// grows to one entry once a genuine (non-mirror) expression forces compilation.</summary>
|
||||
|
||||
Reference in New Issue
Block a user