Latest Results
fix(binding/node): Return an error when a file cannot be read instead of panicking (#12379)
## Description
`parse_file_sync` and `transform_file_sync` call `.expect()` on
`load_file`, so a path that cannot be read panics. The release profile
sets `panic = "abort"` ("Abort on panic to remove unwinding code"), so
the `catch_unwind` inside `try_with` never sees it: the panic takes the
whole Node process down.
With the published 1.16.2, on macOS arm64 and node 24.18.0:
```
$ node -e "require('@swc/core').parseFileSync('/definitely/not/here.js', { syntax: 'ecmascript' })"
thread '<unnamed>' panicked at bindings/binding_core_node/src/parse.rs:237:22:
failed to read program file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
$ echo $?
134
```
The async entry points beside them already do the right thing:
`parse_file` and `transform_file` use `.context(...)?` and surface a
rejected promise. This gives the two sync entry points the same
handling.
I ran into it through dependency-cruiser, whose swc parser calls
`parseFileSync`: a module that disappears between the file listing and
the parse, or a broken symlink, ends the whole cruise instead of failing
one module.
## Test
`packages/core/__tests__/parse/api_test.js` gets two cases: `parseFile`
rejects for a path that is not there, and `parseFileSync` throws.
For reviewers: in a debug build both pass with or without the fix,
because unwinding is on there and `catch_unwind` turns the panic into an
error. In a release build, which is what npm ships, the unfixed sync
case aborts the runner.
## Verified locally
macOS arm64, Rust 1.92, node 24.18.0:
- `cargo check -p binding_core_node` is clean.
- `npm run build:dev` in `packages/core`, then `rstest` over that file:
8 passing.
Happy to add a changeset if you want one; the change is in the node
binding only. feat(es/minifier): honor `/*#__PURE__*/` on property reads and destructuring patterns (#12384)
SWC supports a `pure_getters` compress option, but it is a whole-program
promise that *no* property read anywhere has side effects.
This adds a per-site opt-in — `/*#__PURE__*/` on an individual property
read and destructuring pattern. Rollup and esbuild read the annotation
the same way.
[Playground
(before)](https://play.swc.rs/?version=1.15.46&code=H4sIAAAAAAAC%2F1WQMU%2FEMAyF9%2FyKp7LAiWt3TgwM7Ahxc%2BU2bgmkceQkIHTiv5OoQojR1vP3PtltUTTjApne8I1FZUPn3dSdjBkGPIQgmTLbO1BKZWOLLJgYhOQsH3lZeM7HRZmhTPYWSeAyXIJViZFtb4bD1Tg%2BnZ8fx%2FEwtJ6efql7yTnQX01UTqwfXFETz1RS61o5Z1bMUryFlgDSyWUl%2Fao7y71p1OjJhZ348lqvdqaTmvbV6lP0PaFNsJyyljkXdWFFpAYPvZklpIz%2FuhdQFamfuW%2Fmld5C4rn3sl53VgJ3NyfzA4fr7vBGAQAA)
```js
import { obj } from "lib";
/*#__PURE__*/ obj.annotated; // dropped
obj.plain; // preserved
const /*#__PURE__*/ { a, b } = obj; // dropped
```
---
### 1. `feat(es/minifier): drop ignored property accesses under
pure_getters`
`pure_getters` was consulted in exactly one place —
`take_pat_if_unused`, which only decides whether unused bindings can be
dropped out of a destructuring pattern. A property access in statement
position was *always* preserved, even with `pure_getters: true`, because
`ignore_return_value` had no `Expr::Member` arm. So the option bought
considerably less than its name suggests.
This adds that arm. When the option allows it, the access is replaced by
the side effects of its operands: `x().y` becomes `x()`, and `({p:
1}).p` disappears. The object is evaluated before a computed key,
preserving evaluation order (`x()[y()]` → `x(), y()`).
`can_assume_pure_getter` implements all three variants of the option
rather than only `Bool(true)`:
- `Str` consults the allow list, including string-literal computed keys.
- `Strict` stays **disabled** — in terser it relaxes nullish checks, not
getter effects.
Writes are untouched: assignment, compound assignment, update and
`delete` can all run a setter, and the write is the point of the
statement.
This unblocks four terser fixtures (`issue_2313_6`, `issue_2938_3`,
`issue_2938_4`, `impure_getter_2`), which now match terser's output.
`issue_2838` stays postponed — it needs dead *assignment* removal, a
separate optimization.
### 2. `feat(es/minifier): honor /*#__PURE__*/ on property reads and
destructuring patterns`
Pure marks normally ride on a node's `SyntaxContext`, but `MemberExpr`,
`ObjectPat` and `ArrayPat` have none, so annotations on them go in a
side table keyed by source position. It is collected by `info_marker`
during the traversal it already performs, and read by `pure_optimizer`
and `take_pat_if_unused`.
#### Why a side table rather than `ctxt` on `MemberExpr`
Very feasible but API breaking for the JS-visible AST structures, hence
the side-table.
**Testing:**
New `tests/fixture/pure-getters/` suite covering the three option
variants, evaluation order, annotation ownership, nullish assertion,
survival through inlining, and the boundaries where the optimization
must not fire (writes, optional chaining, `strict`).
- `cargo test -p swc_ecma_minifier` — green, no snapshot churn
- `cargo test -p swc` — green
- `cargo check --workspace --all-targets` — clean
- `cargo clippy -p swc_ecma_minifier --all-targets -- -D warnings` —
clean
- `cargo fmt --all` — applied
**BREAKING CHANGE:**
None. Both changes are opt-in: the first only activates under an
explicit `pure_getters` setting, the second only where the user wrote an
annotation.
**Related issue (if exists):**
None.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> feat(es/minifier): honor `/*#__PURE__*/` on property reads and destructuring patterns
`pure_getters` is a whole-program promise that no property read anywhere
has side effects, which is too blunt to enable safely. This adds a
per-site opt-in: an annotation on a single access or destructuring
pattern, so a library can mark the places it knows are safe without
turning on the unsound global option.
Pure marks normally ride on a node's `SyntaxContext`, but `MemberExpr`,
`ObjectPat` and `ArrayPat` have none. Adding one to `MemberExpr` is
feasible (the `Expr` enum has room, and `CallExpr` already sets its
size), but `ctxt` is serialized into the JS-visible AST, so it would put
a new key on every member expression in `swc.parse()` output and in the
plugin AST schema. That is a breaking change, and not one worth spending
before the optimization has proven itself.
So annotations on those nodes go in a side table keyed by source
position, collected by `info_marker` during the traversal it already
performs and read by `pure_optimizer` and `take_pat_if_unused`. It is
held by shared reference, which keeps `Parallel::create` for `Pure` a
plain copy.
Keying on `lo` rather than the whole span is deliberate: a pass may
rebuild a node with a different `hi`, but `lo` is where a leading comment
attaches. A fixture covers an annotated access that survives inlining
into its caller, which was the failure mode I was most worried about.
Annotation ownership needed care. A member expression starts at the same
position as its object, so the comment in `/*#__PURE__*/ x().y` is found
by a lookup on either node. It belongs to the call, which already
consumes it, and claiming it for the read would drop `x()` — a real
miscompile I hit and fixed before it could land. The table only claims
an annotation when the object could not have taken it.
On a pattern the annotation carries more weight than on a call: it
asserts both that the reads are pure and that the initializer is not
nullish, since dropping the pattern also removes the `TypeError` that
destructuring `null` would throw. That is documented at the use site and
pinned by a fixture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>lukesandberg:lukesandberg/pure_annotations_on_property_access Latest Branches
-2%
lukesandberg:lukesandberg/pure_annotations_on_property_access 0%
kurovskyiii:fix/sync-file-bindings-return-an-error 0%
fix/parser-asserts-linebreak © 2026 CodSpeed Technology