Skip to content

feat(compiler): SYN017 — warn on Notification() construction that bypasses the capability model (?bs 0.7+)#164

Merged
marcelofarias merged 18 commits into
mainfrom
botkowski/syn017-notification-bypass
Jun 18, 2026
Merged

feat(compiler): SYN017 — warn on Notification() construction that bypasses the capability model (?bs 0.7+)#164
marcelofarias merged 18 commits into
mainfrom
botkowski/syn017-notification-bypass

Conversation

@marcelofarias

Copy link
Copy Markdown
Owner

Summary

Adds SYN017: fires a non-blocking warning whenever a fn body constructs new Notification(title), calls bare Notification(title), uses the optional-call form Notification?.(title), or the generic-instantiation form new Notification<T>(title) — all of which create user-visible browser notifications invisible to botscript's capability model.

  • Fires at ?bs 0.7+; silent below that
  • Suppressed inside unsafe "…" { } blocks and unsafe "…" fn bodies
  • Excluded: member calls (obj.Notification(...)), bare references, fn/function declarations named Notification, object method shorthands named Notification, TS type-literal method signatures (with or without optional params, using the ?: fix consistent with SYN012–SYN016)
  • Message distinguishes new Notification() / Notification() / Notification?.()
  • Severity: warning (transform never throws)

Changes

  • packages/compiler/src/error-codes.ts — SYN017 entry
  • packages/compiler/src/passes/syn-check.ts — detection in single dispatch loop
  • packages/compiler/tests/syn017-check.test.ts — 18 tests (new file)
  • packages/compiler/tests/error-codes.test.ts — SYN017 added to allowlist
  • packages/mcp/src/explanations.ts — SYN017 explanation with fails/passes
  • packages/mcp/tests/server.test.ts — SYN017 in KNOWN_CODES
  • AGENTS.md + README.md — docs updated

Test plan

  • npx vitest run syn017 — 18 tests pass
  • npx vitest run server — MCP known-codes list includes SYN017
  • npx vitest run error-codes — allowlist clean
  • All 1302 tests pass (pnpm -r build && npx vitest run)

🤖 Generated with Claude Code

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 syntax warning code SYN017 to flag direct Notification usage inside function bodies in ?bs 0.7+, because browser notifications are user-visible side effects that are not represented in botscript’s capability model. This also propagates through the error-code registry, MCP explain output, tests, and docs so tools can surface consistent guidance.

Changes:

  • Register SYN017 in the compiler error-code registry and MCP explanations, including example fails/passes guidance.
  • Implement SYN017 detection in the compiler syn-check pass with suppression inside unsafe blocks / unsafe "…" fn bodies and exclusions matching existing SYN check patterns.
  • Add a dedicated syn017-check test suite and update existing allowlists/known-code lists and docs to include SYN017.

Reviewed changes

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

Show a summary per file
File Description
README.md Adds SYN017 to the set of stable diagnostic codes supported by the MCP explain tool.
packages/mcp/tests/server.test.ts Updates MCP test expectations to include SYN017 as a known explanation code.
packages/mcp/src/explanations.ts Adds the long-form MCP explanation entry for SYN017 with fails/passes examples.
packages/compiler/tests/syn017-check.test.ts New test file covering SYN017 detection, exclusions, suppression, and message/severity.
packages/compiler/tests/error-codes.test.ts Extends the exhaustive allowlist to include SYN017.
packages/compiler/src/passes/syn-check.ts Implements the SYN017 token-scan detection and adds inline docs for the new check.
packages/compiler/src/error-codes.ts Registers the SYN017 error-code entry (rule/idiom/rewrite/example) for bs explain.
AGENTS.md Documents SYN017 in the agent-facing diagnostics table.

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

Comment on lines +60 to +61
* SYN017 A `new Notification(title)`, `Notification(title)`, or TypeScript instantiation
* form `new Notification<T>(title)` was detected in a fn body (?bs 0.7+).
Comment on lines +783 to +787
rule:
"`new Notification(title)` and bare `Notification(title)` calls create user-visible browser " +
"notifications at runtime — a side effect entirely invisible to botscript's capability model. " +
"No `uses {}`, `reads {}`, or `writes {}` declaration covers notification dispatch: callers " +
"cannot observe, audit, or suppress the UI effect from the fn's declared surface.",

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 8 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +777 to +785
const hasNew17 = prev17 && prev17.kind === "ident" && prev17.text === "new";
// Ternary guard: `cond ? new Notification(title) : other`
const prevBeforeNew17 = hasNew17
? tokens[prevSignificant(tokens, prevIdx17 - 1)]
: undefined;
const isTernaryConsequent17 =
(prev17 !== undefined && prev17 !== null && prev17.kind === "question") ||
(prevBeforeNew17 !== undefined && prevBeforeNew17 !== null && prevBeforeNew17.kind === "question");

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 8 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +834 to +838
// Exclude: function/fn declarations named Notification
if (prev17 && prev17.kind === "ident" && prev17.text === "function") continue;
if (prev17 && prev17.kind === "keyword" && prev17.text === "fn") continue;

