using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
///
/// Advisory (non-blocking) analysis that finds scripts synchronously blocking on a
/// — .Result, .Wait(...), Task.WaitAll/WaitAny
/// and .GetAwaiter().GetResult() (review 2026-10-21 01/SR-U2).
///
///
/// Site scripts run on the dedicated ScriptExecutionScheduler pool, and not
/// every runtime helper completes off that pool, so a script that blocks on a helper's
/// task can hold a worker while the helper's continuation waits for a worker — enough
/// concurrent runs and the pool starves until the stuck-script watchdog detaches
/// workers. await is always the right form. This is an advisory rather than a
/// violation: the trust policy is a deny-list over API
/// roots whose every entry blocks deploy, and turning this into a deploy-blocking
/// error would break already-deployed scripts on their next redeploy.
///
///
///
/// Resolution is semantic (the script is bound against globalsType), so a
/// member that merely happens to be named Result or Wait on a non-Task
/// type is not reported. Unresolved code is silently skipped — compile errors are the
/// compile gate's job.
///
///
public static class BlockingWaitAnalyzer
{
private const string TasksNamespace = "System.Threading.Tasks";
private const string CompilerServicesNamespace = "System.Runtime.CompilerServices";
///
/// Returns one advisory message per blocking wait found in .
/// Empty when the script never blocks on a task (or is blank).
///
/// The C# script source.
/// The globals type the script binds against (e.g. ).
/// Advisory messages naming the member and its line.
public static IReadOnlyList Find(string code, Type? globalsType)
{
if (string.IsNullOrWhiteSpace(code))
return Array.Empty();
var compilation = RoslynScriptCompiler.CreateScript(code, globalsType).GetCompilation();
var tree = compilation.SyntaxTrees.Single();
var model = compilation.GetSemanticModel(tree);
var advisories = new List();
foreach (var access in tree.GetRoot().DescendantNodes().OfType())
{
var symbol = model.GetSymbolInfo(access).Symbol;
var form = symbol switch
{
IPropertySymbol { Name: "Result" } p when IsTaskType(p.ContainingType) => ".Result",
IMethodSymbol { Name: "Wait" } m when IsTaskType(m.ContainingType) => ".Wait()",
IMethodSymbol { Name: "WaitAll" or "WaitAny" } m when IsTaskType(m.ContainingType) => $"Task.{m.Name}()",
IMethodSymbol { Name: "GetResult" } m when IsAwaiterType(m.ContainingType) => ".GetAwaiter().GetResult()",
_ => null,
};
if (form == null)
continue;
var line = access.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
advisories.Add(
$"blocks on a Task with '{form}' (line {line}) — use 'await' instead; a blocked " +
"script-pool worker can starve the pool the awaited work needs to complete.");
}
return advisories;
}
private static bool IsTaskType(INamedTypeSymbol? type)
{
for (var t = type; t != null; t = t.BaseType)
{
if (t.Name is "Task" or "ValueTask"
&& t.ContainingNamespace?.ToDisplayString() == TasksNamespace)
{
return true;
}
}
return false;
}
private static bool IsAwaiterType(INamedTypeSymbol? type)
=> type != null
&& type.Name.EndsWith("Awaiter", StringComparison.Ordinal)
&& (type.ContainingNamespace?.ToDisplayString() == CompilerServicesNamespace
|| type.ContainingType?.ContainingNamespace?.ToDisplayString() == CompilerServicesNamespace);
}