Problem
SYN010 warns on `setTimeout`, `setInterval`, and `queueMicrotask` because they schedule callbacks to run after the current fn returns — those callbacks have invisible capability surfaces. The same problem applies to two other standard scheduling globals:
- `requestAnimationFrame(cb)` — schedules `cb` to run before the next repaint; fires outside the caller fn with no capability declaration
- `requestIdleCallback(cb)` — schedules `cb` to run during idle periods; same issue
A botscript fn that calls either global has an undeclared scheduling side-effect. The callback can make network requests, access storage, write to the console — none of it visible in the outer fn's `uses {}`, `reads {}`, or `writes {}` declarations.
Why they are different from setTimeout (and why they matter)
Unlike `setTimeout`, these globals are browser-only (not Node.js), so they are typically found in agent UIs and web-facing bots rather than CLI workers. But the capability bypass is identical:
```bs
?bs 0.7
fn scheduleRender(frame: number) -> void {
// SYN025: requestAnimationFrame schedules a callback outside this fn.
// The callback's side effects are invisible to the capability model.
requestAnimationFrame(() => http.get("/render/" + frame))
}
```
The declared capability surface for `scheduleRender` is empty. The callback calls `http.get`. CAP001 does not fire because the call is not inside a fn body with a `uses { net }` declaration — it's in a callback registered with a global.
Proposed diagnostics
| Code |
Global |
Trigger |
| SYN025 |
`requestAnimationFrame(cb)` |
Any call (not preceded by `./\?.`) at `?bs 0.7+` |
| SYN026 |
`requestIdleCallback(cb)` |
Any call (not preceded by `./\?.`) at `?bs 0.7+` |
Both follow the SYN010 pattern exactly:
- Excluded: member calls (`obj.requestAnimationFrame`), `fn`/`function`/`function*` declarations with those names, object method shorthands
- Suppressed inside `unsafe {}` and `unsafe "reason" fn` bodies
Fix
```bs
// pass the frame number as a parameter; let the caller decide when to schedule
fn render(frame: number) -> void {
http.get("/render/" + frame)
}
// caller is responsible for scheduling:
requestAnimationFrame(() => render(frameNum))
```
Or wrap in unsafe when scheduling is genuinely required at the entry point:
```bs
fn scheduleRender(frame: number) -> void {
unsafe "schedules render callback for animation" {
requestAnimationFrame(() => http.get("/render/" + frame))
}
}
```
Implementation notes
These extend SYN010 naturally. The detection logic is simpler than SYN010 (no generic forms, no ternary guard needed for the callback argument). They can ship in the same PR or separately by number.
Prior art in this repo
SYN010: setTimeout/setInterval/queueMicrotask (#147)
Problem
SYN010 warns on `setTimeout`, `setInterval`, and `queueMicrotask` because they schedule callbacks to run after the current fn returns — those callbacks have invisible capability surfaces. The same problem applies to two other standard scheduling globals:
A botscript fn that calls either global has an undeclared scheduling side-effect. The callback can make network requests, access storage, write to the console — none of it visible in the outer fn's `uses {}`, `reads {}`, or `writes {}` declarations.
Why they are different from setTimeout (and why they matter)
Unlike `setTimeout`, these globals are browser-only (not Node.js), so they are typically found in agent UIs and web-facing bots rather than CLI workers. But the capability bypass is identical:
```bs
?bs 0.7
fn scheduleRender(frame: number) -> void {
// SYN025: requestAnimationFrame schedules a callback outside this fn.
// The callback's side effects are invisible to the capability model.
requestAnimationFrame(() => http.get("/render/" + frame))
}
```
The declared capability surface for `scheduleRender` is empty. The callback calls `http.get`. CAP001 does not fire because the call is not inside a fn body with a `uses { net }` declaration — it's in a callback registered with a global.
Proposed diagnostics
/\?.`) at `?bs 0.7+`/\?.`) at `?bs 0.7+`Both follow the SYN010 pattern exactly:
Fix
```bs
// pass the frame number as a parameter; let the caller decide when to schedule
fn render(frame: number) -> void {
http.get("/render/" + frame)
}
// caller is responsible for scheduling:
requestAnimationFrame(() => render(frameNum))
```
Or wrap in unsafe when scheduling is genuinely required at the entry point:
```bs
fn scheduleRender(frame: number) -> void {
unsafe "schedules render callback for animation" {
requestAnimationFrame(() => http.get("/render/" + frame))
}
}
```
Implementation notes
These extend SYN010 naturally. The detection logic is simpler than SYN010 (no generic forms, no ternary guard needed for the callback argument). They can ship in the same PR or separately by number.
Prior art in this repo
SYN010: setTimeout/setInterval/queueMicrotask (#147)