feat(adminui): script-analysis contracts, wrapper seam, endpoints + DI
This commit is contained in:
@@ -47,6 +47,9 @@ public static class EndpointRouteBuilderExtensions
|
||||
services.AddSingleton<IDriverBrowser, OpcUaClientDriverBrowser>();
|
||||
services.AddSingleton<IDriverBrowser, GalaxyDriverBrowser>();
|
||||
|
||||
// Roslyn-backed Monaco script-editor analysis (diagnostics/completions/hover/...).
|
||||
services.AddScoped<ScriptAnalysis.ScriptAnalysisService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
||||
|
||||
public record DiagnoseRequest(string Code);
|
||||
public record DiagnoseResponse(IReadOnlyList<DiagnosticMarker> Markers);
|
||||
public record DiagnosticMarker(int Severity, int StartLineNumber, int StartColumn,
|
||||
int EndLineNumber, int EndColumn, string Message, string Code);
|
||||
|
||||
public record CompletionsRequest(string CodeText, int Line, int Column);
|
||||
public record CompletionsResponse(IReadOnlyList<CompletionItem> Items);
|
||||
public record CompletionItem(string Label, string InsertText, string Detail, string Kind, int InsertTextRules = 0);
|
||||
|
||||
public record HoverRequest(string CodeText, int Line, int Column);
|
||||
public record HoverResponse(string? Markdown);
|
||||
|
||||
public record SignatureHelpRequest(string CodeText, int Line, int Column);
|
||||
public record SignatureHelpResponse(string? Label, IReadOnlyList<SignatureHelpParameter>? Parameters, int ActiveParameter);
|
||||
public record SignatureHelpParameter(string Label, string? Documentation);
|
||||
|
||||
public record FormatRequest(string Code);
|
||||
public record FormatResponse(string Code);
|
||||
|
||||
public record InlayHintsRequest(string Code);
|
||||
public record InlayHintsResponse(IReadOnlyList<InlayHint> Hints);
|
||||
public record InlayHint(int Line, int Column, string Label);
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
||||
|
||||
public static class ScriptAnalysisEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapScriptAnalysisEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/script-analysis").RequireAuthorization("FleetAdmin");
|
||||
group.MapPost("/diagnostics", (DiagnoseRequest r, ScriptAnalysisService s) => Results.Ok(s.Diagnose(r)));
|
||||
group.MapPost("/completions", async (CompletionsRequest r, ScriptAnalysisService s) => Results.Ok(await s.CompleteAsync(r)));
|
||||
group.MapPost("/hover", (HoverRequest r, ScriptAnalysisService s) => Results.Ok(s.Hover(r)));
|
||||
group.MapPost("/signature-help", (SignatureHelpRequest r, ScriptAnalysisService s) => Results.Ok(s.SignatureHelp(r)));
|
||||
group.MapPost("/format", (FormatRequest r, ScriptAnalysisService s) => Results.Ok(s.Format(r)));
|
||||
group.MapPost("/inlay-hints", (InlayHintsRequest r, ScriptAnalysisService s) => Results.Ok(s.InlayHints(r)));
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Roslyn-backed analysis for the Monaco virtual-tag script editor. Re-seats the
|
||||
/// scadabridge analysis surface on OtOpcUa's REAL evaluator wrapper: the user's
|
||||
/// source is wrapped in the same synthesized <c>CompiledScript.Run(globals)</c> shape
|
||||
/// that <see cref="ScriptEvaluator{TContext, TResult}"/> compiles, so diagnostics,
|
||||
/// completions, hover, and signature help all see the exact symbol surface a published
|
||||
/// script gets. Built against the <see cref="ScriptSandbox"/> allow-list for the
|
||||
/// <see cref="VirtualTagContext"/> context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Public methods are placeholders returning empty results — later tasks fill in each
|
||||
/// capability via TDD. The shared <see cref="Analyze"/> seam + the offset helpers are
|
||||
/// the load-bearing part every later capability reuses; keep them even while unused.
|
||||
/// </remarks>
|
||||
public sealed class ScriptAnalysisService
|
||||
{
|
||||
private static readonly SandboxConfig Sandbox = ScriptSandbox.Build(typeof(VirtualTagContext));
|
||||
private static readonly string Preamble = BuildPreamble();
|
||||
private static readonly CSharpCompilationOptions CompileOptions = new(
|
||||
OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release,
|
||||
allowUnsafe: false, warningLevel: 4, nullableContextOptions: NullableContextOptions.Enable);
|
||||
|
||||
// Mirrors ScriptEvaluator's wrapper EXACTLY (usings from the sandbox + the
|
||||
// Run(ScriptGlobals<VirtualTagContext>) shape), except the analysis return type is
|
||||
// `object` so a shared script stays valid regardless of any virtual tag's DataType.
|
||||
private static string BuildPreamble()
|
||||
{
|
||||
var usings = string.Join("\n", Sandbox.Imports.Select(i => $"using {i};"));
|
||||
var globalsType = $"global::{typeof(ScriptGlobals<>).Namespace}.ScriptGlobals<global::{typeof(VirtualTagContext).FullName}>";
|
||||
return usings + "\n\n"
|
||||
+ "namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Compiled;\n"
|
||||
+ "public static class CompiledScript\n{\n"
|
||||
+ $" public static object Run({globalsType} globals)\n {{\n"
|
||||
+ " var ctx = globals.ctx;\n#line 1\n";
|
||||
}
|
||||
|
||||
// userSource is appended after Preamble, then the method/class are closed.
|
||||
private static (SyntaxTree Tree, CSharpCompilation Compilation, SemanticModel Model, int PreambleLength) Analyze(string userSource)
|
||||
{
|
||||
var prefix = Preamble;
|
||||
var full = prefix + (userSource ?? "") + "\n }\n}\n";
|
||||
var tree = CSharpSyntaxTree.ParseText(full);
|
||||
var compilation = CSharpCompilation.Create("ScriptAnalysis", new[] { tree }, Sandbox.References, CompileOptions);
|
||||
return (tree, compilation, compilation.GetSemanticModel(tree), prefix.Length);
|
||||
}
|
||||
|
||||
// editor (line,col) in USER-source coords -> physical offset in the wrapped document
|
||||
private static int OffsetInWrapped(string userSource, int line, int column, int preambleLength)
|
||||
=> preambleLength + Math.Clamp(PositionToOffset(userSource ?? "", line, column), 0, (userSource ?? "").Length);
|
||||
|
||||
private static int PositionToOffset(string code, int line, int column)
|
||||
{
|
||||
int offset = 0, curLine = 1, curCol = 1;
|
||||
for (int i = 0; i < code.Length; i++)
|
||||
{
|
||||
if (curLine == line && curCol == column) return offset;
|
||||
if (code[i] == '\n') { curLine++; curCol = 1; } else { curCol++; }
|
||||
offset = i + 1;
|
||||
}
|
||||
return code.Length;
|
||||
}
|
||||
|
||||
public DiagnoseResponse Diagnose(DiagnoseRequest req) => new(Array.Empty<DiagnosticMarker>()); // Task 3
|
||||
public Task<CompletionsResponse> CompleteAsync(CompletionsRequest req)
|
||||
=> Task.FromResult(new CompletionsResponse(Array.Empty<CompletionItem>())); // Tasks 4,6
|
||||
public HoverResponse Hover(HoverRequest req) => new((string?)null); // Task 7
|
||||
public SignatureHelpResponse SignatureHelp(SignatureHelpRequest req) => new(null, null, 0); // Task 7
|
||||
public FormatResponse Format(FormatRequest req) => new(req.Code); // Task 8
|
||||
public InlayHintsResponse InlayHints(InlayHintsRequest req) => new(Array.Empty<InlayHint>()); // Task 8 (stays empty)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using ZB.MOM.WW.OtOpcUa.AdminUI;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Clients;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
||||
using ZB.MOM.WW.OtOpcUa.Cluster;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane;
|
||||
@@ -183,6 +184,7 @@ if (hasAdmin)
|
||||
app.UseAntiforgery();
|
||||
app.MapOtOpcUaAuth();
|
||||
app.MapAdminUI<App>();
|
||||
app.MapScriptAnalysisEndpoints();
|
||||
app.MapOtOpcUaHubs();
|
||||
// Headless deploy trigger for CI/scripts (API-key gated; disabled until Security:DeployApiKey set).
|
||||
app.MapOtOpcUaDeployApi(app.Configuration);
|
||||
|
||||
Reference in New Issue
Block a user