Skip to content

js_parser: parse a TypeScript declaration named as or satisfies - #46065

Open
robobun wants to merge 1 commit into
mainfrom
robobun/5f083a9a/ts-declaration-named-as
Open

robobun wants to merge 1 commit into
mainfrom
robobun/5f083a9a/ts-declaration-named-as

Conversation

@robobun

@robobun robobun commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • type as = number; fails with error: Unexpected =. interface as { a: number } becomes the statement interface;. tsc and bun check accept both.
  • parse_stmt_fallthrough (src/js_parser/parse/parse_stmt.rs:1720) parses an expression first. Its suffix loop (parse_suffix.rs:23) takes the name as the cast operator.

Fix

  • After a statement starts with type, interface, namespace or module, the parser looks past as or satisfies. If =, {, extends or . follows, it is the name. If not, it stays a cast (type satisfies never;).
  • Verified: test/bundler/transpiler/transpiler.test.js (40 new tests, 36 fail on main) and test/bundler/esbuild/dce.test.ts (1 new).
  • Self-reviewed: 21 concerns raised, 15 addressed. Not done: a tsc oracle matrix as a test (TypeScript loads in 20 s on a debug build), and 5 for other PRs.

Background

  • parse_prefix reads one operand. The suffix loop reads the operators after it, such as x as T.
  • tsc and bun check always read an identifier after the keyword as the name. That rule also rejects default: type satisfies never;, which runs today (satisfies does not work on a variable called type microsoft/TypeScript#58248). It is 20 source lines shorter.
  • The transpiler cache version goes from 43 to 44, because an old entry can hold interface;.

Downsides

  • Each TypeScript statement that starts with another identifier pays 6 instructions (foo(1);: 3,262 to 3,268). type T = number;: 1,496 to 1,368.
  • Whole files: src/js/node/fs.ts +0.03% instructions, lib.dom.d.ts -0.59%. Release binary text: +1,280 B.
  • A cast of namespace, module or interface to a { type is now a declaration. namespace as { a?: number }; is now Unexpected :.
Notes

There is no GitHub issue. A differential run against tsc and bun check found the inputs. test/cli/check/differential-cases.json already has one as case 91-R05a. In a module, the stray interface; is reported as "interface" is a reserved word and cannot be used in strict mode.

Inputs (tsc 6.0.2 parseDiagnostics, esbuild 0.28.2, Bun main, this PR)

input tsc esbuild main PR
type as = number; type satisfies<T> = T; ok error error ok
interface as { a: number } ok error interface; ok
namespace as { export const x = 1 } module as.b {..} ok error error ok
namespace as {} ok namespace; namespace; ok
the same after declare, export, export declare, export default interface ok error error ok
type as number; switch (type) { default: type satisfies never; } namespace as T; error cast cast cast
type as <T>() => T; type as { a: number }; error cast cast cast
function f() { namespace as {} } ok (checker: TS1235) namespace; namespace; namespace;
namespace as { a: number }; namespace cast cast namespace
namespace as { a?: number }; namespace as {} | null; interface as { a: number } | null error cast cast error
var y = type as number; (type as number); type + line break + as; ok ok ok ok

swc 1.16, oxc 0.154 and babel 8 accept the type and interface declarations. babel and swc took this as a bug report (babel/babel#16048, swc-project/swc#8121) and moved to the tsc rule.

The rule

  • The parse differs from main only when as or satisfies follows the keyword and the rest of a declaration follows that name. In every other case parse_suffix runs on the same tokens as on main.
  • type: optional type parameters, then =. So type as<T> = T is an alias and type as <T>() => T; is a cast.
  • interface: optional type parameters, then { or extends.
  • namespace / module: { or ., and not in a nested scope, where Bun has no namespace declarations.
  • Only the token after the name decides. So a statement that main read as a cast to an object type is a declaration now. tsc, oxc and babel reject such a statement when the braces are not a valid body or when |, & or as follows them. esbuild and swc read the cast.

Measurements (release builds of main 017b243 and of this change on that commit, user-mode instructions from perf_event_open)

  • These numbers are from before the rebase onto 38afee6. The diff changed in one place since then: the emit_decorator_metadata switch moved into P::speculate (see below). That adds one byte load and two byte stores to the cold look past the name.
  • Instructions per statement, (200k copies - 100k copies) / 100k, min of 3. In .js: foo(1); 3,136.1 to 3,136.1. In .ts: foo(1); 3,262.1 to 3,268.1, expect(1); 3,394.3 to 3,400.3, x = y as T; 3,990.8 to 3,996.8, type T = number; 1,496.1 to 1,368.1, interface I { a: string; b: number } 2,523.2 to 2,404.2, namespace N { export const a = 1 } 10,635.6 to 10,514.6, module.exports.a = 1; 4,878.9 to 4,830.9, type = 1; 3,079.9 to 3,029.9.
  • The rare paths: type as number; 2,461.5 to 3,064.5. type as = number; costs 1,889.1.
  • Whole process, bun build --no-bundle, min of 5: lib.dom.d.ts 38,060,378 to 37,835,915 (-0.59%). src/js/node/fs.ts 7,945,336 to 7,947,504 (+0.027%). src/js/node/http2.ts 26,459,206 to 26,462,659 (+0.013%). typescript.js as .ts (9.1 MB) 1,130,788,509 to 1,130,936,080 (+0.013%).
  • Symbols (llvm-nm -S, llvm-objdump -d): parse_expr_common x4, parse_suffix x4, parse_prefix x4, parse_expr_or_let_stmt x4 and the JavaScript parse_stmt x2 have the same size and 0 differing instructions. The TypeScript parse_stmt x2 grows from 3,455 B to 3,891 B and its frame from 152 B to 168 B. New and cold: parse_ts_decl_keyword_expr 505 B x2 (frame 56 B), ts_decl_follows_name 533 B x2.
  • Maximum nesting depth that parses (8 MB stack): blocks 9,344 to 9,342, functions 5,467 to 5,466, x = function(){ 3,315 to 3,314, module.exports = function(){ 3,316 to 3,315, type = function(){ 3,315 to 3,315, module.exports = () => { 2,719 to 2,824.
  • Release binary: .text 81,368,037 to 81,369,317 (+1,280 B). llvm-size text total +1,880 B.
  • Output and exit code of bun build --no-bundle are identical for 20,475 of 20,475 TypeScript files in this checkout, node_modules included.
  • Transpiler cache: 0 of 3 existing entries are reused after the upgrade, 3 are written again once. Bun 1.4.3 has version 34, so the next release invalidates the cache with or without this PR. 44 is the next free number today. If another PR takes it first, this one takes the next.

Tests

  • Suites run on the debug build: test/bundler/transpiler/transpiler.test.js, runtime-transpiler.test.ts, scope-mismatch-panic.test.ts, transpiler-stack-overflow.test.ts, decorators.test.ts, decorator-metadata.test.ts, es-decorators.test.ts, ts-enum-redecl-panic.test.ts, ts-use-define-for-class-fields.test.ts, test/bundler/esbuild/{ts,dce,importstar_ts}.test.ts, test/js/bun/transpiler/{repl-transform,parse-error-column}.test.ts, test/cli/run/transpiler-cache.test.ts.
  • Four of the new tests pass on main. Each has a variant of the fix that fails it. "a cast of a variable that has the name of the keyword stays a cast" fails with the tsc rule and without the nested-scope test. "only the first token of a statement is tested for the keyword" fails without is_identifier in the new branch (checked with a build). "the keyword is an identifier when no name follows it on the line" fails without the token test in parse_expr_common. "the js and jsx loaders have no such declarations" aborts without IS_TYPESCRIPT_ENABLED in the new branch.
  • The existing test "deeply nested TypeScript types error instead of crashing the process" has one new shape. It fails on main and pins that stack exhaustion during the look past the name is reported as such.
  • ALLOW_IN_OUT_VARIANCE_ANNOTATIONS in the look past the name only changes what is logged, and the log is restored. No test can fail without it. It is there so that the look skips with the flags of the declaration parse.
  • A matrix with tsc as the oracle is not in the tree: in a debug build, loading TypeScript takes 20 s.
  • is, asserts, infer, keyof, readonly, unique, out, of and type as names already work on main.

Self-review

  • Not done, with the reason: a generated matrix with tsc as the oracle, as a test. In a debug build, loading TypeScript takes 20 s. The tests pin the cases by hand.
  • Not done, for other PRs: a RuntimeTranspilerCache key from the build revision (two concerns), the bun fmt parentheses for namespace (two concerns), a run of the transpiler over the tsc corpora in test/cli/check.

Other changes in the diff

Not fixed here

  • type! Foo = number; and type as any Foo = number; still declare Foo. tsc rejects both.
  • declare, abstract and global decide after the suffix loop, as before. declare as number; prints nothing. tsc prints declare;.
  • Bun has no namespace declaration in a nested scope. function f() { namespace Foo {} } is an error on main and stays one.
  • export type as = number; already parses (t_export reads type itself). tsc rejects it.
  • import { type as } from "x" keeps a runtime import "x", where { type Foo } is dropped. js_parser: drop a TypeScript import whose clause binds nothing #41542 is open for that arm.
  • using => 1; and let => 1; fail with Expected ";" but found "=>". parser: fix where using declarations may appear (switch clauses, for heads, await newline) #40813 edits that arm.
  • bun fmt prints (namespace as { a: number }); without the parentheses, which is now a namespace. type, interface and module keep their parentheses there (src/format/js/parentheses/expression.rs:671).

References


[human-review] gate passed · iteration 0 · 7 files touched

fails on main (without fix)
ASAN without fix: 38 failed, 47 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/esbuild/dce.test.ts test/bundler/transpiler/transpiler.test.js
rustup spent 7s installing the pinned toolchain (nightly-2026-09-15); it was missing or incomplete on this machine
bun test v1.4.4 (367d939d9)

test/bundler/esbuild/dce.test.ts:
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepNamedImportES6 [924.36ms]
(pass) bundler > dce/UnreferencedObjectLiteral [413.17ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepNamedImportCommonJS [454.11ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepStarImportES6 [441.28ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepStarImportCommonJS [495.67ms]
(pass) bundler > dce/PackageJsonSideEffectsTrueKeepES6 [524.57ms]
(pass) bundler > dce/PackageJsonSideEffectsTrueKeepCommonJS [461.35ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireES6 [451.22ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireCommonJS [473.92ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseRemoveBareImportES6 [431.95ms]
(pass) bundler > dce/Pack
... (truncated)

release without fix: 70 failed, 47 skipped
bun test v1.4.3-canary.1 (367d939d9)

test/bundler/esbuild/dce.test.ts:
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepNamedImportES6 [452.80ms]
(pass) bundler > dce/UnreferencedObjectLiteral [34.43ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepNamedImportCommonJS [19.22ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepStarImportES6 [19.84ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepStarImportCommonJS [21.81ms]
(pass) bundler > dce/PackageJsonSideEffectsTrueKeepES6 [28.11ms]
(pass) bundler > dce/PackageJsonSideEffectsTrueKeepCommonJS [26.86ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireES6 [25.57ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireCommonJS [27.82ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseRemoveBareImportES6 [13.77ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseRemoveBareImportCommonJS [14.02ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseRemoveNamedImportES6 [12.59ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseRemoveNamedImportCommonJS [13.47ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseRemoveStarImportES6 [15.03ms]
(pass) bundler > dce/Pac
... (truncated)
passes on PR (with fix)
ASAN with fix: 47 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/esbuild/dce.test.ts test/bundler/transpiler/transpiler.test.js
rustup spent 17s installing the pinned toolchain (nightly-2026-09-15); it was missing or incomplete on this machine
bun test v1.4.4 (367d939d9)

test/bundler/esbuild/dce.test.ts:
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepNamedImportES6 [885.65ms]
(pass) bundler > dce/UnreferencedObjectLiteral [391.36ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepNamedImportCommonJS [379.95ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepStarImportES6 [392.13ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepStarImportCommonJS [448.98ms]
(pass) bundler > dce/PackageJsonSideEffectsTrueKeepES6 [399.81ms]
(pass) bundler > dce/PackageJsonSideEffectsTrueKeepCommonJS [402.52ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireES6 [592.49ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireCommonJS [438.51ms]
(pass) bundler > dce/PackageJsonSideEffectsFalseRemoveBareImportES6 [399.26ms]
(pass) bundler > dce/Pac
... (truncated)

release with fix: 47 skipped
$ bun scripts/build.ts --profile=release
rustup spent 9s installing the pinned toolchain (nightly-2026-09-15); it was missing or incomplete on this machine
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     3fe65a0ab9
  features     lto, baseline

23 deps, 228 codegen, 1218 objects in 15513ms

ninja: Entering directory `/workspace/bun/build/release'
[1/4] fetch lolhtml
[lolhtml] up to date
[2/4] fetch rust-argon2
[rust-argon2] up to date
[2/4] cargo plan → /workspace/bun/build/release/rust-target/plan.json
270 units: 194 lib, 16 proc-macro (host), 21 custom-build (host), 17 run custom-build, 17 lib (host), 4 run custom-build (host), 1 rlib
[3/4] reconfigure
[1/1577] mkdir stamps
[2/1577] mkdir codegen
[3/1577] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[4/1577] install /workspace/bun
bun install v1.4.3-canary.1 (367d939d9)

Checked 22 installs across 44 packages (no changes) [2.52s]
[5/1577] install /workspace/bun/packages/bun-error
bun install v1.4.3-canary.1 (367d939d9)

Checked 1 install across 2 packages (no changes) [64.00ms]
[6/1
... (truncated)
diff hotspot
src/js_parser/p.rs                           |  21 +++
 src/js_parser/parse/mod.rs                   |  12 +-
 src/js_parser/parse/parse_skip_typescript.rs |  33 ++--
 src/js_parser/parse/parse_stmt.rs            |  50 +++++-
 src/jsc/RuntimeTranspilerCache.rs            |   3 +-
 test/bundler/esbuild/dce.test.ts             |  21 +++
 test/bundler/transpiler/transpiler.test.js   | 221 +++++++++++++++++++++++++++
 7 files changed, 332 insertions(+), 29 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                          reads  edits  tests
src/js_parser/p.rs                                0      0     43
src/js_parser/parse/mod.rs                        3      1     43
src/js_parser/parse/parse_skip_typescript.rs      0      0     44
src/js_parser/parse/parse_stmt.rs                 5      3     43
src/jsc/RuntimeTranspilerCache.rs                 1      2     43
test/bundler/esbuild/dce.test.ts                  1      1     21
test/bundler/transpiler/transpiler.test.js        6      3     40

@robobun

robobun commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:50 AM PT - Oct 22nd, 2026

⏳ @robobun, your commit 3fe65a0 is still building in Build #130884, but has 1 failures so far (All Failures):

@robobun

robobun commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on main (017b243) and on 1.4.3-canary with bun build --no-bundle:

  • type as = number; const n: as = 1; export { n }; fails with error: Unexpected =.
  • interface as { a: number } const o: as = { a: 1 }; export { o }; fails with error: "interface" is a reserved word and cannot be used in strict mode.

tsc 6.0.2 reports no parse diagnostics for both files, and bun check accepts them.

The fix is in this PR (#46065).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked: the STOP_BEFORE_NAME early return in parse_expr_common skipping the __PURE__ handling (it only fires on a bare type/interface/namespace/module identifier followed by another identifier, so no call or new on a plain statement loses its annotation); the speculative type-parameter skip in ts_decl_follows_name leaving state behind (it runs under speculate, which restores the full parser snapshot, same as the existing conditional-arrow lookahead); and module.exports = ... now routing through the cold parse_ts_decl_keyword_expr (next token is ., not as/satisfies, so it is one extra call and no speculation).

Extended reasoning...

The change touches the TypeScript statement parser (new branch in parse_stmt_fallthrough plus two cold helpers), adds a const-generic flag to parse_expr_common, extracts a generic speculate helper used by the refactored conditional-arrow lookahead, and bumps the transpiler cache version to 36. No security-sensitive surface is involved. A confirmed finding on the namespace/module/interface { heuristic is posted inline, so a human should look at that before merging.

Comment thread src/js_parser/parse/parse_stmt.rs
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: e0d3b831-f4c3-4275-8798-3f8ba5c1d34b
📥 Commits

Reviewing files that changed from the base of the PR and between b4ca7a3 and d1cc644.

📒 Files selected for processing: 4
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_stmt.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • test/bundler/transpiler/transpiler.test.js

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The parser now distinguishes TypeScript declarations named as or satisfies from identifier and cast-expression uses. The changes add parser speculation, declaration lookahead, parser and bundler tests, and a runtime transpiler cache format version update.

Changes

TypeScript Declaration Name Parsing

Layer / File(s) Summary
Parser speculation and expression stopping
src/js_parser/p.rs, src/js_parser/parse/mod.rs, src/js_parser/parse/parse_skip_typescript.rs
Adds P::speculate, the STOP_BEFORE_NAME option to parse_expr_common, and speculation-based arrow-function parsing.
TypeScript declaration lookahead
src/js_parser/parse/parse_stmt.rs
Routes leading type, interface, namespace, and module identifiers through dedicated parsing. Checks syntax after as or satisfies to distinguish declarations from expression suffixes.
Parser and bundler coverage
test/bundler/transpiler/transpiler.test.js, test/bundler/esbuild/dce.test.ts, src/jsc/RuntimeTranspilerCache.rs
Adds transform, scan, parse-error, runtime, deep-type, and bundler coverage. Increments the runtime transpiler cache format version from 37 to 38.

Suggested reviewers: jarred-sumner, dylan-conway

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to d1cc6

The declaration-parsing change has no confirmed issue requiring a fix before merge.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title is concise and accurately identifies the main parser change: support for TypeScript declarations named as or satisfies.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is complete and relevant. It explains the problem, fix, scope, trade-offs, test coverage, verification results, and known limitations. It does not use the template headings exactly, bu…
Full details: Description check

Explanation

The description is complete and relevant. It explains the problem, fix, scope, trade-offs, test coverage, verification results, and known limitations. It does not use the template headings exactly, but it provides the required information in equivalent sections.

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun
robobun force-pushed the robobun/5f083a9a/ts-declaration-named-as branch from b4ca7a3 to d1cc644 Compare October 21, 2026 03:02

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I re-reviewed the re-pushed version and found no bugs; since this changes statement parsing in the TypeScript parser and deliberately trades one accepted shape for another, a human maintainer's look is still worthwhile.

What was reviewed:

  • P::speculate against the inline snapshot/restore code it replaces in parse_skip_typescript.rs — same snapshot, log-disable, restore order and the same StackOverflow/Alloc pass-through, so the conditional-arrow path is unchanged.
  • The STOP_BEFORE_NAME early return in parse_expr_common — parse_suffix only continues on an identifier token for as/satisfies, and parse_ts_decl_keyword_expr re-runs parse_suffix in exactly that case when no declaration follows, so other callers and the non-declaration cast path keep main's behavior; the skipped @ __PURE__ branch is covered by the new dce test.
  • ts_decl_follows_name lookahead respects StatementScope::Nested for namespace/module and the newline rule is enforced downstream by the existing keyword handlers; EXPECTED_VERSION is bumped for the changed interface; output.
  • The open tradeoff for a maintainer: a top-level namespace/module/interface variable cast to an object type (namespace as { a?: number };) now parses as a declaration and errors, matching tsc but diverging from esbuild and from main. The PR documents this under Downsides and pins it in tests.
Extended reasoning...

The change touches the TypeScript statement fallthrough in src/js_parser/parse/parse_stmt.rs, adds a const-generic flag to parse_expr_common in src/js_parser/parse/mod.rs, extracts a speculate helper in src/js_parser/p.rs, and bumps the transpiler cache version; it touches no security-sensitive surface. The refactors were checked line-by-line against the code they replace and are behavior-preserving, and the new tests cover ts/tsx loaders, transform/scan/scanImports, nested scopes, js/jsx negatives and a stack-exhaustion shape. Deferring rather than approving because the PR intentionally changes which inputs parse (object-type casts of variables named namespace/module/interface become declarations), a tsc-versus-esbuild policy choice for a parser maintainer, and because the author's reply to the prior inline comment on that point is not independently resolved.

A statement led by `type`, `interface`, `namespace` or `module` was
parsed as an expression first. The suffix loop took a following `as` or
`satisfies` as the cast operator, so `type as = number` was a syntax
error and `interface as {}` became the statement `interface;`.

For these four keywords the statement parser now stops the expression
before an identifier. If that identifier is `as` or `satisfies`, the
parser looks past it. When the rest of a declaration follows, the
identifier is the name. If not, the suffix loop runs and reads the cast
as before, so `type satisfies never;` still casts a variable named
`type`.

`parse_expr_common` takes a const flag for the stop, so the keyword
path shares its body. `P::speculate` holds the snapshot, restore and
error triage of a speculative parse for this look past the name and for
the arrow return type in a conditional.
@robobun
robobun force-pushed the robobun/5f083a9a/ts-declaration-named-as branch from d1cc644 to 3fe65a0 Compare October 22, 2026 11:40

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed the latest revision and didn't find any bugs. Because this changes a hot parser path and deliberately follows tsc rather than esbuild/main for namespace/module/interface followed by as/satisfies and an object type (now a declaration body, not a cast), a human look at that tradeoff is still worthwhile.

What was reviewed:

  • The STOP_BEFORE_NAME early return in parse_expr_common: the skipped parse_suffix only acts on an identifier token when it is as/satisfies (sfx_handle_typescript_as), which parse_ts_decl_keyword_expr handles itself, and the skipped __PURE__ branch cannot apply when an identifier follows the prefix.
  • ASI: type\nas = 3 still reaches parse_stmt_fallthrough_ts_keyword's has_newline_before checks and falls through to an expression statement, as before.
  • speculate: the lexer snapshot restores is_log_disabled, and the conditional-arrow-return-type refactor moves the old snapshot/restore/error-triage code unchanged.
Extended reasoning...

The change touches the TypeScript statement parser (src/js_parser/parse/parse_stmt.rs, parse/mod.rs, parse_skip_typescript.rs, p.rs), adds a const-generic flag to the shared parse_expr_common, extracts a speculate helper, bumps the runtime transpiler cache version, and adds tests in transpiler.test.js and dce.test.ts. It touches no security-sensitive surface. The code is coherent and the tests cover the ASI, cast-stays-cast, nested-scope and js/jsx-loader variants, but the parser is a hot path and the PR intentionally changes the parse of a cast-to-object-type after these keywords to match tsc, which is a behavior tradeoff a maintainer should weigh rather than an obviously correct mechanical change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant