fix(adminui): ctx-receiver guard + truthful SetVirtualTag hover in script-editor completions
This commit is contained in:
@@ -161,7 +161,7 @@ public sealed class ScriptAnalysisService
|
||||
// position is the offset just AFTER the caret; -1 finds the token the caret sits at/just-after (e.g. the dot in "ctx.").
|
||||
var token = root.FindToken(Math.Max(0, position - 1));
|
||||
|
||||
if (_catalog != null && TryGetTagPathLiteral(token, out var pathPrefix))
|
||||
if (_catalog != null && TryGetTagPathLiteral(token, out var pathPrefix, out _))
|
||||
{
|
||||
const string equipDot = EquipmentScriptPaths.EquipToken + "."; // "{{equip}}."
|
||||
if (pathPrefix.StartsWith(equipDot, StringComparison.Ordinal))
|
||||
@@ -210,9 +210,10 @@ public sealed class ScriptAnalysisService
|
||||
.Select(ToCompletionItem).Take(200).ToList();
|
||||
}
|
||||
|
||||
private static bool TryGetTagPathLiteral(SyntaxToken token, out string prefix)
|
||||
private static bool TryGetTagPathLiteral(SyntaxToken token, out string prefix, out bool isSetVirtualTag)
|
||||
{
|
||||
prefix = "";
|
||||
isSetVirtualTag = false;
|
||||
var literal = token.Parent as LiteralExpressionSyntax
|
||||
?? token.GetPreviousToken().Parent as LiteralExpressionSyntax;
|
||||
if (literal is null || !literal.IsKind(SyntaxKind.StringLiteralExpression)) return false;
|
||||
@@ -220,11 +221,16 @@ public sealed class ScriptAnalysisService
|
||||
if (arg.Parent is not ArgumentListSyntax argList) return false;
|
||||
if (argList.Parent is not InvocationExpressionSyntax inv) return false;
|
||||
if (inv.Expression is not MemberAccessExpressionSyntax ma) return false;
|
||||
// Receiver guard: only ctx.GetTag(...) / ctx.SetVirtualTag(...) are real tag-path calls. Mirrors the
|
||||
// runtime harvest (EquipmentScriptPaths.GetTagRefRegex is syntactically `ctx`-anchored), so the editor
|
||||
// offers tag completions/hover for exactly what Phase7Composer harvests — not an unrelated x.GetTag(...).
|
||||
if (ma.Expression is not IdentifierNameSyntax { Identifier.ValueText: "ctx" }) return false;
|
||||
var method = ma.Name.Identifier.ValueText;
|
||||
if (method is not ("GetTag" or "SetVirtualTag")) return false;
|
||||
// only the FIRST argument is the path
|
||||
if (argList.Arguments.Count > 0 && argList.Arguments[0] != arg) return false;
|
||||
prefix = literal.Token.ValueText ?? "";
|
||||
isSetVirtualTag = method == "SetVirtualTag";
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -252,7 +258,7 @@ public sealed class ScriptAnalysisService
|
||||
// Tag-path hover takes priority over C# symbol resolution: when the caret sits on a
|
||||
// ctx.GetTag("…")/ctx.SetVirtualTag("…") path literal, show the resolved tag's info
|
||||
// (or note it's not a known configured tag path) instead of the string-literal symbol.
|
||||
if (_catalog is not null && TryGetTagPathLiteral(token, out var tagPath) && !string.IsNullOrEmpty(tagPath))
|
||||
if (_catalog is not null && TryGetTagPathLiteral(token, out var tagPath, out var isSetVirtualTag) && !string.IsNullOrEmpty(tagPath))
|
||||
{
|
||||
static string Code(string s) => s.Replace("`", "\\`");
|
||||
// Equipment-relative literal (contains the {{equip}} token): it cannot resolve to a
|
||||
@@ -270,6 +276,11 @@ public sealed class ScriptAnalysisService
|
||||
? $"**Tag path** `{Code(tagPath)}`\n\n⚠ Not a known configured tag path."
|
||||
: $"**Tag path** `{Code(info.Path)}`\n\n{info.Kind} · Type **{info.DataType}**"
|
||||
+ (info.DriverInstanceId is null ? "" : $" · Driver `{Code(info.DriverInstanceId)}`");
|
||||
// Truthfulness: ctx.SetVirtualTag(...) is a no-op in the live single-tag evaluator
|
||||
// (RoslynVirtualTagEvaluator drops cross-tag writes; the cascade VirtualTagEngine is dormant).
|
||||
if (isSetVirtualTag)
|
||||
tagMd += "\n\n⚠ Cross-tag `SetVirtualTag` writes are currently dropped in single-tag mode " +
|
||||
"(the cascade engine is not wired into the host); this write will not take effect at runtime.";
|
||||
return new HoverResponse(tagMd);
|
||||
}
|
||||
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;
|
||||
|
||||
public sealed class CtxCompletionGuardTests
|
||||
{
|
||||
private sealed class FakeCatalog : IScriptTagCatalog
|
||||
{
|
||||
public Task<IReadOnlyList<string>> GetPathsAsync(string? f, CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<string>>(new[] { "Line1.Speed" });
|
||||
public Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct)
|
||||
=> Task.FromResult<ScriptTagInfo?>(new ScriptTagInfo(path, "Virtual tag", "Double", null));
|
||||
public Task<IReadOnlyList<string>> GetEquipmentRelativeLeavesAsync(string? f, CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<string>>(System.Array.Empty<string>());
|
||||
}
|
||||
|
||||
private static readonly ScriptAnalysisService Svc = new(new FakeCatalog());
|
||||
|
||||
[Fact] public async Task Completion_on_ctx_GetTag_literal_offers_catalog_paths()
|
||||
{
|
||||
// "return ctx.GetTag(" + '""' + ')' — col 20 lands on the closing quote of the empty string literal.
|
||||
var res = await Svc.CompleteAsync(new CompletionsRequest("return ctx.GetTag(\"\")", 1, 20));
|
||||
res.Items.ShouldContain(i => i.InsertText == "Line1.Speed");
|
||||
}
|
||||
|
||||
[Fact] public async Task Completion_on_non_ctx_receiver_does_NOT_offer_catalog_paths()
|
||||
{
|
||||
// Same shape but receiver is "foo" — must NOT offer catalog paths.
|
||||
var res = await Svc.CompleteAsync(new CompletionsRequest("return foo.GetTag(\"\")", 1, 20));
|
||||
res.Items.ShouldNotContain(i => i.InsertText == "Line1.Speed");
|
||||
}
|
||||
|
||||
[Fact] public async Task Hover_on_ctx_SetVirtualTag_literal_warns_write_is_dropped()
|
||||
{
|
||||
var md = (await Svc.Hover(new HoverRequest("return ctx.SetVirtualTag(\"V\", 1);", 1, 27))).Markdown;
|
||||
md.ShouldNotBeNull();
|
||||
md!.ShouldContain("single-tag mode");
|
||||
}
|
||||
|
||||
[Fact] public async Task Hover_on_ctx_GetTag_literal_has_no_dropped_write_note()
|
||||
{
|
||||
var md = (await Svc.Hover(new HoverRequest("return ctx.GetTag(\"V\").Value;", 1, 20))).Markdown;
|
||||
md.ShouldNotBeNull();
|
||||
md!.ShouldNotContain("single-tag mode");
|
||||
}
|
||||
|
||||
[Fact] public async Task Hover_on_non_ctx_receiver_literal_is_not_treated_as_a_tag_path()
|
||||
{
|
||||
var md = (await Svc.Hover(new HoverRequest("return bar.GetTag(\"V\");", 1, 20))).Markdown;
|
||||
(md is null || !md.Contains("Tag path")).ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user