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); }