Skip to content

feat(compiler): SYN025/SYN026 — warn on requestAnimationFrame/requestIdleCallback (scheduling bypass, like SYN010) #179

Description

@marcelofarias

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    proposalRFC or design proposal

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions