fix(scriptanalysis): warn on scripts blocking on a Task (.Result/.Wait()/GetAwaiter().GetResult()) (review 2026-10-21 01/SR-U2)

Advisory, not a trust-policy violation: every policy entry blocks deploy, and
an error here would fail the next redeploy of already-deployed scripts.
ValidationService surfaces it as a non-blocking ScriptCompilation warning.
This commit is contained in:
Joseph Doherty
2026-10-22 06:22:01 -04:00
parent 2ca60f86ff
commit 5919a71b8b
8 changed files with 270 additions and 17 deletions
@@ -126,7 +126,7 @@ Violations from both passes are merged and deduplicated before being returned.
#### `Compile(string code, Type? globalsType = null, IEnumerable<MetadataReference>? extraReferences = null, IEnumerable<string>? extraImports = null)`
- Creates a `CSharpScript` with the given code, `globalsType`, references (defaults from `ScriptTrustPolicy.DefaultReferences` plus `extraReferences`), and imports (defaults from `ScriptTrustPolicy.DefaultImports` plus `extraImports`).
- Calls `.Compile()` and returns the resulting `Diagnostic[]` filtered to **error-severity diagnostics only**. This is a compile *gate*: a warning must not block a deploy, so only errors (undefined symbols, type mismatches) are surfaced to callers as gate failures.
- Reads `Script.GetCompilation().GetDiagnostics()` (no emit, no assembly load — see the verdict-cache notes under REQ-SA-4) and returns the diagnostics filtered to **error-severity diagnostics only**. Non-diagnostic faults are thrown, never returned as a message. This is a compile *gate*: a warning must not block a deploy, so only errors (undefined symbols, type mismatches) are surfaced to callers as gate failures.
- Each caller passes its own `globalsType` — `ScriptCompileSurface` for the design-time deploy gate, the real `ScriptGlobals` for Site Runtime execution, `null` for pure syntax checks.
#### `ParseDiagnostics(string code)`
@@ -134,6 +134,12 @@ Violations from both passes are merged and deduplicated before being returned.
- Parses the script text using Roslyn's `CSharpSyntaxTree.ParseText` and returns **error-severity** syntax-level diagnostics only (consistent with `Compile` — warnings do not fail the gate).
- No compilation is performed — useful for fast syntax checks where no globals type is available.
#### `BlockingWaitAnalyzer.Find(string code, Type? globalsType)` — advisory
Review 2026-10-21 01/SR-U2. Returns one **advisory** message per synchronous wait on a task in a script body — `.Result` / `.Wait(...)` on `Task`/`ValueTask` (and subclasses), `Task.WaitAll`/`WaitAny`, and `GetResult()` on a `System.Runtime.CompilerServices` awaiter (`.GetAwaiter().GetResult()`, including through `ConfigureAwait`). Site scripts run on the dedicated `ScriptExecutionScheduler` pool and not every runtime helper completes off that pool, so `var r = Instance.CallScript("Calc").Result;` in enough concurrent runs starves the pool until the stuck-script watchdog detaches workers. Resolution is semantic against the caller's `globalsType`, so a non-task member named `Result`/`Wait` is not flagged.
It is deliberately **not** a `ScriptTrustPolicy` violation: every trust-policy entry blocks deploy, and promoting this to an error would fail the next redeploy of any already-deployed script that blocks (none of the rig seed scripts do, but customer scripts may). The Template Engine surfaces it as a non-blocking `ScriptCompilation` **Warning** on validation (`ScriptCompiler.FindAdvisories`, memoised under its own cache surface key, a faulting analysis yields no advisory). Surfacing it as an editor marker in the Central UI is a Central UI concern.
---
### REQ-SA-4: Compile-Only Globals Stubs
@@ -259,7 +259,7 @@ Before a deployment is sent to a site, the Template Engine performs comprehensiv
- **Flattening**: The full template hierarchy resolves and flattens without errors.
- **Naming collision detection**: No duplicate attribute, alarm, or script names in the flattened configuration.
- **Script compilation and trust check**: All instance scripts and alarm on-trigger scripts are compiled via `ScriptCompiler.TryCompile`, which performs a real Roslyn type-checking compile (using `ScriptCompileSurface` from the Script Analysis component as the globals type) and an authoritative forbidden-API check (via `ScriptTrustValidator.FindViolations`). Scripts with compilation errors, type errors, or forbidden-API violations are rejected. This gate is authoritative — not advisory — meaning alias, `using static`, and `global::` bypasses are caught by semantic symbol resolution.
- **Script compilation and trust check**: All instance scripts and alarm on-trigger scripts are compiled via `ScriptCompiler.TryCompile`, which performs a real Roslyn type-checking compile (using `ScriptCompileSurface` from the Script Analysis component as the globals type) and an authoritative forbidden-API check (via `ScriptTrustValidator.FindViolations`). Scripts with compilation errors, type errors, or forbidden-API violations are rejected. This gate is authoritative — not advisory — meaning alias, `using static`, and `global::` bypasses are caught by semantic symbol resolution. A script that compiles is additionally run through `BlockingWaitAnalyzer` (via `ScriptCompiler.FindAdvisories`): `.Result` / `.Wait()` / `.GetAwaiter().GetResult()` on a task produces a non-blocking `ScriptCompilation` **Warning** naming the script and line — it never gates deploy (review 2026-10-21 01/SR-U2).
- **Alarm trigger references**: Alarm trigger definitions reference attributes that exist in the flattened configuration.
- **Script trigger references**: Script triggers (value change, conditional) reference attributes that exist in the flattened configuration.
- **Expression-trigger syntax/trust check**: Each Expression-triggered script/alarm has its boolean expression run through `ValidationService.CheckExpressionSyntax` — a `ScriptTrustValidator.FindViolations` forbidden-API verdict followed by a real Roslyn compile against `TriggerCompileSurface`. Both stages report **all** findings (every forbidden API, every compile error), not just the first, so an operator fixing a multi-error expression sees the full list in one round-trip. The verdict is memoised in the surface-keyed `ScriptCompileVerdictCache`, so an unchanged expression compiles once per process.