const hasNew17 = prev17 && prev17.kind === "ident" && prev17.text === "new";

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 8 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +837 to +842
// Generator: `function* Notification` — prev token is `*`, token before that is `function`
if (prev17 && prev17.kind === "punct" && prev17.text === "*") {
const prevPrevIdx17 = prevSignificant(tokens, prevIdx17 - 1);
const prevPrev17 = tokens[prevPrevIdx17];
if (prevPrev17 && prevPrev17.kind === "ident" && prevPrev17.text === "function") continue;
}

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 8 out of 8 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 8 out of 8 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 8 out of 8 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 8 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +1035 to +1046
// Exclude method shorthands and TS method signatures.
if (callTok17.matchedAt !== undefined) {
const afterCloseIdx17 = nextSignificant(tokens, callTok17.matchedAt + 1);
const afterClose17 = tokens[afterCloseIdx17];
if (afterClose17 && (
(afterClose17.kind === "open" && afterClose17.text === "{") ||
afterClose17.kind === "fatArrow" ||
(!isTernaryConsequent17 && afterClose17.kind === "punct" && afterClose17.text === ":")
)) continue;
// Exclude TS method signatures with omitted return type or optional params:
// `{ Notification(title: string) }` / `{ Notification(title?: string) }`.
let hasTypeAnnotation17 = false;

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 8 out of 8 changed files in this pull request and generated no new comments.

@marcelofarias marcelofarias requested a review from Copilot June 15, 2026 22:46
Marcelo Farias and others added 3 commits June 17, 2026 15:38
…, options)

Copilot flagged the message suggesting `Notification?.(title, options)` as
misleading — the original call may not have an options argument, and the
diagnostic fires on Notification(title) too. Use `(...)` as a neutral
placeholder, consistent with other SYN warnings.

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

Two fixes:
1. Add test for type T = { Notification() } — empty parens in a type literal
   context was already excluded by the code at 1397-1424 (the prevOpen === '='
   guard), but had no explicit test coverage. Copilot flagged the gap.
2. Fix the MCP explanations fails example: { body } shorthand inside the
   Notification options object was hitting a compiler assertion. Simplified
   to new Notification(title) which compiles cleanly and still demonstrates
   the diagnostic.

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

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.

⚠️ Not ready to approve

The new SYN017 TS type-literal exclusion heuristic is too narrow and can produce false-positive warnings in valid type-literal contexts.

Copilot's findings
  • Files reviewed: 8/8 changed files
  • Comments generated: 1

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

Comment on lines +1459 to +1464
const firstInsideBraceIdx17 = nextSignificant(tokens, openBraceIdx17 + 1);
const isNotificationFirst17 = firstInsideBraceIdx17 === i;
if (isNotificationFirst17 && prevOpen17 && (
prevOpen17.kind === "eq" || // type T = { ... }
(prevOpen17.kind === "punct" && prevOpen17.text === ":") // x: { ... }
)) continue;
…non-first members

type T = { x: string; Notification() } was incorrectly producing a SYN017
warning because the exclusion only fired when Notification was the first
significant token inside the enclosing type-literal brace.

Fix: expand the member-position check to also match when the token immediately
preceding Notification is a ';' or ',' separator (subsequent type members).
Gate the whole block on isEmptyParens17 so non-empty calls like
Notification('title') in value objects are unaffected.

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.

⚠️ Not ready to approve

There are verified inconsistencies/edge-case gaps in the new SYN017 implementation/documentation that should be resolved to match the PR’s stated exclusions and keep registry metadata consistent.

Copilot's findings
  • Files reviewed: 8/8 changed files
  • Comments generated: 2

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

Comment on lines +1443 to +1472
// Conditions: (a) parens are empty (no significant tokens between `(` and `)`),
// (b) enclosing `{` is in a type context (preceded by `=` or `:`),
// (c) `Notification` is at a method-signature position: either the first
// significant token inside the `{`, or preceded by `;` / `,`
// (subsequent type member, e.g. `{ x: string; Notification() }`).
{
const isEmptyParens17 = nextSignificant(tokens, callIdx17 + 1) >= (callTok17.matchedAt as number);
if (isEmptyParens17) {
let closeBrace17 = afterClose17;
if (closeBrace17 &&
closeBrace17.kind === "punct" &&
(closeBrace17.text === ";" || closeBrace17.text === ",")) {
const nextAfterSepIdx17 = nextSignificant(tokens, afterCloseIdx17 + 1);
closeBrace17 = tokens[nextAfterSepIdx17];
}
if (closeBrace17 && closeBrace17.kind === "close" && closeBrace17.text === "}" &&
closeBrace17.matchedAt !== undefined) {
const openBraceIdx17 = closeBrace17.matchedAt;
const prevOpenIdx17 = prevSignificant(tokens, openBraceIdx17 - 1);
const prevOpen17 = tokens[prevOpenIdx17];
const firstInsideBraceIdx17 = nextSignificant(tokens, openBraceIdx17 + 1);
// Method-signature position: first token in the type literal, or preceded by
// a member separator (handles `{ x: string; Notification() }` etc.)
const isAtMemberPos17 =
firstInsideBraceIdx17 === i ||
(prev17 && prev17.kind === "punct" && (prev17.text === ";" || prev17.text === ","));
if (isAtMemberPos17 && prevOpen17 && (
prevOpen17.kind === "eq" || // type T = { ... }
(prevOpen17.kind === "punct" && prevOpen17.text === ":") // x: { ... }
)) continue;
Comment thread packages/mcp/src/explanations.ts
Copilot review feedback on PR #164:

1. syn-check.ts: the method-signature exclusion was too narrow — only
   caught `=` and `:` before `{`. Now also skips when `{` is preceded by
   `&`/`|`/`<`/`>` (intersection, union, generic) or `as`/`extends`/
   `satisfies` keywords, covering all common TS type-literal positions.

2. explanations.ts: SYN017 title was "Notification() / new Notification()"
   (flipped order vs error-codes.ts "new Notification() / Notification()").
   Aligned to match.

Co-Authored-By: Botkowski <noreply@botkowski.ai>
@marcelofarias marcelofarias requested a review from Copilot June 18, 2026 02:29
Copilot AI dismissed their stale review, a newer Copilot review was requested. June 18, 2026 02:31
Copilot AI previously approved these changes Jun 18, 2026

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.

✅ Ready to approve

The change is additive and well-contained, with comprehensive tests and consistent updates across compiler, MCP, and documentation.

Note: this review does not count toward required approvals for merging.

Copilot's findings
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

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.

⚠️ Not ready to approve

The new SYN017 test fixtures use await inside non-async fn bodies, making the examples invalid JS/TS and inconsistent with existing compiler warning regression tests.

Copilot's findings
  • Files reviewed: 8/8 changed files
  • Comments generated: 2

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

it("fires on Notification() in ternary consequent with await (not a method signature)", () => {
const src =
"?bs 0.7\n" +
"fn check(cond: boolean, title: string) -> void {\n" +
it("fires on new Notification() in ternary consequent with await (not a method signature)", () => {
const src =
"?bs 0.7\n" +
"fn check(cond: boolean, title: string) -> void {\n" +
…ures as async fn

Two ternary+await test fixtures used `await` inside a non-async fn body,
producing invalid JS/TS. Changed both to `async fn ... -> Promise<void>`,
consistent with how other SYN check tests handle ternary+await regressions
(e.g. SYN007).

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.

⚠️ Not ready to approve

The new TS type-literal exclusion logic for empty-parens Notification() signatures appears to miss the generic-argument form Foo<Bar, { Notification() }> (false-positive warning risk) and should be corrected before approval.

Copilot's findings
  • Files reviewed: 8/8 changed files
  • Comments generated: 1

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

Comment on lines +1469 to +1483
if (isAtMemberPos17 && prevOpen17 && (
prevOpen17.kind === "eq" || // type T = { ... }
(prevOpen17.kind === "punct" && prevOpen17.text === ":") || // x: { ... }
(prevOpen17.kind === "operator" && ( // intersection / union / generic
prevOpen17.text === "&" || // Foo & { ... }
prevOpen17.text === "|" || // Foo | { ... }
prevOpen17.text === "<" || // Foo<{ ... }>
prevOpen17.text === ">" // closing generic: Foo<Bar, { ... }>
)) ||
(prevOpen17.kind === "ident" && ( // keyword-led type positions
prevOpen17.text === "as" || // x as { ... }
prevOpen17.text === "extends" || // T extends { ... }
prevOpen17.text === "satisfies" // x satisfies { ... }
))
)) continue;
…args

Foo<Bar, { Notification() }> — the `{` is preceded by `,` (comma between
generic type arguments), which was not treated as a type-context token.
Replaces the incorrect `>` case (which never matched a real type-literal
position) with the correct `,` case; adds a regression test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@marcelofarias marcelofarias requested a review from Copilot June 18, 2026 14:39
Copilot AI dismissed their stale review, a newer Copilot review was requested. June 18, 2026 14:44

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.

✅ Ready to approve

The new diagnostic is correctly version-gated, suppressed in unsafe contexts, comprehensively tested, and fully wired through compiler registry, MCP explanations, and docs.

Note: this review does not count toward required approvals for merging.

Copilot's findings
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

@marcelofarias marcelofarias merged commit a1b1fce into main Jun 18, 2026
3 checks passed
@marcelofarias marcelofarias deleted the botkowski/syn017-notification-bypass branch June 18, 2026 18:26
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