Skip to content

feat(compiler): SYN007 — warn on fetch() calls that bypass the net capability model (?bs 0.7+)#148

Open
marcelofarias wants to merge 11 commits into
mainfrom
botkowski/syn007-fetch-bypass
Open

feat(compiler): SYN007 — warn on fetch() calls that bypass the net capability model (?bs 0.7+)#148
marcelofarias wants to merge 11 commits into
mainfrom
botkowski/syn007-fetch-bypass

Conversation

@marcelofarias

Copy link
Copy Markdown
Owner

Summary

  • Adds SYN007: a new warning that fires when a fn body calls fetch(...), fetch?.(...), or TypeScript instantiation form fetch<T>(...) at ?bs 0.7+
  • fetch() is a real network call that CAP001 cannot see — it only checks http.* member calls. A fn that uses fetch has an undeclared net dependency: no uses { net } in the header, no signal to callers, no capability manifest entry
  • Same bypass class as console.* (SYN003): real effects, invisible to the declared capability surface
  • Suppressed inside unsafe "reason" { } blocks and unsafe "reason" fn bodies
  • Idiomatic fix: replace with http.get() / http.post() and declare uses { net }

Changes

File Change
packages/compiler/src/error-codes.ts Add SYN007 entry (rule, idiom, rewrite, example)
packages/compiler/src/passes/syn-check.ts Detection: fetch not preceded by ./?., followed by (, ?.(, or <T>(; unsafe suppression
packages/compiler/tests/syn007-check.test.ts 10 tests: fires/not-fires cases, severity, unsafe suppression, method-call exclusion, optional-call and generic forms
packages/compiler/tests/error-codes.test.ts Add SYN007 to exhaustive allowlist
packages/mcp/src/explanations.ts Add SYN007 long-form explanation with fails/passes examples
packages/mcp/tests/server.test.ts Add SYN007 to KNOWN_CODES list
AGENTS.md Add SYN007 row to diagnostic table

Test plan

  • pnpm -r build passes
  • npx vitest run — 10/10 SYN007 tests pass, 1159 total tests pass
  • SYN007 fires on fetch(url), fetch(url, init), fetch?.(url)
  • SYN007 fires on TypeScript instantiation form fetch<Response>(url)
  • SYN007 does NOT fire below ?bs 0.7
  • SYN007 does NOT fire inside unsafe {} or unsafe fn
  • SYN007 does NOT fire on obj.fetch(...) (member call on a local)
  • SYN007 does NOT fire on bare fetch reference (not called)
  • MCP explain tool returns long-form for SYN007

🤖 Generated with Claude Code

…pability model (?bs 0.7+)

fetch() makes real HTTP requests but is invisible to CAP001, which only
checks http.* member calls. A fn that calls fetch() has an undeclared net
dependency — uses { net } is absent from its header and callers cannot see
the network access. This is the same bypass class as console.* (SYN003).

Detects: fetch(...), fetch?.(...), and TypeScript instantiation forms
fetch<T>(...). Suppressed inside unsafe { } blocks and unsafe fn bodies.
Excludes obj.fetch(...) method calls. Fires at ?bs 0.7+.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new compiler warning (SYN007) to detect fetch(...) calls inside function bodies (from ?bs 0.7+) because fetch performs real network I/O while bypassing the http.*-based net capability surface enforced by CAP001, and wires the new code through the compiler’s registries, MCP explanations, and docs/tests.

Changes:

  • Register SYN007 (rule/idiom/rewrite/example) and surface it through MCP explain and diagnostic docs.
  • Implement token-based detection of fetch(...), fetch?.(...), and fetch<T>(...) with suppression inside unsafe {} / unsafe fn bodies.
  • Add compiler + MCP tests to keep the error-code/explanation registries exhaustive and validate SYN007 behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/compiler/src/passes/syn-check.ts Adds SYN007 token-scan to warn on fetch calls, with unsafe suppression.
packages/compiler/src/error-codes.ts Registers SYN007 metadata (rule/idiom/rewrite/example).
packages/compiler/tests/syn007-check.test.ts Adds unit tests covering SYN007 firing/non-firing cases.
packages/compiler/tests/error-codes.test.ts Extends exhaustive compiler diagnostic allowlist to include SYN007.
packages/mcp/src/explanations.ts Adds long-form MCP explain content for SYN007.
packages/mcp/tests/server.test.ts Adds SYN007 to the expected known-code list used by MCP tests.
AGENTS.md Documents SYN007 in the diagnostic code table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/compiler/src/passes/syn-check.ts Outdated
Comment thread packages/compiler/tests/syn007-check.test.ts
Comment thread packages/compiler/src/error-codes.ts Outdated
Comment thread packages/mcp/src/explanations.ts Outdated
…and Promise return type

- `anglDepth -= at.text.length - 1` was off by one for `>>` and `>>>`
  tokens; `fetch<Promise<Response>>(url)` was never detected. Aligns with
  the SYN002 pattern that uses `depth -= t.text.length`.
- Add nested-generic test to prevent regressions.
- `http.post` takes an optional `RequestInit` bag, not a raw body string;
  fix idiom in error-codes.ts and MCP explanation to use `{ body }`.
- MCP explanation stated `Result<Response, Error>` as the return type;
  the actual return is `Promise<Result<Response, Error>>` (runtime/effects.ts).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Comment thread packages/compiler/src/error-codes.ts Outdated
Comment thread packages/compiler/src/error-codes.ts Outdated
Comment thread packages/mcp/src/explanations.ts Outdated
Comment thread packages/mcp/src/explanations.ts Outdated
Comment thread AGENTS.md Outdated
Comment thread packages/compiler/src/passes/syn-check.ts
…gnature, >> consistency

- Add SYN006 to syn-check.ts header comment (doc was listing SYN002/SYN003/SYN005/SYN007, skipping SYN006)
- error-codes.ts SYN007 rewrite/example: fn signatures using await must be async fn with Promise<> return
- explanations.ts SYN007 fails/passes examples: same async fn + Promise<> fix
- AGENTS.md SYN007 row: http.post(url, body) → http.post(url, { body }) to match RequestInit signature
- syn-check.ts generic depth scan: unify >> / >>> handling to match SYN002 pattern (single clause, depth -= t.text.length)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread packages/compiler/src/passes/syn-check.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread packages/compiler/src/passes/syn-check.ts
Comment thread packages/compiler/tests/syn007-check.test.ts
…l signatures from SYN007

Add the same guard SYN002 uses: if fetch(...) is followed by { / => / : after the closing
paren it is a method definition, not a runtime call — skip to avoid false positives.
Add two regression tests covering the object-method-shorthand and type-literal cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread packages/compiler/src/passes/syn-check.ts
Comment thread packages/compiler/src/passes/syn-check.ts Outdated
Comment thread packages/compiler/tests/syn007-check.test.ts
…ll forms

Previously, the object/type-literal method exclusion only ran when the token
immediately after `fetch` was `(` (direct call). Generic (`fetch<T>(...)`) and
optional (`fetch?.(...)`) forms bypassed the exclusion, causing false positives
on method signatures like `{ fetch<T>(url): T }` inside fn bodies.

Refactor to normalize `parenIdx7` to the actual `(` token index for all three
call forms before running the exclusion check. Add two regression tests covering
the generic method-signature and return-type-annotated method-shorthand cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread packages/compiler/tests/syn007-check.test.ts Outdated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@marcelofarias marcelofarias requested a review from Copilot June 11, 2026 22:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

marcelofarias and others added 4 commits June 11, 2026 23:30
…r-declared (?bs 0.9+)

* feat(compiler): DEP003/DEP004 — warn when reads/writes labels are over-declared (?bs 0.9+)

Squash-rebased onto current main. DEP003 fires when a fn declares
reads { x } but no same-file callee (transitively) reads { x } —
the annotation likely became stale after a refactor. DEP004 is the
symmetric check for writes { x }.

Both are warning-level (non-blocking). Leaf fns are excluded.
Adds DEP003/DEP004 to compiler registry, MCP explain, and tests.

Supersedes PR #100 (same content, rebased). All 996 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dep003): hasOpaqueCall now follows full member chain — detects obj.a.b() and obj.a?.b()

Previous logic only checked one segment deep: obj.method() was detected but
obj.a.b() was not, because afterMethod was `.` rather than `(`. Walk the full
chain until a call or non-chain token is found.

Adds two regression tests for the multi-segment cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dep003): address Copilot review — arrow parens and conditional wording

- Add parens around single-param arrow functions in dep-check.ts (style consistency)
- Clarify DEP001/DEP002 cross-refs: "may be warned about" with scope conditions, not unconditional

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dep003): add DEP003/DEP004 to exhaustive error-codes test allowlist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(compiler): derive STDLIB_NAMESPACES from STDLIB_TO_CAP to eliminate drift risk

STDLIB_NAMESPACES was a hardcoded duplicate of the key set of STDLIB_TO_CAP
(_stdlib.ts). Adding or removing a stdlib namespace required updating two
places and hasOpaqueCall behavior could silently diverge. Now derived from
the single source of truth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(compiler): collectTopLevelParamNames handles untyped/default params, collectFnBodyLocalNames handles var

collectTopLevelParamNames:
- Adds skipUntilNextParam flag so type annotations and default values
  after the param name are not re-scanned as candidate param names.
  Fixes false captures like treating `string` in `(name: string)` as a
  param name when a untyped-param lookahead would have matched it.
- Adds fallback for completely untyped and default params: bare identifier
  immediately before `,`, `)`, or `=` is collected as a param name.
  Fixes the original Copilot gap: `fn f(x) -> void` left `x` unknown,
  causing `x.trim()` to be treated as an opaque external call.
- Closing `}` at depth 1 sets skipUntilNextParam so the type annotation
  after a destructured binding (`{a, b}: Context`) is not captured.

collectFnBodyLocalNames:
- Adds `var` alongside `const`/`let` so that `var x = ...` bindings are
  recognised as local names and `x.method()` is not mistaken for an
  opaque namespace call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(dep-check): add coverage for untyped param and var local opaque suppression

Two regression tests to cover the fixes in collectTopLevelParamNames and
collectFnBodyLocalNames:
- Untyped param `fn f(name)`: name.trim() must not suppress DEP003
- Var binding `var x = ...`: x.method() must not suppress DEP003

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(compiler): DEP003/DEP004 rule/idiom text and docs reflect moduleEffects and opaque-call suppression

Update DEP003/DEP004 entries in error-codes.ts (rule, idiom), MCP
explanations, and AGENTS.md to accurately describe the two conditions
under which the warning is suppressed: fns with opaque/untracked external
calls, and tracked callees that include moduleEffects entries (not just
same-file fns).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dep003): correct doc comment — collectFnBodyLocalNames also collects var

The implementation collects `const`, `let`, and `var` bindings, but the
doc comment only mentioned `const`/`let`. Updated to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(callgraph): collect destructured binding names in params and local vars

collectTopLevelParamNames: when a parameter is a destructuring pattern
({ a, b: c } or [x]), extract the binding names (those NOT followed by
`:`) so method calls on destructured locals (e.g. name.trim()) are not
mistaken for opaque external calls that suppress DEP003/DEP004/THR004.

collectFnBodyLocalNames: add collectDestructuredTokenBindings helper that
uses the token matchedAt index to scan { } and [ ] destructuring patterns
recursively, collecting binding names by the same rule (ident not followed
by `:` is a binding, not a property key).

Adds two regression tests: one for destructured params and one for
destructured const locals — both confirm DEP003 still fires when the
only member calls in the body are on locally-scoped destructured names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(callgraph): skip bracket groups in type annotations; fix destructuring depth

collectTopLevelParamNames: when skipping past a param's type annotation
(skipUntilNextParam=true), skip entire {}/[] groups rather than char-by-char
so commas inside `x: { a: string, b: number }` are not mistaken for
param separators.

collectDestructuringStringBindings: change depth guard from `depth !== 1`
to `depth < 1` so nested destructuring patterns like `{ a: { b } }` are
handled — binding names at depth > 1 are now correctly collected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dep003): skip default-value expressions in destructuring binding collectors

