feat(compiler): SYN020/SYN021 — warn on Date/performance ambient time access that bypasses the capability model (?bs 0.7+)#176
Open
marcelofarias wants to merge 5 commits into
Open
Conversation
… SYN021 (performance.now/timeOrigin time bypass) rebased onto main Ports SYN020 and SYN021 detection from botkowski/syn021-performance-time, inserting them in numeric order after SYN019 and before SYN022/SYN023 which were added to main since the original branch was opened. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds two new compiler syn-check warnings (SYN020/SYN021) to detect ambient time reads via Date and performance APIs that bypass botscript’s uses { time } capability model, and wires the new diagnostics through the compiler + MCP explanation surfaces with tests.
Changes:
- Add SYN020 detection for
Date.now(),new Date()/new Date(no parens), andDate()/Date?.()ambient-time reads in?bs 0.7+. - Add SYN021 detection for
performance.now()calls andperformance.timeOriginreads in?bs 0.7+. - Register both codes in error-code registries/MCP explanations and add dedicated compiler tests + MCP KNOWN_CODES coverage.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/compiler/src/passes/syn-check.ts | Implements SYN020/SYN021 token-pattern detection with unsafe {} / unsafe fn suppression and version gating. |
| packages/compiler/src/error-codes.ts | Registers SYN020/SYN021 (rule/idiom/rewrite/example) in the compiler error-code registry. |
| packages/mcp/src/explanations.ts | Adds long-form MCP explain entries for SYN020 and SYN021, including detected forms and suggested fixes. |
| packages/mcp/tests/server.test.ts | Extends MCP KNOWN_CODES allowlist to include SYN020 and SYN021. |
| packages/compiler/tests/error-codes.test.ts | Updates compiler registry allowlist to include SYN020 and SYN021. |
| packages/compiler/tests/syn020-check.test.ts | New tests covering SYN020 positive/negative cases, version gating, and unsafe suppression. |
| packages/compiler/tests/syn021-check.test.ts | New tests covering SYN021 positive/negative cases, version gating, and unsafe suppression. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+140
to
+144
| * SYN020 A `Date.now()`, `new Date()`, `new Date` (no parens), or `Date()` call was detected | ||
| * in a fn body (?bs 0.7+). These forms inject the current time at runtime but are invisible | ||
| * to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the | ||
| * `Date` global. A fn that calls these forms has an undeclared time dependency — callers | ||
| * cannot see it and tests cannot control the time value observed by the fn. |
…ain list Addresses Copilot review comment on PR #176: AGENTS.md and README.md were missing SYN020 (Date ambient time) and SYN021 (performance timing) entries between SYN019 and SYN022. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+1681
to
+1683
| const hasDateArgs20 = !hasNew20 && firstInsideIdx20 !== callTok20.matchedAt; | ||
| const callForm20 = hasNew20 ? "new Date()" : isOpt20 ? "Date?.()" : hasDateArgs20 ? "Date(...)" : "Date()"; | ||
| const formDesc20 = hasNew20 ? `constructs new Date()` : isOpt20 ? `calls Date?.()` : hasDateArgs20 ? `calls Date(...)` : `calls Date()`; |
Comment on lines
+1751
to
+1755
| line: loc21.line, | ||
| column: loc21.column, | ||
| start: tok.start, | ||
| end: memberTok21.end, | ||
| message: |
| | SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. | | ||
| | SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. | | ||
| | SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection: `Date` ident not preceded by `.`/`?.`, followed by `.` or `?.` then `now`, or preceded by `new` / bare call form, followed by `(` or `?.(` or `)` (no-arg). `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. | | ||
| | SYN021 | (0.7+, warning) A fn body calls `performance.now()`, `performance?.now()`, `performance.now?.()`, or reads `performance.timeOrigin` / `performance?.timeOrigin`. These inject ambient timing information at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `performance` global. Detection: `performance` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `now` (with trailing `(` or `?.(`) or `timeOrigin`. `obj.performance.*` member calls, `fn`/`function` declarations named `performance`, and `unsafe {}` / `unsafe "reason" fn` bodies are suppressed. | Pass the start time as an explicit parameter. If monotonic ordering (not wallclock) is the goal, use `clock.sequence()` (no capability required). If `performance.now` is genuinely needed, wrap in `unsafe "uses performance.now for <reason>" { performance.now() }`. | |
…t, end position - SYN020: when isOpt20 + args present, render Date?.(…) not Date?.() so the diagnostic text accurately reflects the call form callers wrote - SYN021: set end to afterNow21.start + 1 (the opening `(`) to match the convention in SYN004/SYN019 and point precisely at the call site Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| | SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. | | ||
| | SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. | | ||
| | SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection: `Date` ident not preceded by `.`/`?.`, followed by `.` or `?.` then `now`, or preceded by `new` / bare call form, followed by `(` or `?.(` or `)` (no-arg). `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. | | ||
| | SYN021 | (0.7+, warning) A fn body calls `performance.now()`, `performance?.now()`, `performance.now?.()`, or reads `performance.timeOrigin` / `performance?.timeOrigin`. These inject ambient timing information at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `performance` global. Detection: `performance` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `now` (with trailing `(` or `?.(`) or `timeOrigin`. `obj.performance.*` member calls, `fn`/`function` declarations named `performance`, and `unsafe {}` / `unsafe "reason" fn` bodies are suppressed. | Pass the start time as an explicit parameter. If monotonic ordering (not wallclock) is the goal, use `clock.sequence()` (no capability required). If `performance.now` is genuinely needed, wrap in `unsafe "uses performance.now for <reason>" { performance.now() }`. | |
…() forward reference from AGENTS.md Adds a regression test verifying SYN020 formats Date?.(x) as Date?.(...) in the warning message when the optional call has arguments. Removes the clock.sequence() reference from the SYN021 AGENTS.md fix-hint — clock stdlib does not exist in main yet; the guidance was misleading. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| | SYN016 | (0.7+, warning) A fn body accesses `indexedDB.*` — any member access (`.`, `?.`) on the `indexedDB` global. `indexedDB` is same-origin persistent database storage invisible to botscript's capability model (`reads {}` / `writes {}` labels cover declared resource identifiers, not the Web Storage API). Unlike `localStorage`, `indexedDB` is asynchronous and has no practical size limit, making invisible access higher-impact. A fn that accesses `indexedDB` has undeclared persistent state dependencies invisible to callers and audit tooling. Detection: `indexedDB` ident not preceded by `.`/`?.`, followed by `.` or `?.`. Bare references and `fn`/`function` declarations named `indexedDB` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass an `IDBDatabase` handle as an explicit fn parameter so callers control what database is accessed and tests can inject a mock. If direct global access is required, wrap in `unsafe "reads/writes indexedDB for <reason>" { indexedDB.open(name) }`. | | ||
| | SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. | | ||
| | SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. | | ||
| | SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection: `Date` ident not preceded by `.`/`?.`, followed by `.` or `?.` then `now`, or preceded by `new` / bare call form, followed by `(` or `?.(` or `)` (no-arg). `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. | |
…s including no-parens new Date The previous description's "followed by `(` or `?.(` or `)` (no-arg)" phrasing implied all three SYN020 forms require a trailing delimiter, but `new Date` without parentheses has neither — it's detected by the ABSENCE of `(`, `<`, `?.`, or `.` after `Date`. Rewrite the detection description to enumerate the three forms explicitly so contributors don't misread the check when extending it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 tasks
| | SYN016 | (0.7+, warning) A fn body accesses `indexedDB.*` — any member access (`.`, `?.`) on the `indexedDB` global. `indexedDB` is same-origin persistent database storage invisible to botscript's capability model (`reads {}` / `writes {}` labels cover declared resource identifiers, not the Web Storage API). Unlike `localStorage`, `indexedDB` is asynchronous and has no practical size limit, making invisible access higher-impact. A fn that accesses `indexedDB` has undeclared persistent state dependencies invisible to callers and audit tooling. Detection: `indexedDB` ident not preceded by `.`/`?.`, followed by `.` or `?.`. Bare references and `fn`/`function` declarations named `indexedDB` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass an `IDBDatabase` handle as an explicit fn parameter so callers control what database is accessed and tests can inject a mock. If direct global access is required, wrap in `unsafe "reads/writes indexedDB for <reason>" { indexedDB.open(name) }`. | | ||
| | SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. | | ||
| | SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. | | ||
| | SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection has three forms: (1) member call — `Date` ident (not preceded by `.`/`?.`) followed by `.` or `?.` then `now`, then `(` or `?.(` or `?.(`; (2) no-parens construction — `Date` preceded by `new` and NOT followed by `(`, `<`, `?.`, or `.` (JS/TS `new Date` without parens is equivalent to `new Date()`); (3) bare call — `Date` followed by `(` or `?.(` without a preceding `new`. Excluded: `new Date(arg)` with explicit arguments (constructs a specific date, not ambient time), `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date`. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. | |
| | SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. | | ||
| | SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. | | ||
| | SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection has three forms: (1) member call — `Date` ident (not preceded by `.`/`?.`) followed by `.` or `?.` then `now`, then `(` or `?.(` or `?.(`; (2) no-parens construction — `Date` preceded by `new` and NOT followed by `(`, `<`, `?.`, or `.` (JS/TS `new Date` without parens is equivalent to `new Date()`); (3) bare call — `Date` followed by `(` or `?.(` without a preceding `new`. Excluded: `new Date(arg)` with explicit arguments (constructs a specific date, not ambient time), `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date`. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. | | ||
| | SYN021 | (0.7+, warning) A fn body calls `performance.now()`, `performance?.now()`, `performance.now?.()`, or reads `performance.timeOrigin` / `performance?.timeOrigin`. These inject ambient timing information at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `performance` global. Detection: `performance` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `now` (with trailing `(` or `?.(`) or `timeOrigin`. `obj.performance.*` member calls, `fn`/`function` declarations named `performance`, and `unsafe {}` / `unsafe "reason" fn` bodies are suppressed. | Pass the start time as an explicit parameter so callers control the clock value. If `performance.now` is genuinely needed (e.g. for high-resolution elapsed time), wrap in `unsafe "uses performance.now for <reason>" { performance.now() }`. | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Date.now(),Date?.now(),Date.now?.(), constructsnew Date()/new Date<T>()(no-arg, ambient time), or callsDate()/Date?.()in?bs 0.7+.performance.now(),performance?.now(),performance.now?.(), or readsperformance.timeOrigin/performance?.timeOriginin?bs 0.7+.uses { time }coverstime.*stdlib calls, not theDateorperformanceglobals. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed.explainoutput, KNOWN_CODES contract test, AGENTS.md diagnostic table, README.md explain-tool code list.This is a clean rebase of PR #169 onto current main (which added SYN019, SYN022, SYN023 since the original branch was opened). SYN020 and SYN021 are inserted in numeric order after SYN019 and before SYN022/SYN023. All 1499 tests pass.
What botscript form triggers SYN020
?bs 0.7 fn isExpired(expiresAt: number) -> bool { return Date.now() > expiresAt // SYN020 } fn ts() -> string { return new Date() // SYN020 } fn ts2() -> string { return Date?.() // SYN020 }What botscript form triggers SYN021
?bs 0.7 fn elapsed(start: number) -> number { return performance.now() - start // SYN021 }Fix
// preferred — pass nowMs as a parameter; tests can control it fn isExpired(nowMs: number, expiresAt: number) -> bool { return nowMs > expiresAt } // escape hatch when ambient read is necessary at an entry point fn elapsed(start: number) -> number { return unsafe "uses performance.now for timing" { performance.now() } - start }