Skip to content

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

Merged
marcelofarias merged 11 commits into
mainfrom
botkowski/syn006-eval-check
Jun 12, 2026
Merged

feat(compiler): SYN004 — warn on eval() and new Function() calls in fn bodies (?bs 0.7+)#145
marcelofarias merged 11 commits into
mainfrom
botkowski/syn006-eval-check

Conversation

@marcelofarias

@marcelofarias marcelofarias commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds SYN004: a new warning that fires when a fn body calls eval(...), constructs new Function(...), or calls bare Function(...) / optional-call forms at ?bs 0.7+
  • eval() and Function()/new Function() execute strings as code at runtime — every static check botscript provides can be bypassed by routing any unsafe pattern through eval (CAP001/CAP002, reads/writes declarations, SYN002/SYN003)
  • Suppressed inside unsafe "reason" {} blocks and unsafe "reason" fn bodies
  • Idiomatic fix: refactor to explicit code; if eval is genuinely needed, wrap in unsafe "<reason>" { eval(...) }

Closes #144 (filed as SYN006; implemented as SYN004 — next available code on main; SYN005 is still in open PR #143).

Changes

File Change
packages/compiler/src/error-codes.ts Add SYN004 entry (rule, idiom, rewrite, example)
packages/compiler/src/passes/syn-check.ts Detection loop: eval not preceded by ./?. then ( or ?.( or <T>(; new Function / bare Function + optional/generic call forms; unsafe suppression; ternary guard; return-type-annotated declaration exclusion
packages/compiler/tests/syn004-check.test.ts 26 tests: fires/not-fires cases, severity, unsafe suppression, method-call exclusion, ternary branches, optional-call forms (eval?.(), Function?.())
packages/compiler/tests/error-codes.test.ts Add SYN004 to exhaustive allowlist
packages/mcp/src/explanations.ts Add SYN004 long-form explanation with fails/passes examples
packages/mcp/tests/server.test.ts Add SYN004 to KNOWN_CODES list
AGENTS.md Add SYN004 row to diagnostic table; clarify SYN005 row wording
README.md Add SYN004 to explain tool code list

Test plan

  • pnpm -r build passes
  • npx vitest run — 26/26 SYN004 tests pass
  • SYN004 fires on eval(code), eval('string'), eval<any>(code), eval?.(code)
  • SYN004 fires on new Function(body), new Function<any>(body), bare Function(body), Function?.(body)
  • SYN004 does NOT fire below ?bs 0.7, inside unsafe {}, inside unsafe fn
  • SYN004 does NOT fire on vm.eval() (member call on local), Function.prototype.call(), bare Function reference
  • SYN004 does NOT fire on return-type-annotated declarations (function eval(x: T): U {})
  • SYN004 fires correctly on eval/Function in ternary branches (not suppressed by :)
  • MCP explain tool returns long-form for SYN004; example pair compiles correctly

🤖 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

This PR introduces SYN004, a new ?bs 0.7+ compiler warning intended to flag dynamic code execution via eval(...) and new Function(...) in fn bodies, with suppression inside unsafe "reason" { ... } blocks and unsafe "reason" fn bodies. It also wires the new diagnostic through the compiler’s error-code registry, MCP explain support, docs, and tests.

Changes:

  • Add SYN004 to the compiler registry and implement token-based detection in passSynCheck.
  • Add a new SYN004 test suite and extend existing “known codes” allowlists.
  • Add SYN004 to MCP explanations and end-user documentation lists (README / AGENTS).

Reviewed changes

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

Show a summary per file
File Description
README.md Adds SYN004 to the list of codes supported by the explain tool.
packages/mcp/tests/server.test.ts Adds SYN004 to MCP KNOWN_CODES contract test.
packages/mcp/src/explanations.ts Adds long-form SYN004 explanation and example pair.
packages/compiler/tests/syn004-check.test.ts Adds SYN004 detection/suppression/severity tests.
packages/compiler/tests/error-codes.test.ts Adds SYN004 to the exhaustive error-code allowlist.
packages/compiler/src/passes/syn-check.ts Implements SYN004 detection for eval / new Function in fn bodies.
packages/compiler/src/error-codes.ts Registers SYN004 metadata (rule/idiom/rewrite/example).
AGENTS.md Documents SYN004 in the diagnostic 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/syn004-check.test.ts
Comment thread packages/mcp/src/explanations.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 8 out of 8 changed files in this pull request and generated 10 comments.

Comment thread packages/compiler/src/passes/syn-check.ts
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 packages/mcp/src/explanations.ts
Comment thread packages/mcp/src/explanations.ts Outdated
Comment thread AGENTS.md Outdated
Comment thread packages/compiler/tests/syn004-check.test.ts Outdated
Comment thread packages/compiler/tests/syn004-check.test.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 8 out of 8 changed files in this pull request and generated 7 comments.

Comment thread packages/compiler/src/passes/syn-check.ts
Comment thread packages/compiler/src/passes/syn-check.ts
Comment thread packages/compiler/src/passes/syn-check.ts Outdated
Comment thread packages/compiler/src/passes/syn-check.ts Outdated
Comment thread packages/compiler/src/passes/syn-check.ts
Comment thread packages/mcp/src/explanations.ts
Comment thread packages/compiler/src/error-codes.ts

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 +384 to +387
message:
`fn '${decl.name}' constructs ${hasNew ? "new " : ""}Function${funcCallSep}() — ` +
`the Function constructor executes a string as code and bypasses all static checks; ` +
`refactor to explicit code or wrap in unsafe "reason" { new Function(body) }`,

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 3 comments.

Comment on lines +287 to +298
// Must be followed by `(` (direct call) or `?.(` (optional call).
const nextIdx4 = nextSignificant(tokens, i + 1);
const next4 = tokens[nextIdx4];
let isOptEval = false;
let callIdx4 = nextIdx4;
if (next4 && next4.kind === "questionDot") {
isOptEval = true;
callIdx4 = nextSignificant(tokens, nextIdx4 + 1);
}
const callTok4 = tokens[callIdx4];
if (!callTok4 || !(callTok4.kind === "open" && callTok4.text === "(")) continue;

Comment on lines +346 to +357
// Exclude: `Function.prototype.*` — followed by `.` (member access, not a call)
// Must be followed by `(` (direct call) or `?.(` (optional call).
const nextIdx4 = nextSignificant(tokens, i + 1);
const next4 = tokens[nextIdx4];
let isOptFunc = false;
let callIdx4 = nextIdx4;
if (next4 && next4.kind === "questionDot") {
isOptFunc = true;
callIdx4 = nextSignificant(tokens, nextIdx4 + 1);
}
const callTok4 = tokens[callIdx4];
if (!callTok4 || !(callTok4.kind === "open" && callTok4.text === "(")) continue;
const result = transform(src);
expect(result.warnings.some((w) => w.code === "SYN004")).toBe(true);
});

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 3 comments.

Comment on lines +318 to +326
if (callTok4.matchedAt !== undefined) {
const afterCloseIdx = nextSignificant(tokens, callTok4.matchedAt + 1);
const afterClose = tokens[afterCloseIdx];
if (afterClose && (
(afterClose.kind === "open" && afterClose.text === "{") ||
(afterClose.kind === "punct" && afterClose.text === ":") ||
afterClose.kind === "fatArrow"
)) continue;
}
Comment on lines +392 to +400
if (callTok4.matchedAt !== undefined) {
const afterCloseIdx = nextSignificant(tokens, callTok4.matchedAt + 1);
const afterClose = tokens[afterCloseIdx];
if (afterClose && (
(afterClose.kind === "open" && afterClose.text === "{") ||
(afterClose.kind === "punct" && afterClose.text === ":") ||
afterClose.kind === "fatArrow"
)) continue;
}
Comment on lines +18 to +19

it("fires on eval() with a string literal", () => {

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 4 comments.

Comment on lines +324 to +336
// Exclude declarations: `function eval(params) {}` and method shorthands `{ eval(params) {} }`.
// Only exclude when `)` is directly followed by `{` (method body) or `=>` (arrow method).
// We intentionally omit `:` here: `eval(x) : y` in a ternary would be incorrectly
// excluded, causing a false negative. Real `eval(): T {}` declarations are caught by
// the `{` check via the type annotation scan, and the pattern is rare in botscript.
if (callTok4.matchedAt !== undefined) {
const afterCloseIdx = nextSignificant(tokens, callTok4.matchedAt + 1);
const afterClose = tokens[afterCloseIdx];
if (afterClose && (
(afterClose.kind === "open" && afterClose.text === "{") ||
afterClose.kind === "fatArrow"
)) continue;
}
Comment on lines +401 to +411
// Exclude declarations: `function Function(params) {}` and method shorthands.
// Omit `:` for the same reason as the eval check above — it causes false negatives
// in ternary branches (e.g. `cond ? Function(body) : fallback`).
if (callTok4.matchedAt !== undefined) {
const afterCloseIdx = nextSignificant(tokens, callTok4.matchedAt + 1);
const afterClose = tokens[afterCloseIdx];
if (afterClose && (
(afterClose.kind === "open" && afterClose.text === "{") ||
afterClose.kind === "fatArrow"
)) continue;
}
Comment thread packages/mcp/src/explanations.ts Outdated
Comment thread AGENTS.md Outdated
| SYN003 | (0.7+, warning) A fn body contains a `console.*` call (console.log, console.error, etc.). Direct console output bypasses the `stdout`/`stderr` capability model — the compiler cannot enforce or surface the output declaration for callers. | Replace `console.log(...)` with `stdout.write(...)` and add `uses { stdout }` to the fn header; replace `console.error(...)` with `stderr.write(...)` and add `uses { stderr }`. |
| SYN005 | (0.7+, warning) A fn body accesses `process.env`. `process.env` is a global deployment-environment namespace — access is invisible to callers, there is no capability or resource declaration that covers it, and the fn has an undeclared dependency on deployment configuration. Detection: `process` not preceded by `.`/`?.`, followed by `.`/`?.` then `env`. `obj.process.env` (member access on a local), `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. | Pass config and secrets as explicit fn parameters so the dependency is visible in the call signature; if env access is required at the load site, wrap in `unsafe "reads deployment env" { }`. |
| SYN004 | (0.7+, warning) A fn body calls `eval(...)` / `eval?.(...)` (global eval not preceded by `.`/`?.`) or calls `Function(...)` / `Function?.(...)` / `new Function(...)` (Function constructor not preceded by `.`/`?.`). All forms execute strings as code at runtime — every static capability check (CAP001/CAP002), resource declaration (reads/writes), and safety check (SYN002/SYN003) can be bypassed by routing any unsafe pattern through eval or the Function constructor. Suppressed inside `unsafe {}` blocks and `unsafe fn` bodies. `.eval(...)` (method call on a local) and `Function.*` member accesses are excluded. | Refactor the eval-based pattern to use explicit code paths. If eval is genuinely required (e.g. sandboxed interpreter), wrap in `unsafe "<reason>" { eval(...) }`. |
| SYN005 | (0.7+, warning) A fn body accesses `process.env`. `process.env` is a global deployment-environment namespace — access is invisible to callers, there is no capability or resource declaration that depends on deployment configuration. Detection: `process` not preceded by `.`/`?.`, followed by `.`/`?.` then `env`. `obj.process.env` (member access on a local), `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. | Pass config and secrets as explicit fn parameters so the dependency is visible in the call signature; if env access is required at the load site, wrap in `unsafe "reads deployment env" { }`. |

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 3 comments.

Comment thread AGENTS.md Outdated
| SYN003 | (0.7+, warning) A fn body contains a `console.*` call (console.log, console.error, etc.). Direct console output bypasses the `stdout`/`stderr` capability model — the compiler cannot enforce or surface the output declaration for callers. | Replace `console.log(...)` with `stdout.write(...)` and add `uses { stdout }` to the fn header; replace `console.error(...)` with `stderr.write(...)` and add `uses { stderr }`. |
| SYN005 | (0.7+, warning) A fn body accesses `process.env`. `process.env` is a global deployment-environment namespace — access is invisible to callers, there is no capability or resource declaration that covers it, and the fn has an undeclared dependency on deployment configuration. Detection: `process` not preceded by `.`/`?.`, followed by `.`/`?.` then `env`. `obj.process.env` (member access on a local), `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. | Pass config and secrets as explicit fn parameters so the dependency is visible in the call signature; if env access is required at the load site, wrap in `unsafe "reads deployment env" { }`. |
| SYN004 | (0.7+, warning) A fn body calls `eval(...)` / `eval?.(...)` (global eval not preceded by `.`/`?.`) or calls `Function(...)` / `Function?.(...)` / `new Function(...)` (Function constructor not preceded by `.`/`?.`). All forms execute strings as code at runtime — every static capability check (CAP001/CAP002), resource declaration (reads/writes), and safety check (SYN002/SYN003) can be bypassed by routing any unsafe pattern through eval or the Function constructor. Suppressed inside `unsafe {}` blocks and `unsafe fn` bodies. `.eval(...)` (method call on a local) and `Function.*` member accesses are excluded. | Refactor the eval-based pattern to use explicit code paths. If eval is genuinely required (e.g. sandboxed interpreter), wrap in `unsafe "<reason>" { eval(...) }`. |
| SYN005 | (0.7+, warning) A fn body accesses `process.env`. `process.env` is a global deployment-environment namespace — access is invisible to callers, there is no capability or resource declaration that depends on deployment configuration. Detection: `process` not preceded by `.`/`?.`, followed by `.`/`?.` then `env`. `obj.process.env` (member access on a local), `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. | Pass config and secrets as explicit fn parameters so the dependency is visible in the call signature; if env access is required at the load site, wrap in `unsafe "reads deployment env" { }`. |
Comment thread packages/compiler/src/error-codes.ts Outdated
Comment on lines +555 to +558
idiom:
"refactor eval-based patterns to use explicit code paths or config parameters; " +
"if eval is unavoidable (e.g. a sandboxed interpreter or intentional scripting surface), " +
"wrap in `unsafe \"<reason>\" { }` to make the escape hatch visible in the diff",
Comment on lines +761 to +764
"SYN004 fires at `?bs 0.7+` as a non-blocking warning. Detection is token-based: " +
"`eval` not preceded by `.`/`?.` followed by `(` or `?.(`; bare `Function(...)` / " +
"`Function?.(...)` / `new Function(...)` not preceded by `.`/`?.`. " +
"`.eval(...)` (method call on a local object) and `Function.*` member accesses are excluded.",

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 2 comments.

// TypeScript instantiation form: eval<T>(...)
let depth = 1;
let j = nextIdx4 + 1;
while (j < tokens.length && depth > 0) {
// TypeScript instantiation form: Function<T>(...) / new Function<T>(...)
let depth = 1;
let j = nextIdx4 + 1;
while (j < tokens.length && depth > 0) {

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 5 comments.

Comment on lines +305 to +321
} else if (next4 && next4.kind === "operator" && next4.text === "<") {
// TypeScript instantiation form: eval<T>(...)
let depth = 1;
let j = nextIdx4 + 1;
while (j < tokens.length && depth > 0) {
const t = tokens[j];
if (!t) break;
if (t.kind === "operator" && t.text === "<") depth++;
else if (t.kind === "operator" && (t.text === ">" || t.text === ">>" || t.text === ">>>"))
depth -= t.text.length;
j++;
}
const afterGenericIdx4 = nextSignificant(tokens, j);
const afterGeneric4 = tokens[afterGenericIdx4];
if (afterGeneric4 && afterGeneric4.kind === "open" && afterGeneric4.text === "(")
callIdx4 = afterGenericIdx4;
}
Comment on lines +305 to +321
} else if (next4 && next4.kind === "operator" && next4.text === "<") {
// TypeScript instantiation form: eval<T>(...)
let depth = 1;
let j = nextIdx4 + 1;
while (j < tokens.length && depth > 0) {
const t = tokens[j];
if (!t) break;
if (t.kind === "operator" && t.text === "<") depth++;
else if (t.kind === "operator" && (t.text === ">" || t.text === ">>" || t.text === ">>>"))
depth -= t.text.length;
j++;
}
const afterGenericIdx4 = nextSignificant(tokens, j);
const afterGeneric4 = tokens[afterGenericIdx4];
if (afterGeneric4 && afterGeneric4.kind === "open" && afterGeneric4.text === "(")
callIdx4 = afterGenericIdx4;
}
Comment on lines +383 to +399
} else if (next4 && next4.kind === "operator" && next4.text === "<") {
// TypeScript instantiation form: Function<T>(...) / new Function<T>(...)
let depth = 1;
let j = nextIdx4 + 1;
while (j < tokens.length && depth > 0) {
const t = tokens[j];
if (!t) break;
if (t.kind === "operator" && t.text === "<") depth++;
else if (t.kind === "operator" && (t.text === ">" || t.text === ">>" || t.text === ">>>"))
depth -= t.text.length;
j++;
}
const afterGenericIdx4 = nextSignificant(tokens, j);
const afterGeneric4 = tokens[afterGenericIdx4];
if (afterGeneric4 && afterGeneric4.kind === "open" && afterGeneric4.text === "(")
callIdx4 = afterGenericIdx4;
}
Comment on lines +352 to +355
message:
`fn '${decl.name}' calls eval${callSep4}() — ` +
`eval executes a string as code and bypasses all static capability, ` +
`resource, and safety checks; refactor to explicit code or wrap in unsafe "reason" { eval(src) }`,
Comment on lines +436 to +439
message:
`fn '${decl.name}' constructs ${hasNew ? "new " : ""}Function${funcCallSep}() — ` +
`the Function constructor executes a string as code and bypasses all static checks; ` +
`refactor to explicit code or wrap in unsafe "reason" { ${hasNew ? "new Function(body)" : "Function(body)"} }`,

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.


for (const { decl } of program.fns) {
// An `unsafe "reason" fn` body is an explicit acknowledgment — skip SYN002/SYN003/SYN005.
// An `unsafe "reason" fn` body is an explicit acknowledgment — skip SYN002/SYN003/SYN004/SYN005.

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.


for (const { decl } of program.fns) {
// An `unsafe "reason" fn` body is an explicit acknowledgment — skip SYN002/SYN003/SYN005.
// An `unsafe "reason" fn` body is an explicit acknowledgment — skip SYN002/SYN003/SYN004/SYN005.
Marcelo Farias and others added 11 commits June 11, 2026 23:30
…n 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>
`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>
… 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>
…n; 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>
…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>
…tion<T>()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ry 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>
…n 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>
- 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>
…ers instantiation forms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ken 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.
@marcelofarias marcelofarias force-pushed the botkowski/syn006-eval-check branch from 1bd1069 to e6c0872 Compare June 12, 2026 02:33
@marcelofarias marcelofarias merged commit 796145e into main Jun 12, 2026
2 checks passed
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.

proposal: SYN006 — warn on eval() and new Function() calls in fn bodies (?bs 0.7+)

2 participants