Identifiers in `= <expr>` default values (e.g. `{ a = externalDb }`) were
incorrectly collected as binding names, causing hasOpaqueCall() to treat
member calls on those names as local and suppress DEP003/DEP004/THR004
when it shouldn't.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dep003): clarify DEP003/DEP004 titles — tracked callee, not same-file only

Copilot pointed out the titles said "in the same file" but the
implementation also counts moduleEffects entries as tracked callees.
Updated titles in error-codes.ts and explanations.ts to say
"any tracked callee" instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dep003): exclude module-level stdlib aliases from opaque-call detection

hasOpaqueCall suppresses DEP003/DEP004 when a fn contains a member call whose
receiver is not in STDLIB_NAMESPACES or effectiveLocalNames. Module-level stdlib
aliases like `const t = time` were not included in either set, causing `t.now()`
to be treated as an opaque external call and incorrectly suppressing the warning.

Fix: collect module-level stdlib alias names via collectStdlibAliases() once per
pass invocation and add them to localNames at each hasOpaqueCall call site.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(callgraph): move orphaned JSDoc to collectTopLevelParamNames

The JSDoc block for collectTopLevelParamNames was placed before
collectDestructuringStringBindings, leaving both functions without
proper documentation. Moved the doc block to attach to the correct symbol.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(thr-check): pass stdlib aliases to hasOpaqueCall for THR004 suppression

hasOpaqueCall in passThrCheck was called without localNames, so module-level
stdlib aliases (e.g. `const t = time`) were treated as opaque external
member calls and incorrectly suppressed THR004 warnings. passDepCheck already
passed stdlibAliasNames correctly; align passThrCheck to match.

- Import collectStdlibAliases from _alias.js and collectFnBodyLocalNames
  from _callgraph.js in thr-check.ts
- Compute stdlibAliasNames once at passThrCheck entry
- Build localNames = paramNames + bodyLocals + stdlibAliasNames for
  the hasOpaqueCall call (mirrors the pattern in passDepCheck)
- Add regression test: fn with t.now() (stdlib alias) still fires THR004

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(callgraph): track angle-bracket depth in collectTopLevelParamNames

Commas inside generic type argument lists like `Map<T, { a }>` were
incorrectly resetting skipUntilNextParam, causing the `{ a }` fragment to
be parsed as a destructuring parameter and adding `a` to the param-names
set. This made `a.method()` in the body appear local rather than opaque,
falsely suppressing DEP003/DEP004 when they should fire (and vice versa
— allowing opaque-call suppression to be defeated).

Fix: track `<`/`>` angle depth in skipUntilNextParam mode. Commas are
only treated as top-level param separators when angleDepth === 0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(callgraph): remove unused chainWalk: label in hasOpaqueCall

The label had no corresponding `break chainWalk` or `continue chainWalk`
usage; all break/continue statements in the loop are bare. Removing it
eliminates the noise and avoids potential linter warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(thr-check): filter import aliases to tracked moduleEffects targets for opaque-call check

THR004 opaque-call suppression used allCalleeNames, which includes ALL import
aliases. An alias to an untracked external (not in moduleEffects) is itself an
opaque call — including it in knownForOpaque caused THR004 to fire for fns with
genuinely unknown external dependencies (false warnings).

Fix: derive opaqueKnownBase from fnNames + knownExternalNames + only the
aliases whose resolved target exists in moduleEffects. allCalleeNames is
preserved as-is for collectCallees (call graph building).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dep003/dep004): correct rule/idiom text — call-graph heuristic, not body scanner

The previous wording implied the compiler scans fn bodies for direct resource
access, which it does not. Reword rule and idiom to accurately describe the
implementation: a call-graph justification check that fires only when all
same-file callees are resolvable and none propagates the declared label.

---------

Co-authored-by: Marcelo Farias <mfarias@Marcelos-MacBook-Pro-4.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…n bodies (?bs 0.7+)

* feat(compiler): SYN004 — warn on eval() and new Function() calls in fn bodies (?bs 0.7+)

eval() and new Function() execute strings as code at runtime, bypassing
every static check botscript provides: CAP001/CAP002 cannot see capability
calls hidden in the string, reads/writes labels are unenforceable, and
SYN002/SYN003 checks can be routed around entirely. This is the universal
bypass — a fn could use eval('process.env.KEY') with no SYN005, or
eval('http.get(...)') with no CAP001.

Detection (token-based, same suppression model as SYN002/SYN003):
  - eval(...) — 'eval' ident not preceded by ./ ?., followed by (
  - new Function(...) — 'new' followed by 'Function' followed by (
  - .eval() (method call on a local) and Function.* member accesses: excluded
  - Suppressed inside unsafe {} blocks and unsafe fn bodies

Closes #144 (filed as SYN006; implemented as SYN004 — next available code
on main, SYN004/SYN005 are still in open PRs).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): detect bare Function() calls without new; fix SYN005 xref

`Function(body)` without `new` is equivalent to `new Function(body)` at
runtime and was an easy bypass of SYN004's intent. Now both forms fire.

Also fixes Copilot's comment about the explanations.ts cross-reference to
SYN005 — reworded to not depend on SYN005 existing on main yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): detect Function?.() optional call; align docs/tests with bare Function() detection

- Add `Function?.(` optional call detection in syn-check.ts (mirrors eval?.(  handling)
- Update error-codes.ts, MCP explanations, AGENTS.md, and test header/describe label to
  mention bare `Function(...)` / `Function?.(...)` alongside `new Function(...)` — the
  previous commit added bare Function() detection but left docs still saying only new Function()
- Fix "behaviour" → "behavior" in MCP explanation (American English consistency)
- Add test for Function?.() optional call form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): exclude declaration contexts from eval/Function detection; fix message text

- eval and Function detection now skip when the paren group is followed by
  `{`, `:`, or `=>` — catches `function eval(x) {}` and method shorthands
  that would otherwise false-positive
- Warning messages use concrete unsafe block examples (`{ eval(src) }`,
  `{ new Function(body) }`) instead of the confusing empty `{ }`
- error-codes.ts title: "call bypasses" → "calls bypass" (plural)
- explanations.ts: clarify Function constructor bullet — not SYN003/Result
  contract specifically, but all static checks
- Two new not-fires tests covering the declaration exclusion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(compiler): SYN004 Function warning suggests correct unsafe-block syntax

When the flagged call is bare Function(...) (no `new`), the suppression
example in the warning message now matches the detected form.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): detect TypeScript instantiation forms eval<T>() and Function<T>()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): remove colon from declaration-exclusion check; add ternary regression tests

The ): exclusion was causing false negatives: cond ? eval(x) : y was
not firing SYN004 because the ternary colon after eval(x) matched the
`:` (return-type annotation) exclusion. The `{` and `=>` exclusions
correctly handle all real declaration contexts (method shorthands and
function declarations). Removes the `:` case from both eval and Function
detection and adds two regression tests for ternary branches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): handle return-type-annotated declarations; guard Function ternary then-branch

- Add `:` to declaration-exclusion check in both eval and Function blocks,
  so `function eval(x): T {}` and `{ eval(x): T {} }` are not flagged.
- Guard the `:` check with `isTernaryConsequent` in both blocks to avoid
  suppressing `? eval(x) : y` and `? Function(x) : y` (both are real calls).
- Fix grammar in explanations.ts: "bypasses" → "bypass" (plural subject).
- Add 4 regression tests: return-type-annotated eval/Function declarations
  must not fire; Function() in ternary then-branch must fire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): add eval?.() test; clarify SYN005 AGENTS.md description

- Add missing eval?.(…) optional-call test case (SYN004 fires on this
  form but it was untested — Function?.() had a test, eval?.() did not)
- Clarify SYN005 AGENTS.md row: rephrase from the ambiguous "no
  declaration depends on deployment config" to the accurate "no
  capability covers it; fn has an undeclared dependency on deployment
  configuration"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): idiom shows content in unsafe block; MCP explanation covers instantiation forms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn004): fire on new Function() in ternary then-branch — check token before new

When `? new Function(body) : other`, prev4 is `new` (not `?`), so the old
isTernaryConsequent4 check evaluated to false and the `:` guard skipped the
warning as if it were a return-type annotation. Now also check the token before
`new` for `?`. Add regression test.

---------

Co-authored-by: Marcelo Farias <mfarias@Marcelos-MacBook-Pro-4.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…k calls in fn bodies (?bs 0.7+)

* feat(compiler): SYN010 — warn on setTimeout/setInterval/queueMicrotask calls in fn bodies (?bs 0.7+)

- Adds **SYN010**: a new warning that fires when a fn body calls `setTimeout(...)`,
  `setInterval(...)`, or `queueMicrotask(...)` at `?bs 0.7+`
- These globals schedule callbacks that run after the fn returns — any effects inside
  those callbacks are invisible to callers: no capability declaration, no `writes {}`
  label, and no `throws {}` entry can cover them
- Same bypass class as `fetch` (SYN007) and `WebSocket` (SYN008): real side effects
  that sidestep the declared capability surface, but deferred rather than immediate
- Suppressed inside `unsafe "reason" { }` blocks and `unsafe "reason" fn` bodies
- Member calls (`obj.setTimeout(...)`) and bare references (without `(`) are excluded
- Note: should be merged after PR #150 (SYN009) lands; SYN010 is the next available code

Changes:
- error-codes.ts: add SYN010 entry with rule/idiom/rewrite/example
- syn-check.ts: add SYN010 detection; add SYN006 + SYN010 to module header comment;
  fix outdated unsafe-fn skip comment
