feat(commons): TryParseRelayBody — detect pure ctx.GetTag relay scripts

This commit is contained in:
Joseph Doherty
2026-06-11 20:59:10 -04:00
parent 93a9c6d3db
commit 2ba2f8a899
2 changed files with 58 additions and 0 deletions
@@ -32,6 +32,13 @@ public static class EquipmentScriptPaths
private static readonly Regex PathLiteralRegex =
new(@"(ctx\s*\.\s*(?:GetTag|SetVirtualTag)\s*\(\s*"")([^""]*)("")", RegexOptions.Compiled);
// A pure pass-through virtual-tag body: exactly `return ctx.GetTag("<ref>").Value;`
// (whitespace-insensitive). Captures <ref> for relay→alias conversion. Anything with extra
// statements, arithmetic, a different member than .Value, or multiple GetTag calls is NOT a relay.
private static readonly Regex RelayBodyRegex = new(
@"^\s*return\s+ctx\s*\.\s*GetTag\s*\(\s*""([^""]+)""\s*\)\s*\.\s*Value\s*;\s*$",
RegexOptions.Compiled);
/// <summary>True when the source uses the <c>{{equip}}</c> token anywhere.</summary>
/// <param name="source">The script source to scan.</param>
public static bool ContainsEquipToken(string? source) =>
@@ -136,4 +143,30 @@ public static class EquipmentScriptPaths
return result;
}
/// <summary>
/// Recognise a pure relay virtual-tag body — one whose entire body is exactly
/// <c>return ctx.GetTag("<paramref name="tagReference"/>").Value;</c> (whitespace-insensitive) —
/// and capture its single <c>GetTag</c> reference. Bodies with extra statements, arithmetic,
/// a member other than <c>.Value</c>, or multiple <c>GetTag</c> calls are <em>not</em> relays.
/// </summary>
/// <param name="source">The virtual-tag script source to inspect.</param>
/// <param name="tagReference">
/// When the method returns <see langword="true"/>, the captured <c>GetTag</c> path literal
/// (e.g. <c>TestMachine_020.TestChangingInt</c> or <c>{{equip}}.Speed</c>);
/// otherwise <see langword="null"/>.
/// </param>
/// <returns>
/// <see langword="true"/> when <paramref name="source"/> is a pure pass-through relay;
/// <see langword="false"/> for any other body or <see langword="null"/>/<see langword="whitespace"/> input.
/// </returns>
public static bool TryParseRelayBody(string? source, out string? tagReference)
{
tagReference = null;
if (string.IsNullOrWhiteSpace(source)) return false;
var m = RelayBodyRegex.Match(source);
if (!m.Success) return false;
tagReference = m.Groups[1].Value;
return true;
}
}
@@ -211,4 +211,29 @@ public class EquipmentScriptPathsTests
{
EquipmentScriptPaths.ContainsEquipToken(null).ShouldBeFalse();
}
// ---- TryParseRelayBody ----
[Theory]
[InlineData("return ctx.GetTag(\"TestMachine_020.TestChangingInt\").Value;", "TestMachine_020.TestChangingInt")]
[InlineData(" return ctx . GetTag ( \"A.B\" ) . Value ; ", "A.B")]
[InlineData("return ctx.GetTag(\"{{equip}}.Speed\").Value;", "{{equip}}.Speed")]
public void TryParseRelayBody_accepts_exact_passthrough(string src, string expectedRef)
{
EquipmentScriptPaths.TryParseRelayBody(src, out var r).ShouldBeTrue();
r.ShouldBe(expectedRef);
}
[Theory]
[InlineData("return ctx.GetTag(\"A.B\").Value * 2;")]
[InlineData("var x = ctx.GetTag(\"A.B\").Value; return x;")]
[InlineData("return ctx.GetTag(\"A.B\").Value + ctx.GetTag(\"C.D\").Value;")]
[InlineData("return ctx.GetTag(\"A.B\").Quality;")]
[InlineData("return 5;")]
[InlineData("")]
public void TryParseRelayBody_rejects_non_relay(string src)
{
EquipmentScriptPaths.TryParseRelayBody(src, out var r).ShouldBeFalse();
r.ShouldBeNull();
}
}