- tests/syn010-check.test.ts: 12 tests covering all three globals, optional-call form,
  severity, version gating, unsafe suppression, member-call exclusion, bare reference
- tests/error-codes.test.ts: add SYN010 to exhaustive allowlist
- mcp/src/explanations.ts: add SYN010 long-form explanation with fails/passes examples
- mcp/tests/server.test.ts: add SYN010 to KNOWN_CODES list
- AGENTS.md: add SYN010 row to diagnostic table
- README.md: add SYN010 to explain tool code list

* fix(syn010): address Copilot review — hoist TIMER_GLOBALS, fix param name and reason string

- Hoist TIMER_GLOBALS to module scope (was re-created per fn, wasteful in hot pass)
- Rename `fn` parameter to `callback` in unsafe-fn test (fn is a reserved keyword)
- Fix unsafe reason string: "suspends execution" → "schedules deferred effect" (consistent with idiom)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(syn010): address remaining Copilot feedback — false positive on declarations and explanation codes

- syn-check: exclude function declarations named setTimeout/setInterval/queueMicrotask
  (prev significant token is `function`) — these are definitions, not calls
- syn-check: exclude object/class method shorthands where after closing `)` is `{` or `:`
- tests: add two tests covering function-declaration and object-method-shorthand exclusions
- explanations: remove SYN007/SYN008 code references in SYN010 body; reference by name instead
  to avoid dangling code cross-references until those PRs land

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(compiler): address SYN010 Copilot review feedback — exclude fn keyword declarations

- syn-check.ts: also skip `fn setTimeout(...) -> void {}` botscript-style nested
  function declarations (prev token is `keyword`/`fn`), not just JS `function`
  declarations — prevents false positives on nested fn declarations inside fn bodies
- syn010-check.test.ts: add test asserting SYN010 does NOT fire on
  `fn setTimeout(delay: number) -> void { }` inside a fn body

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Marcelo Farias <mfarias@Marcelos-MacBook-Pro-4.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…pability model (?bs 0.7+)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comment thread README.md
| `primer` | (no args) | The canonical language primer (same text the `?primer` directive emits). |
| `transform` | `{ source: string, filename?: string }` | `{ ok: true, code, forms, version, warnings: [...] }` on success, or `{ ok: false, diagnostics: [...] }` on failure. `warnings` is an array of non-blocking diagnostics (e.g. CAP003). |
| `explain` | `{ code: string }` | Long-form explanation for any stable diagnostic code (`ALI001`, `ALI002`, `ALI003`, `BS001`, `BS002`, `CAP001`–`CAP003`, `DEP001`, `DEP002`, `EFF002`–`EFF004`, `FMT001`, `INT001`–`INT005`, `MAT001`–`MAT004`, `RES001`, `RES002`, `SYN001`, `SYN002`, `SYN003`, `SYN005`, `SYN006`, `THR001`–`THR004`, `UNS001`–`UNS005`, `VER001`–`VER003`) plus a fails/passes example pair. |
| `explain` | `{ code: string }` | Long-form explanation for any stable diagnostic code (`ALI001`, `ALI002`, `ALI003`, `BS001`, `BS002`, `CAP001`–`CAP003`, `DEP001`–`DEP004`, `EFF002`–`EFF004`, `FMT001`, `INT001`–`INT005`, `MAT001`–`MAT004`, `RES001`, `RES002`, `SYN001`, `SYN002`, `SYN003`, `SYN004`, `SYN005`, `SYN006`, `SYN010`, `THR001`–`THR004`, `UNS001`–`UNS005`, `VER001`–`VER003`) plus a fails/passes example pair. |
Comment on lines +296 to +300
// SYN004: eval() and Function() / new Function() call detection.
// Fires on:
// eval(...) — global eval not preceded by `.`/`?.`, followed by `(`
// Function(…) — bare Function call not preceded by `.`/`?.`, followed by `(`
// new Function(…) — same as bare Function call, `new` prefix only affects message
Comment on lines +296 to +302
// SYN004: eval() and Function() / new Function() call detection.
// Fires on:
// eval(...) — global eval not preceded by `.`/`?.`, followed by `(`
// Function(…) — bare Function call not preceded by `.`/`?.`, followed by `(`
// new Function(…) — same as bare Function call, `new` prefix only affects message
// Suppressed inside `unsafe { }` blocks and `unsafe fn` bodies.
// `.eval(...)` (method call on a local) and `Function.*` member accesses are NOT flagged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants