mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
feat(codegen): add oxc-codegen package (#25488)
Add `oxc-codegen` package - a printer written in TS. ### Features - Prints JS, JSX, TS, TSX. - Matches `oxc_codegen` exactly on all Test262, TypeScript, Acorn-JSX fixtures. - Very fast. ### Deficiencies - Pretty-printing only (no compact/minified output). - No support for printing comments. - Source map support is semi-there, but largely untested, and poor API (design doc has some ideas on how to improve it). ### This PR adds - The printer code. - Conformance testing infra. - Further tests for edge cases. - README. - Design doc - how it differs from `oxc_codegen` and why, what produces the perf. ### This PR doesn't add - Not integrated into CI or release pipeline. ### Notes This started as a slop port of `oxc_codegen`. I've iterated on that base, cleaned it up a lot, and at least skimmed through all of it. I think it's in fairly decent shape. The design doc is I think worth a read. It explains the rationale for the choices, in particular the "1 codebase, 4 builds" approach. I've put it in a new top-level directory called `packages`. It's not an app, so didn't seem to me to fit alongside `apps/oxlint` and `apps/oxfmt`. It's pure JS, so not like e.g. `napi/parser`. And it felt too heavyweight to put into `npm` dir. But maybe there's a better place for it. Co-authored-by: Cameron <cameron.clark@hey.com>
This commit is contained in:
1 parent
73acba93fb
commit
a4478e960a
61 files changed
+10718
-4
No files matched your search
@@ -505,6 +505,7 @@ jobs:
|
||||
run: cargo clippy --all-features --profile dev-no-debug-assertions
|
||||
- run: pnpm --filter oxlint-app build-js
|
||||
- run: pnpm --filter oxfmt-app build-js
|
||||
- run: pnpm --filter oxc-codegen build
|
||||
- run: node --run lint
|
||||
|
||||
doc:
|
||||
@@ -566,6 +567,44 @@ jobs:
|
||||
just coverage
|
||||
git diff --exit-code
|
||||
|
||||
test-codegen:
|
||||
name: Test Codegen
|
||||
runs-on: namespace-profile-linux-x64-default
|
||||
steps:
|
||||
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
|
||||
|
||||
- name: Check if codegen tests should run
|
||||
id: filter
|
||||
uses: ./.github/actions/check-changes
|
||||
with:
|
||||
packages: oxc_codegen_conformance,oxc_parser_napi
|
||||
paths: .github/workflows/ci.yml,packages/codegen/,tasks/codegen_conformance/,napi/parser/,npm/oxc-types/,tasks/coverage/,pnpm-lock.yaml,pnpm-workspace.yaml,package.json
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
- uses: ./.github/actions/clone-submodules
|
||||
if: steps.filter.outputs.changed == 'true'
|
||||
with:
|
||||
node-compat-table: false
|
||||
|
||||
- uses: oxc-project/setup-node@d59b270414aa7d0eed947eed4eb5b0b8a675f01d # v1.4.0
|
||||
if: steps.filter.outputs.changed == 'true'
|
||||
|
||||
- uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
if: steps.filter.outputs.changed == 'true'
|
||||
with:
|
||||
path: /home/runner/.rustup
|
||||
cache: |
|
||||
rust
|
||||
pnpm
|
||||
|
||||
- name: Run codegen tests
|
||||
if: steps.filter.outputs.changed == 'true'
|
||||
run: |
|
||||
pnpm --filter oxc-codegen build-test
|
||||
pnpm --filter oxc-codegen test
|
||||
git diff --exit-code
|
||||
|
||||
minification:
|
||||
name: Minification Size
|
||||
runs-on: namespace-profile-linux-x64-default
|
||||
|
||||
@@ -10,6 +10,7 @@ target/
|
||||
/website/node_modules/
|
||||
/benchmark/node_modules/
|
||||
/tasks/benchmark/codspeed/node_modules/
|
||||
/tasks/codegen_conformance/node_modules/
|
||||
/tasks/transform_conformance/node_modules/
|
||||
/tasks/compat_data/node_modules/
|
||||
/tasks/compat_data/compat-table/
|
||||
|
||||
Generated
+16
@@ -1907,6 +1907,22 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_codegen_conformance"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_ast_visit",
|
||||
"oxc_codegen",
|
||||
"oxc_napi",
|
||||
"oxc_parser",
|
||||
"oxc_syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_compat"
|
||||
version = "0.144.0"
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"apps/oxfmt/src-js/config.generated.ts",
|
||||
"apps/oxlint/conformance/submodules/**",
|
||||
"napi/parser/src-js/bindings.js",
|
||||
"tasks/codegen_conformance/index.js",
|
||||
"tasks/codegen_conformance/index.d.ts",
|
||||
"napi/playground/playground.wasi-browser.js",
|
||||
"napi/{transform,transform-react,minify,playground}/index.js",
|
||||
"napi/{transform,transform-react,minify,playground}/index.d.ts",
|
||||
|
||||
+4
-4
@@ -2,11 +2,11 @@
|
||||
"name": "oxc",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" build",
|
||||
"build-dev": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" build-dev",
|
||||
"build-test": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" build-test",
|
||||
"build": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" --filter \"./packages/*\" build",
|
||||
"build-dev": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" --filter \"./packages/*\" build-dev",
|
||||
"build-test": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" --filter \"./packages/*\" build-test",
|
||||
"build-wasm-dev": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" build-wasm-dev",
|
||||
"test": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" test",
|
||||
"test": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" --filter \"./packages/*\" test",
|
||||
"fmt": "oxfmt -c oxfmtrc.jsonc",
|
||||
"lint": "oxlint -c oxlintrc.json --deny-warnings --report-unused-disable-directives"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
@@ -0,0 +1,620 @@
|
||||
# Design
|
||||
|
||||
Why `oxc-codegen` is shaped the way it is.
|
||||
|
||||
It is a port of Oxc's Rust [`oxc_codegen`] crate, and its output has to be byte-identical to it.
|
||||
But it is not a transliteration. A straight port would not take into account differences between Rust and JS,
|
||||
and would likely be several times slower.
|
||||
|
||||
This document is for someone about to change the printer, wondering which of its oddities are load bearing.
|
||||
Everything here is also documented at the point it applies. This is an overview.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Why not native?](#why-not-native)
|
||||
- [What this is](#what-this-is)
|
||||
- [What it is not at present](#what-it-is-not-at-present)
|
||||
- [`state.last`](#statelast)
|
||||
- [Four builds from one source tree](#four-builds-from-one-source-tree)
|
||||
- [Other things which make it fast](#other-things-which-make-it-fast)
|
||||
- [Where the ESTree AST differs](#where-the-estree-ast-differs)
|
||||
- [How it is tested](#how-it-is-tested)
|
||||
- [Rules to keep](#rules-to-keep)
|
||||
|
||||
## Why not native?
|
||||
|
||||
Oxc's other JS packages are bindings around a native library. This one deliberately is not.
|
||||
|
||||
The starting point was a hunch:
|
||||
|
||||
> **When the AST is already in JS, no native printer can beat a JS one.**
|
||||
|
||||
### The argument
|
||||
|
||||
- **Printing is cheap.** It is a linear walk of the AST, emitting strings. There is no parsing, no analysis,
|
||||
no search. It doesn't play to the strengths of a faster language the way parsing or minifying does.
|
||||
- **Crossing into native is not cheap.** A native printer cannot see a JS object graph.
|
||||
The whole AST has to be serialized across the boundary before it can print a single character.
|
||||
- **The boundary dominates.** The serialization is proportional to the size of the AST, just as the printing is -
|
||||
but it is work a JS printer can skip entirely. It is added to a total that was small to begin with.
|
||||
|
||||
The conclusion: A native printer loses the game before it's even started - still shovelling data over to native side,
|
||||
while the JS printer already finished work and has gone off to the pub.
|
||||
|
||||
### Where this applies
|
||||
|
||||
Only when the AST is **already** in JS - it came from `oxc-parser`, or a plugin built it, or something
|
||||
transformed it on JS side. That is the common use case this package is for.
|
||||
|
||||
It does **not** apply when you are printing source text you also parsed natively. Then stay on native side -
|
||||
the AST never has to leave native memory, and Rust `oxc_codegen` is the right tool. The two are complementary.
|
||||
|
||||
The benchmarks in the [README](./README.md) measure `oxc-codegen` over representative JS-held ASTs.
|
||||
|
||||
## What this is
|
||||
|
||||
A printer from an ESTree AST to source text, written in TypeScript.
|
||||
|
||||
### The output contract
|
||||
|
||||
Byte-identical to `oxc_codegen` with default options - tab indentation, double quotes, comments off.
|
||||
|
||||
The conformance suites check this on every fixture. It is what makes the two printers substitutable for one another.
|
||||
|
||||
## What it is not at present
|
||||
|
||||
`oxc_codegen` (Rust printer) is more capable than this one.
|
||||
|
||||
- No minify mode.
|
||||
- No support for comments.
|
||||
- No symbol mangling.
|
||||
- Source map support is implemented, but not well-tested.
|
||||
|
||||
Many of these features can be added in future, and we should be able to do so without _much_ impact on performance.
|
||||
|
||||
## `state.last`
|
||||
|
||||
This is the largest divergence from Rust, and most of the others follow from it.
|
||||
|
||||
### The problem
|
||||
|
||||
Two adjacent tokens can merge into a different token when written straight up against each other.
|
||||
`in` after an identifier becomes part of that identifier. `+` after `+` becomes `++`.
|
||||
|
||||
Deciding whether to insert a space means knowing what was written last.
|
||||
|
||||
### How Rust answers it
|
||||
|
||||
Rust just looks. Its output buffer is a `Vec<u8>`, so `code.last_byte()` is a pointer dereference - very cheap.
|
||||
|
||||
### Why this doesn't work in JS
|
||||
|
||||
**In JS, reading the last character is catastrophic for perf.** `state.output` is built by repeated `+=`,
|
||||
which V8 represents as a **cons string** - a tree of the pieces, never actually concatenated. Reading any
|
||||
character of it forces V8 to flatten that tree.
|
||||
|
||||
Do that after every append and the printer spends its time flattening ropes it is about to extend again.
|
||||
Result is a **4x slow-down**.
|
||||
|
||||
So `oxc-codegen` has to find a different solution. Every time it pushes a string to output, it also records
|
||||
in `state.last` what the output now ends with. `state.output` never needs to be read from.
|
||||
|
||||
### The answer: A category, not a character
|
||||
|
||||
It turns out the functions which decide whether spaces need to be inserted between keywords / identifiers
|
||||
don't actually need to know what the last character was, they only need to know what _category_ of character
|
||||
it was. i.e. It doesn't matter if the last character was `"a"` or `"Z"` or `"$"` - they're all equivalent.
|
||||
|
||||
Almost everywhere in the printer, it knows statically what _category_ it's writing, even if it doesn't know
|
||||
the exact bytes until runtime. e.g. When writing an identifier, it's the `IDENT` category.
|
||||
|
||||
```ts
|
||||
write(state, node.name, CAT_IDENT);
|
||||
```
|
||||
|
||||
This has 3 advantages over storing the last character:
|
||||
|
||||
1. Categories are just small integers, the cheapest data type there is.
|
||||
2. The categories are statically defined at call sites. No `node.name.at(-1)`.
|
||||
3. Checking the category of last write is cheap - `last === CAT_IDENT` not `isIdentifierPart(lastChar)`.
|
||||
|
||||
That one `last` field replaces all of this:
|
||||
|
||||
| Rust | Question it answers | JS |
|
||||
| :------------------------------------ | :---------------------------------- | :----------------------- |
|
||||
| `last_byte()` + `is_identifier_part` | Would a following identifier merge? | `CAT_IDENT` |
|
||||
| `last_byte() == Some(b'?')` | Would a following `?` make `??`? | `CAT_QUESTION` |
|
||||
| `peek_nth_byte_back(1) == Some(b'<')` | Is this `!` the `!` of a `<!`? | `CAT_OP_UN_NOT_AFTER_LT` |
|
||||
|
||||
### Extending this scheme to operators
|
||||
|
||||
Rust `oxc_codegen` also holds state about what operator was last written. It stores this info as a pair:
|
||||
|
||||
- `prev_op` - Last operator that was written
|
||||
- `prev_op_end` - Position of the end of that operator
|
||||
|
||||
Each time an operator is written, it sets these 2 fields. When other code wants to know "am I writing this straight
|
||||
after an operator, and which operator?" it:
|
||||
|
||||
1. Checks if `code.len() == prev_op_end`. If true, then yes, it's right after an operator.
|
||||
2. `prev_op` then says which operator.
|
||||
|
||||
In Rust printer, this makes sense. Checking these 2 fields, and comparing to `code.len()` costs 2 or 3 memory accesses,
|
||||
and 1 or 2 comparisons. But that's fine! It only reads or writes these 2 fields infrequently - only when an operator
|
||||
is being written. And operators are only a small fraction of code.
|
||||
|
||||
In JS codegen, we're in a different situation. We've been forced to update `state.last` after every write,
|
||||
to avoid the huge perf hit of reading the last character from `output`.
|
||||
|
||||
So we might as well use `last` to record operators too, and also some similar character classes:
|
||||
|
||||
| Rust | Question it answers | JS |
|
||||
| :-------------------------------------- | :------------------------------------------------------ | :---------------- |
|
||||
| `code.len() == prev_op_end` + `prev_op` | Which operator came last, and was it right before this? | `CAT_OP_*` |
|
||||
| `code.len() == need_space_before_dot` | Is space needed after digit? e.g. `0 .toExponential()` | `CAT_INT_DIGIT` |
|
||||
| `code.len() == prev_reg_exp_end` | Did a flagless regex just close? | `CAT_REGEX_SLASH` |
|
||||
|
||||
All these Rust state fields, and every read of the output buffer, collapse into one field in the JS printer.
|
||||
|
||||
It costs no write barrier to store (because categories are represented by "SMI" small integers), and one compare
|
||||
to test. Being touched by every single write, it is the hottest field in `State`, reliably in L1 cache,
|
||||
and probably often also benefits from fast store-to-load forwarding.
|
||||
|
||||
#### The `<!--` case, as an example
|
||||
|
||||
Rust decides whether a `!` is the `!` of a `<!--` hazard by peeking at the _second_-to-last byte,
|
||||
at the moment a `--` is about to be written.
|
||||
|
||||
Here the question is answered when the `!` is written, where the preceding character is already known,
|
||||
and the answer is baked into which category gets stored. The reader has nothing left to look up.
|
||||
|
||||
### Position marks
|
||||
|
||||
Similar to operators, Rust printer also maintains state about the _context_. For example:
|
||||
|
||||
```js
|
||||
// Function declaration
|
||||
export default function foo () {}
|
||||
// Function expression
|
||||
export default (function bar () {});
|
||||
```
|
||||
|
||||
These 2 are semantically different - `foo` is available in outer scope, `bar` is not.
|
||||
|
||||
When printing a `FunctionExpression`, print needs to know "am I the value of an `export default`?"
|
||||
to know whether it needs to wrap the function in parentheses.
|
||||
|
||||
Similar to operators, Rust printer stores this info in field `start_of_default_export`.
|
||||
When `code.len() == start_of_default_export` then, yes, what's currently being printed is the value of
|
||||
an `export default` statement.
|
||||
|
||||
Again, JS codegen uses `last` to store this info. Except here `last` doesn't describe what _was_ written,
|
||||
it says what the context is.
|
||||
|
||||
Note that the node which reads the mark need not be the direct child of the `export default`.
|
||||
In `export default (function bar() {})();` the function is the callee of a call expression, and the parens
|
||||
are still needed. It gets them because `printCallExpression` wrote nothing before descending into the callee,
|
||||
so the mark was still there when `printFunction` looked. Whatever is printed leftmost sees the mark,
|
||||
however deeply nested it is.
|
||||
|
||||
| Rust | Question it answers | JS |
|
||||
| :-------------------------------------- | :------------------------------------ | :---------------------------- |
|
||||
| `code.len() == start_of_stmt` | At the start of a statement? | `CAT_START_OF_STMT` |
|
||||
| `code.len() == start_of_arrow_expr` | At the start of a concise arrow body? | `CAT_START_OF_ARROW_EXPR` |
|
||||
| `code.len() == start_of_default_export` | At the start of an `export default`? | `CAT_START_OF_DEFAULT_EXPORT` |
|
||||
|
||||
The reason this works is that in every case the last character printed in these positions was a space, a line break,
|
||||
or the start of the file, which all fall into the `CAT_OTHER` category. So these values of `last` aren't ambiguous
|
||||
to any other code which judges what to do next based on what came last. They just treat these values as
|
||||
"last character written was something I don't need to do anything special about".
|
||||
|
||||
#### Why the substitution is safe
|
||||
|
||||
Two facts, both checked across every printer file:
|
||||
|
||||
1. **Nothing writes output on the way down from a mark to its reader.** Every path from a mark to a
|
||||
reader either writes nothing, or writes a real `(` - never a `writeNoLast`, which would leave the
|
||||
mark visible past output which should have killed it.
|
||||
2. **It can only ever add parentheses, never remove them.** A real `write` always grows the output
|
||||
(empty writes are illegal), so the set of things which kill a mark under categories is a strict
|
||||
subset of what killed the offsets.
|
||||
|
||||
So every possible failure is a spurious pair of parentheses - slightly longer output, but not _invalid_ output.
|
||||
The byte-for-byte conformance tests would catch this, but even if there are gaps in the conformance suite
|
||||
for crazy edge cases and a bug sneaks through, it can't produce invalid JavaScript.
|
||||
|
||||
### Why category numbering is load bearing
|
||||
|
||||
The full table is at the top of [`print/write.ts`] and is the authority.
|
||||
|
||||
Three properties are relied on. Adding a code without preserving them will silently space output wrongly.
|
||||
|
||||
1. **Identifier hazards are the lowest codes.** So `printSpaceBeforeIdentifier` is `last <= CAT_REGEX_SLASH` -
|
||||
one compare, no table, no branch tree. The operators `printSpaceBeforeOperator` must distinguish are the highest,
|
||||
for the same reason (`last >= CAT_OP_UN_NOT_AFTER_LT`).
|
||||
2. **The `CAT_START_OF_*` codes sit between those two ranges**, which is what makes both range checks
|
||||
treat them as "nothing to separate".
|
||||
3. **`CAT_START_OF_STMT` is odd, with the other two marks either side.** The five reader sites each ask
|
||||
about a _pair_ of marks, never all three, so both pairs fall out as one `|`-and-compare:
|
||||
|
||||
```
|
||||
(last | 1) === CAT_START_OF_STMT // Statement or `export default`
|
||||
((last - 1) | 1) === CAT_START_OF_STMT // Statement or concise arrow body
|
||||
```
|
||||
|
||||
Each property has an `if (DEBUG)` block which iterates every category and asserts the compare selects
|
||||
exactly the intended set and nothing else - in [`print/write.ts`] for the `|` identities, and in [`print/space.ts`]
|
||||
for the two range checks.
|
||||
|
||||
Renumber freely. The assertions will tell you what you broke, and cost release builds nothing.
|
||||
|
||||
**One deliberate exception**: `CAT_OP_UN_NOT` is an operator code but sits _below_ the operator range.
|
||||
No operator merges with a plain `!`, so storing it must not cost `printSpaceBeforeOperator` a call into its slow path.
|
||||
|
||||
### `writeNoLast`
|
||||
|
||||
Not every write needs to update `last`.
|
||||
|
||||
When one token is written in pieces - a string's opening quote, its contents, its closing quote -
|
||||
only the last piece's category can possibly be read. Hence four write primitives in [`print/write.ts`]:
|
||||
|
||||
| Function | Updates `last` | Records a source mapping |
|
||||
| :------------------- | :------------- | :----------------------- |
|
||||
| `write` | Yes | No |
|
||||
| `writeNoLast` | No | No |
|
||||
| `writeWithMap` | Yes | Yes |
|
||||
| `writeWithMapNoLast` | No | Yes |
|
||||
|
||||
- The `*NoLast` pair is used at 89 sites, against 596 for the other two.
|
||||
- The rule is exact: **only sound where the value of `last` is provably dead**.
|
||||
Another real write must follow before anything reads it.
|
||||
- JSX carries the longest runs. Printing `<a.b x="1">hi</a.b>` updates `last` exactly once, on the final `>`.
|
||||
- `writeNoLast` accepts an empty string, `write` does not.
|
||||
A `write` of `""` would leave `last` claiming a category for a character that was never written.
|
||||
|
||||
### How the invariants are enforced
|
||||
|
||||
Every rule above fails quietly - a missing space in one construct out of thousands. So three debug-only
|
||||
mechanisms guard them, all removed from release builds.
|
||||
|
||||
1. **`debugAssertCategoryMatches`** runs on every `write`, checking the category truthfully describes
|
||||
the final character written. It cannot be a simple lookup - a category encodes the merge hazard of
|
||||
the trailing _token_, not the trailing character - a digit legitimately ends both a `CAT_IDENT` write
|
||||
and a `CAT_INT_DIGIT` one - so each final character has a permitted set.
|
||||
2. **`lastIsStale` / `debugAssertLastFresh`** track the `writeNoLast` rule. `writeNoLast` sets the flag,
|
||||
the next real write clears it, and every reader of `last` calls `debugAssertLastFresh` first,
|
||||
to make sure they're not reading a stale value.
|
||||
An author who was wrong about `last` being dead gets a clear failure, not a wrongly spaced construct.
|
||||
3. **The numbering assertions** described above.
|
||||
|
||||
This is why the debug build is the one to run the conformance suites against - `pnpm run build-test`.
|
||||
|
||||
## Four builds from one source tree
|
||||
|
||||
`dist` holds the printer compiled four times, plus an entry point which picks between them.
|
||||
|
||||
| Build | `TS` | `SOURCEMAPS` | Size |
|
||||
| :----------------- | :---- | :----------- | ----: |
|
||||
| `print_js.js` | false | false | 25 KB |
|
||||
| `print_js_maps.js` | false | true | 26 KB |
|
||||
| `print_ts.js` | true | false | 39 KB |
|
||||
| `print_ts_maps.js` | true | true | 40 KB |
|
||||
|
||||
`index.ts` picks one from the caller's `ts` and `sourceMap` options and `require`s it on first use -
|
||||
`require` rather than `import()`, because `printSync` is synchronous.
|
||||
|
||||
A caller printing only JavaScript never loads, parses or compiles the TypeScript printers at all.
|
||||
|
||||
### Why separate TS and JS builds
|
||||
|
||||
**The obvious reason is code size and complexity.** JS-only builds have all TS-related code removed,
|
||||
as it's all gated behind compile-time `TS` constant. Every `if (TS && node.declare) { ... }` block is removed
|
||||
by minifier, and every `case "TSEnumDeclaration":` arm by the `strip_ts` TSDown plugin. With the arms gone,
|
||||
the TS-only printer functions lose their last references and are tree-shaken away. That is a third of the code.
|
||||
However, it has a surprisingly small impact on perf.
|
||||
|
||||
**The bigger reason is monomorphism.** `oxc-parser` produces differently-_shaped_ node objects for a
|
||||
JS-shaped AST and a TS-shaped one - the TS shape carries `optional`, `declare`, `accessibility`,
|
||||
`override` and the rest.
|
||||
|
||||
If both shapes flow through one `printFunction`, V8 sees two hidden classes at every property access,
|
||||
the inline caches go polymorphic, and the whole printer slows down.
|
||||
|
||||
Two builds means each one only ever sees one shape. JS printer sees only JS AST-shaped objects,
|
||||
TS printer sees only TS AST-shaped objects. This gives a perf boost of over 10%.
|
||||
|
||||
Additionally, the split helps branch prediction. If feeding a mix of JS and TS ASTs through a single printer,
|
||||
for a JS AST branches for "does this node have a type annotation?" always find the answer is "no".
|
||||
Branch predictor believes there's a pattern and when that code runs again, it expects no type annotation again.
|
||||
But this time, the AST is a TS AST, and the answer is "yes". Mispredict! Go back 100 instructions, go directly back,
|
||||
do not pass Go, do not collect $200!
|
||||
|
||||
With 2 different builds for JS and TS, this doesn't arise. In the JS build the branch isn't there at all -
|
||||
it was compiled away. The TS build only ever sees TS ASTs, so what the branch predictor learns stays true.
|
||||
|
||||
### Why `State` lives outside `print` directory
|
||||
|
||||
The same argument, one level up.
|
||||
|
||||
`State` is defined in `src-js/state.ts`, which is **not** part of any printer build. The printers import
|
||||
it as a type only; the entry point constructs it. So all four builds share one class, and therefore one
|
||||
hidden class, for the object they thread through every single function.
|
||||
|
||||
This is why **a build-time flag must never add or remove a field on `State`**.
|
||||
|
||||
- The source-map fields are present in every build, and set to `null` when unused.
|
||||
- The debug fields are present in every build, and only _initialised_ under `if (DEBUG)`.
|
||||
|
||||
Making either conditional would give each build a different object shape, undoing the whole arrangement.
|
||||
|
||||
### The build plugins
|
||||
|
||||
There are 4 TSDown plugins in `tsdown_plugins`. Order matters - `strip_ts` is a text transform,
|
||||
so it must run before the plugins which parse.
|
||||
|
||||
#### `strip_ts`
|
||||
|
||||
Deletes regions fenced with `/* IF TS */` ... `/* END_IF */` from JS-only builds.
|
||||
|
||||
They exist because a minifier can fold `if (TS)` away, but cannot remove a `switch` `case` arm on the grounds
|
||||
that its node type never occurs.
|
||||
|
||||
It fails the build if the fences are unbalanced - and also if it finds _no_ fences at all.
|
||||
A plugin silently doing nothing is a failure mode worth guarding against.
|
||||
|
||||
#### `unmap_writes`
|
||||
|
||||
Rewrites `writeWithMap(state, code, cat, node)` into `write(state, code, cat)` in non-sourcemap builds,
|
||||
drops the `node` argument, and rewrites the import to match.
|
||||
|
||||
Without it, the node argument would still be evaluated and held live across a call which ignores it.
|
||||
|
||||
It re-parses its own output and fails the build if anything still reaches a mapped write, or calls a name
|
||||
it did not bring into scope.
|
||||
|
||||
#### `const_functions`
|
||||
|
||||
Rewrites top-level `function foo(...) { ... }` into `const foo = (...) => { ... }`.
|
||||
|
||||
A function declaration creates a binding which could be assigned later, so every call has to read
|
||||
the current value out of module scope. A `const` cannot be, so V8 calls it directly.
|
||||
|
||||
- **Measured at ~3%** across all benchmark fixtures.
|
||||
- It really is the `const`. The same rewrite with `let` measures ~4% _slower_ than this, and slower than
|
||||
leaving the declarations alone.
|
||||
- The source keeps its declarations, because they hoist and read better. Which is why this is a build step
|
||||
rather than a house style.
|
||||
|
||||
#### `remove_asserts`
|
||||
|
||||
Removes `debugAssert` and `typeAssertIs` calls _and the expressions inside them_ from release builds.
|
||||
|
||||
The minifier can drop the calls on its own, but cannot prove `node.value` in `debugAssert(node.value !== null)`
|
||||
has no getter, so it would leave `node.value !== null` behind as a bare expression statement.
|
||||
|
||||
This is what gives us Rust `debug_assert!`-style checks in tests and conformance, with zero cost in release build.
|
||||
|
||||
## Other things which make it fast
|
||||
|
||||
### Popularity-ordered dispatch
|
||||
|
||||
`printExpression` (34 arms) and `printStatement` (33 arms) are megamorphic string switches.
|
||||
|
||||
V8 lowers a switch on strings to what amounts to an `if / else if` chain, so arm order is execution order.
|
||||
Both are ordered roughly by how often each node type occurs in real code - common variants like `Identifier`,
|
||||
`MemberExpression`, `CallExpression` first.
|
||||
|
||||
This sped up the printer by ~30%.
|
||||
|
||||
It also means **adding an arm near the top of either switch has a cost**, however rare the node type is.
|
||||
|
||||
Even after ordering `case` arms, these "dispatch" functions remain a large part of runtime (~25%).
|
||||
Much sweat and tokens were spilled trying to reduce it further:
|
||||
|
||||
- Perfect hash table to convert `node.type` to an integer ID.
|
||||
- `switch`-ing on an ID to reduce the switch to a jump table.
|
||||
- Branching on first character to narrow down the range - `switch (node.type.charCodeAt(0)) { case 69: ... }`.
|
||||
- Many others...
|
||||
|
||||
All to no effect. Arm ordering worked, nothing else did.
|
||||
|
||||
If anyone else can crack this, it's probably the single largest unrealized perf gain on the table.
|
||||
|
||||
### Monomorphism
|
||||
|
||||
As much as possible, call paths are structured so that functions receive the same object shape consistently.
|
||||
Once it has gathered type feedback from the first runs, V8 tiers up hot functions to TurboFan, which generates
|
||||
code along these lines (pseudo-code):
|
||||
|
||||
```ts
|
||||
function printThing(state, node): void {
|
||||
if (SHAPE_OF(node) !== K_EXPECTED_SHAPE) {
|
||||
DE_OPTIMIZE(printThing);
|
||||
return printThing(state, node);
|
||||
}
|
||||
|
||||
// Now it's known exactly what shape `node` is, it becomes static-typed like Rust.
|
||||
// Working with it is very cheap.
|
||||
|
||||
// Original code: `const { left } = node;`
|
||||
const left: Expression = GET_UNCHECKED(node, K_LEFT_OFFSET);
|
||||
// ... fast code from here on ...
|
||||
}
|
||||
```
|
||||
|
||||
In particular, it is **essential** that `State` remains the same shape _everywhere_. Every function touches it,
|
||||
so any inconsistency in shape would turn every access polymorphic and completely tank performance.
|
||||
|
||||
The obvious exception is the dispatch functions discussed above (`printExpression` etc), which are
|
||||
unavoidably megamorphic.
|
||||
|
||||
We have not done a full audit of all functions to check if any are receiving polymorphic/megamorphic inputs
|
||||
when that could be avoided. Such an audit might throw up potential improvements.
|
||||
|
||||
#### Moral of the story
|
||||
|
||||
Consistent object shapes are _really_ important for good perf in JS engines.
|
||||
|
||||
We should provide users with utility functions to construct AST nodes which produce the exact same object shape
|
||||
as what `oxc-parser` produces. This would speed up not just `oxc-codegen`, but also their own code that
|
||||
operates on ASTs.
|
||||
|
||||
```ts
|
||||
// Good
|
||||
const binary = oxc.binaryExpression(left, operator, right);
|
||||
// Bad - different field order, different object shape
|
||||
const binary = { left, right, operator };
|
||||
```
|
||||
|
||||
### Intended parser pairing
|
||||
|
||||
The printer is meant to be used with `oxc-parser` with `experimentalRawTransfer` enabled.
|
||||
|
||||
Raw transfer builds the AST with a JS deserializer rather than via JSON. Node `type` strings then come out
|
||||
as source literals, so those big `switch`es compare interned strings by pointer, rather than character by character.
|
||||
This gives a 10% - 30% perf boost versus a JSON AST.
|
||||
|
||||
### Fast paths
|
||||
|
||||
- Pretty mode always puts a single space either side of a binary or assignment operator, so the whole
|
||||
token is one constant string - `" + "`, `" ??= "` - and one `write`, instead of space-operator-space.
|
||||
It also makes the token-glue checks unobservable around those operators, so they are not made at all.
|
||||
See `PADDED_BIN_OPERATORS` in [`print/operators.ts`].
|
||||
- `printNonNegativeFloat` prints integers below 1000 straight from `String()`. They are the overwhelming
|
||||
majority, and three digits are too few for any shortening form to win.
|
||||
- `printString` tests the whole string against one regex of characters needing escapes, and appends it
|
||||
whole when there are none. The escaping loop is a separate function because it almost never runs.
|
||||
- `printSpaceBeforeOperator` is one compare, with its three-clause slow path split out so the common path
|
||||
stays small enough for V8 to inline.
|
||||
- `printLiteral` switches on `typeof value` first, identifying strings and numbers without touching the
|
||||
`regex` or `bigint` properties.
|
||||
- `printNumericLiteral` tests `value > 0 && value < Infinity` first, reaching the same branch the full
|
||||
chain would without the two builtin calls.
|
||||
|
||||
Each of these produced a small perf bump individually, ~10% gain in aggregate.
|
||||
|
||||
### Indent cache
|
||||
|
||||
`printIndent` reads a pre-built string for the current level out of an array.
|
||||
|
||||
- The cache is created once per _process_, alongside the module-level state in `state.ts`.
|
||||
Every `State` carries a reference to it. So indent strings one printer run has grown are there for the next.
|
||||
This cache is shared between JS and TS, sourcemap and no-sourcemap builds.
|
||||
- `growIndents` deliberately forces each new indent string flat (a throwaway `charCodeAt(0)`) before caching it,
|
||||
paying the flatten cost once rather than on every append.
|
||||
|
||||
### Source maps computed at the end
|
||||
|
||||
During printing, a mapped write records only an output _offset_ and the node's original position.
|
||||
|
||||
`emitMappings` then walks the output once at the end, counting newlines, to turn those offsets into
|
||||
generated line/column. Tracking a line and column throughout would cost every write in every build.
|
||||
|
||||
The `Mapping` objects handed to `addMapping` are reused across calls rather than allocated per mapping.
|
||||
|
||||
#### Potential future improvement
|
||||
|
||||
Producing source maps is exactly the kind of raw number-crunching that native code excels at.
|
||||
And, unlike the AST, it's just small integers - the kind of data that transfers across the JS-native
|
||||
boundary cheaply.
|
||||
|
||||
It might be worthwhile assembling all the offsets on JS side in an `Int32Array`, instead of a plain `Array`,
|
||||
and making a single JS-Rust-JS round-trip, with the sourcemap generation (VLQ encoding etc) happening on Rust side.
|
||||
|
||||
WASM might also be a good option for this.
|
||||
|
||||
### No allocation in the binary walk
|
||||
|
||||
`printBinaryish` walks the left spine of a binary/logical chain iteratively. `a + b + c + d` nests as deep
|
||||
as it is long, so recursion would put the stack at the mercy of the input.
|
||||
|
||||
Rust does the same, but with an explicit `Stack` on `Codegen`. Here the pending levels are threaded
|
||||
through a `parent` field on the visitor object, so a non-nested operand allocates nothing extra.
|
||||
|
||||
### Lookup tables are `__proto__: null`
|
||||
|
||||
So a property miss cannot walk up to `Object.prototype`.
|
||||
|
||||
## Where the ESTree AST differs
|
||||
|
||||
The two printers walk different ASTs, and some of the differences reach the output.
|
||||
|
||||
### Node types
|
||||
|
||||
- **One `Literal` node type** covers strings, numbers, booleans, `null`, regexes and bigints, where Rust
|
||||
has six separate types. `printLiteral` tells them apart from `typeof value` and the presence of `regex` / `bigint`.
|
||||
- **`#x in obj` arrives as a `BinaryExpression`** with a `PrivateIdentifier` on the left, not as its own node type.
|
||||
Hence the extra test in `printExpression`'s `BinaryExpression` arm.
|
||||
|
||||
### Parenthesized function expressions
|
||||
|
||||
Oxc's Rust AST records that a function expression was parenthesized in a `pife` flag, and prints the parentheses
|
||||
back from it.
|
||||
|
||||
ESTree has no such flag. The information is only there when the AST was parsed with `preserveParens`,
|
||||
as a `ParenthesizedExpression` wrapper. `printExpression`'s arm for that node re-emits the parentheses
|
||||
around a function or arrow, and is transparent around anything else.
|
||||
|
||||
This is why `printBinaryish` prints its operands **unstripped**. `withoutParens` is used for
|
||||
the structural type tests, but the node handed to `printExpression` keeps its wrapper,
|
||||
so that arm can run.
|
||||
|
||||
### Things ESTree cannot express
|
||||
|
||||
- `import "m";` versus `import {} from "m";`
|
||||
- An absent `with` clause versus an empty `with {}`
|
||||
- `assert {...}` versus `with {...}`
|
||||
- A rest parameter's decorators (a deficiency in `oxc-parser`, fixable, not blocked by ESTree spec)
|
||||
|
||||
The conformance harness normalizes the Rust AST down to what ESTree can express before printing,
|
||||
rather than expecting the JS side to reproduce information it was never given.
|
||||
See `Normalize` in `tasks/codegen_conformance/src/lib.rs`.
|
||||
|
||||
### Positions, and other producers
|
||||
|
||||
- **`loc`, not spans.** Oxc's AST records byte spans. Source mappings need line/column, which other
|
||||
producers put in `loc`. Mapped writes check the node for `loc` rather than checking the build.
|
||||
- **Acorn and TS-ESLint ASTs are accepted**, and differ from Oxc's in a handful of places
|
||||
(`TSMappedType`, `TSImportType`, `TSEnumDeclaration`, `TSModuleDeclaration`).
|
||||
[`print/types.ts`] holds the widened node types and documents each difference.
|
||||
|
||||
## How it is tested
|
||||
|
||||
`oxc-codegen` is tested against all Test262, Acorn-JSX, and TypeScript test cases - about 62,000 fixtures.
|
||||
|
||||
Every fixture is printed twice, and the two must agree byte for byte:
|
||||
|
||||
1. In Rust, via the `oxc-codegen-conformance` NAPI addon (`tasks/codegen_conformance`).
|
||||
2. In JS, by this package.
|
||||
|
||||
Both sides parse the same source text with the same `SourceType`, derived by the same function,
|
||||
so the only thing under test is the printing. Fixtures which do not parse cleanly are skipped,
|
||||
rather than passing quietly.
|
||||
|
||||
Every fixture is checked in **both `preserveParens` modes**. They are different paths through the printer -
|
||||
with the wrappers present it must decide what to re-emit, without them it must re-derive every parenthesis
|
||||
from precedence.
|
||||
|
||||
## Rules to keep
|
||||
|
||||
A checklist, all of it argued for above.
|
||||
|
||||
1. **Never read a character of `state.output`.**
|
||||
2. **A `write` must declare a category which truthfully describes its final character**, and must never be
|
||||
given an empty string.
|
||||
3. **Only use `writeNoLast` where `last` is provably dead.** A real write must follow before any reader runs.
|
||||
4. **Preserve the three numbering properties** when adding or renumbering a `CAT_*` code, and keep the
|
||||
`if (DEBUG)` assertions which check them.
|
||||
5. **Never let a build-time flag add or remove a field on `State`.**
|
||||
6. **Do not add an arm near the top of `printExpression`, `printStatement` or other dispatch functions**
|
||||
unless the node type really is that common.
|
||||
7. **Do not relax `const_functions` to `let`.**
|
||||
8. **Run the conformance suites against a debug build** (`pnpm run build-test`), so the assertions are live.
|
||||
9. **Benchmark against a release build** (`pnpm run bench` rebuilds one first). A debug build keeps
|
||||
`debugAssert` calls and does not represent shipped performance.
|
||||
|
||||
[`oxc_codegen`]: https://github.com/oxc-project/oxc/tree/main/crates/oxc_codegen
|
||||
[`print/write.ts`]: https://github.com/oxc-project/oxc/blob/main/packages/codegen/src-js/print/write.ts
|
||||
[`print/space.ts`]: https://github.com/oxc-project/oxc/blob/main/packages/codegen/src-js/print/space.ts
|
||||
[`print/operators.ts`]: https://github.com/oxc-project/oxc/blob/main/packages/codegen/src-js/print/operators.ts
|
||||
[`print/types.ts`]: https://github.com/oxc-project/oxc/blob/main/packages/codegen/src-js/print/types.ts
|
||||
@@ -0,0 +1,75 @@
|
||||
# oxc-codegen
|
||||
|
||||
A fast JavaScript code generator from an [ESTree](https://github.com/estree/estree)-compliant AST,
|
||||
written in TypeScript.
|
||||
|
||||
It is a faithful port of Oxc's Rust [`oxc_codegen`](https://github.com/oxc-project/oxc/tree/main/crates/oxc_codegen)
|
||||
crate (pretty-printing mode). Output is byte-identical to `oxc_codegen` with default options
|
||||
(tab indentation, double quotes, comments off).
|
||||
|
||||
Unlike the rest of Oxc's JS packages, this package contains no Rust and no native bindings.
|
||||
It is pure JavaScript, and consumes an AST which already lives on the JS side, so doesn't need
|
||||
to serialize it across a JS/native boundary. This is the key to `oxc-codegen`'s speed -
|
||||
along with many optimizations to hit JS engines' fast paths.
|
||||
|
||||
See [DESIGN.md](https://github.com/oxc-project/oxc/blob/main/packages/codegen/DESIGN.md) for more details
|
||||
on the implementation, and what makes it fast.
|
||||
|
||||
100% tests passing against Test262, Acorn-JSX, and TypeScript conformance suites (62,000 tests in total).
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { parseSync } from "oxc-parser";
|
||||
import { printSync } from "oxc-codegen";
|
||||
|
||||
const { program } = parseSync("foo.js", "let x = 1");
|
||||
console.log(printSync(program));
|
||||
```
|
||||
|
||||
## Supported language variants
|
||||
|
||||
- JS
|
||||
- JSX
|
||||
- TS
|
||||
- TSX
|
||||
|
||||
## API
|
||||
|
||||
### `printSync(node, options?)`
|
||||
|
||||
Returns a string containing the code for the AST `node`.
|
||||
|
||||
`node` must be a whole AST (`Program` node) or a statement node.
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| :-------------------- | :------------------- | :------ | :---------------------------------------------- |
|
||||
| `indent` | `string` | `"\t"` | Indentation - spaces and/or tabs only |
|
||||
| `startingIndentLevel` | `number` | `0` | Indent level to start from |
|
||||
| `jsx` | `boolean` | `false` | `.tsx` mode - lone type parameters print `<T,>` |
|
||||
| `ts` | `boolean` | `false` | AST may contain TypeScript syntax |
|
||||
| `sourceMap` | `SourceMapGenerator` | - | If present, source mappings are emitted into it |
|
||||
|
||||
## Missing features
|
||||
|
||||
- There is currently no support for printing comments.
|
||||
- Pretty-printing only, no compact/minified output.
|
||||
- Source map support is only lightly tested, and API is likely to change.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
| fixture | bytes | Oxc |
|
||||
| :------------------------- | ------: | --------: |
|
||||
| tiny.js | 26 | 0.0001ms |
|
||||
| RadixUIAdoptionSection.jsx | 2518 | 0.0033ms |
|
||||
| react.development.js | 72141 | 0.1138ms |
|
||||
| binder.ts | 193077 | 0.2472ms |
|
||||
| App.tsx | 415340 | 0.7490ms |
|
||||
| lodash.js | 544096 | 0.4995ms |
|
||||
| kitchen-sink.tsx | 732222 | 2.5682ms |
|
||||
| antd.js | 6683633 | 11.3914ms |
|
||||
|
||||
Numbers above are from one machine and are not a regression baseline - the `antd.js` figures in
|
||||
particular move by over 10% run to run.
|
||||
@@ -0,0 +1,107 @@
|
||||
// Benchmarks. Run with `pnpm run bench`.
|
||||
//
|
||||
// Source maps are not measured. `printSync` is given no `sourceMap`, so it selects the builds
|
||||
// compiled without source map support, which is what most callers use.
|
||||
//
|
||||
// Imports from `dist`, which must hold a RELEASE build. `pnpm run build-dev` replaces `dist` with a debug build,
|
||||
// which keeps `debugAssert` calls live and does not represent shipped performance.
|
||||
// `pnpm run bench` builds a release build first, so benchmarking that way is always correct.
|
||||
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join as pathJoin } from "node:path";
|
||||
import { parseSync } from "oxc-parser";
|
||||
import { bench, describe } from "vitest";
|
||||
|
||||
import { printSync as oxcPrintSync } from "./dist/index.js";
|
||||
|
||||
// `TestFiles::minimal()` from `tasks/common/src/test_file.rs`, which the Rust `codegen` benchmark
|
||||
// uses, plus 2 larger pure-JS files from `TestFiles::minifier()` for throughput on real bundles.
|
||||
const FIXTURE_URLS = [
|
||||
"https://cdn.jsdelivr.net/gh/oxc-project/benchmark-files@main/RadixUIAdoptionSection.jsx",
|
||||
"https://cdn.jsdelivr.net/npm/react@17.0.2/cjs/react.development.js",
|
||||
"https://cdn.jsdelivr.net/gh/excalidraw/excalidraw@f6d85bc80fe328e8f472636eb0d541f7bb891aa0/packages/excalidraw/components/App.tsx",
|
||||
"https://cdn.jsdelivr.net/gh/microsoft/TypeScript@v5.3.3/src/compiler/binder.ts",
|
||||
"https://cdn.jsdelivr.net/gh/oxc-project/benchmark-files@ac5609a8fe9ae8d1a1de0f2ef251d562630c77e0/kitchen-sink.tsx",
|
||||
"https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.js",
|
||||
"https://cdn.jsdelivr.net/npm/antd@4.16.1/dist/antd.js",
|
||||
];
|
||||
|
||||
// Same directory the Rust benchmarks download to, so nothing is downloaded twice
|
||||
const CACHE_DIR_PATH = pathJoin(import.meta.dirname, "../../target");
|
||||
|
||||
// `experimentalRawTransfer` is not in the published `ParserOptions` type - it is experimental
|
||||
// and untyped. Declaring the object here rather than inline at the call site keeps TypeScript's
|
||||
// excess property check (which only applies to object literals) from rejecting it, without a cast.
|
||||
//
|
||||
// `preserveParens: false` because this printer deliberately does not support the redundant
|
||||
// `ParenthesizedExpression` / `TSParenthesizedType` nodes.
|
||||
//
|
||||
// `experimentalRawTransfer` builds the AST with the JS deserializer rather than via JSON,
|
||||
// which is the pairing this package is meant to be used in - node `type` strings come out
|
||||
// as source literals, so the printer's type switches compare pointers rather than characters.
|
||||
const PARSE_OPTIONS = {
|
||||
preserveParens: false,
|
||||
experimentalRawTransfer: true,
|
||||
};
|
||||
|
||||
// Timings, raised well above Vitest's defaults of 500ms timed and 100ms warmup.
|
||||
//
|
||||
// The defaults leave enough run-to-run variance to swamp a 1-2% change,
|
||||
// which is the size of win a printer optimization typically produces.
|
||||
// The long warmup matters just as much as the long run:
|
||||
// V8 has to tier the printer up to optimized code before any of the timed iterations start,
|
||||
// or the first results measure the interpreter instead.
|
||||
const BENCH_OPTIONS = { time: 3000, warmupTime: 1000, iterations: 50, warmupIterations: 20 };
|
||||
|
||||
/**
|
||||
* Load a fixture, downloading it and caching it on disk if it isn't there already.
|
||||
*
|
||||
* @param url - URL to download the fixture from
|
||||
* @returns Fixture's filename and source text
|
||||
*/
|
||||
async function loadFixture(url: string): Promise<{ filename: string; code: string }> {
|
||||
const filename = url.split("/").at(-1)!,
|
||||
path = pathJoin(CACHE_DIR_PATH, filename);
|
||||
|
||||
let code: string;
|
||||
try {
|
||||
code = await readFile(path, "utf8");
|
||||
} catch {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to download ${url}: ${res.status}`);
|
||||
code = await res.text();
|
||||
await mkdir(CACHE_DIR_PATH, { recursive: true });
|
||||
await writeFile(path, code);
|
||||
}
|
||||
|
||||
return { filename, code };
|
||||
}
|
||||
|
||||
// `tiny.js` is one line long, so its result is a comparison of the two printers' fixed overhead
|
||||
// rather than of their throughput. Fixtures are ordered by size.
|
||||
const fixtures = [
|
||||
{ filename: "tiny.js", code: "export default printSync;\n" },
|
||||
...(await Promise.all(FIXTURE_URLS.map(loadFixture))),
|
||||
];
|
||||
fixtures.sort((fixture1, fixture2) => fixture1.code.length - fixture2.code.length);
|
||||
|
||||
for (const { filename, code } of fixtures) {
|
||||
const { program, errors } = parseSync(filename, code, PARSE_OPTIONS);
|
||||
if (errors.length > 0) throw new Error(`Failed to parse ${filename}: ${errors[0].message}`);
|
||||
|
||||
// `.tsx`/`.jsx` fixtures need JSX-safe printing of type parameter lists (`<T,>`)
|
||||
const options = { jsx: filename.endsWith("x"), ts: /\.tsx?$/.test(filename) };
|
||||
|
||||
// Print once before timing, so a benchmark can never measure the printer bailing out.
|
||||
oxcPrintSync(program, options);
|
||||
|
||||
describe(`${filename} (${code.length} bytes)`, () => {
|
||||
bench(
|
||||
"oxc-codegen",
|
||||
() => {
|
||||
oxcPrintSync(program, options);
|
||||
},
|
||||
BENCH_OPTIONS,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "oxc-codegen",
|
||||
"version": "0.144.0",
|
||||
"description": "Fast JavaScript code generator from an ESTree-compliant AST",
|
||||
"keywords": [
|
||||
"ast",
|
||||
"code generator",
|
||||
"codegen",
|
||||
"estree",
|
||||
"javascript",
|
||||
"oxc",
|
||||
"typescript"
|
||||
],
|
||||
"homepage": "https://oxc.rs",
|
||||
"bugs": "https://github.com/oxc-project/oxc/issues",
|
||||
"license": "MIT",
|
||||
"author": "Boshen and oxc contributors",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/oxc-project/oxc.git",
|
||||
"directory": "packages/codegen"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/build.ts",
|
||||
"build-dev": "cross-env DEBUG=true node scripts/build.ts",
|
||||
"build-test": "pnpm run build-dev && pnpm --filter oxc-parser run build-test && pnpm --filter oxc-codegen-conformance run build",
|
||||
"test": "vitest run --dir ./test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"cross-env": "catalog:",
|
||||
"oxc-codegen-conformance": "workspace:*",
|
||||
"oxc-parser": "workspace:*",
|
||||
"rolldown": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Build script.
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { rmSync } from "node:fs";
|
||||
import { join as pathJoin } from "node:path";
|
||||
|
||||
const packageDirPath = pathJoin(import.meta.dirname, "..");
|
||||
|
||||
// Delete `dist` directory.
|
||||
// TSDown's `clean` option can't do it, because both builds output to the same directory,
|
||||
// so whichever runs second would delete the other's output.
|
||||
rmSync(pathJoin(packageDirPath, "dist"), { recursive: true, force: true });
|
||||
|
||||
// Build both flavours with TSDown
|
||||
execSync("pnpm tsdown", { stdio: "inherit", cwd: packageDirPath });
|
||||
@@ -0,0 +1,47 @@
|
||||
// Assertions.
|
||||
//
|
||||
// Both of these are compiled away - `tsdown_plugins/remove_asserts.ts` removes every call to them,
|
||||
// and the expressions inside it, so neither costs a release build anything at all.
|
||||
|
||||
/**
|
||||
* Assert a value is of a certain type.
|
||||
*
|
||||
* Has no runtime effect - only for guiding the type-checker.
|
||||
* Minification removes this function and all calls to it, so it has zero runtime cost.
|
||||
*
|
||||
* @param value - Value
|
||||
*/
|
||||
// oxlint-disable-next-line no-unused-vars
|
||||
export function typeAssertIs<T>(value: unknown): asserts value is T {}
|
||||
|
||||
/**
|
||||
* Assert a condition.
|
||||
*
|
||||
* In release builds, is a no-op. Only does runtime checks in debug builds.
|
||||
* Minification removes this function and all calls to it in release builds, so it has zero runtime cost.
|
||||
*
|
||||
* Use this for testing conditions which would indicate a bug in the code.
|
||||
* Do NOT use this for validating user input.
|
||||
*
|
||||
* If creating the error message is expensive, or potentially creating the message itself can result in an error
|
||||
* when the assertion passes, pass a function which returns the message.
|
||||
*
|
||||
* ```ts
|
||||
* debugAssert(condition, () => `Condition failed: ${getErrorMessage()}`);
|
||||
* ```
|
||||
*
|
||||
* @param condition - Condition which is expected to be `true`
|
||||
* @param message - Message to include in error if condition is `false`,
|
||||
* or a function which returns the message to include in error if condition is `false` (optional).
|
||||
*/
|
||||
export function debugAssert(
|
||||
condition: boolean,
|
||||
message?: string | (() => string),
|
||||
): asserts condition {
|
||||
if (!DEBUG) return;
|
||||
|
||||
if (!condition) {
|
||||
if (typeof message === "function") message = message();
|
||||
throw new Error(message ?? "Assertion failed");
|
||||
}
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
// Global constants defined at build time by TSDown.
|
||||
|
||||
// `true` if build emits source maps
|
||||
declare const SOURCEMAPS: boolean;
|
||||
|
||||
// `true` if build supports TypeScript syntax.
|
||||
// In builds where it's `false`, `tsdown_plugins/strip_ts.ts` also removes the switch arms
|
||||
// fenced with `/* IF TS */` / `/* END_IF */` comments.
|
||||
declare const TS: boolean;
|
||||
|
||||
// `true` if is debug build.
|
||||
// Built with `pnpm run build-dev`, which sets `DEBUG=true`. Debug builds keep `debugAssert` calls
|
||||
// (see `src/asserts.ts`). Release builds strip them in `tsdown_plugins/remove_asserts.ts`.
|
||||
declare const DEBUG: boolean;
|
||||
@@ -0,0 +1,85 @@
|
||||
// Entry point. Dispatches to whichever build of the printer suits the options it's given.
|
||||
//
|
||||
// `DESIGN.md` in the package root is the overview of the whole printer - why a JS printer beats
|
||||
// a native one here, where this port diverges from Rust `oxc_codegen` (notably `state.last`),
|
||||
// and what makes it fast. Read it before changing anything below, or in `print`.
|
||||
//
|
||||
// The printer is built 4 times from `print/`, over 2 build-time feature flags.
|
||||
//
|
||||
// 1. Source map support - costs a little speed even when it's unused.
|
||||
// 2. TypeScript support - costs JS-only ASTs a swathe of field checks and switch arms.
|
||||
//
|
||||
// The most important effect of separate JS and TS builds is not the reduced logic when printing a JS AST,
|
||||
// but that they receive differently-shaped AST node objects. If both JS-shape and TS-shape ASTs
|
||||
// pass through the same functions, every function becomes polymorphic, which is a large slow-down.
|
||||
// Having specialized code paths for JS and TS-shape ASTs avoids this problem - most functions remain monomorphic.
|
||||
//
|
||||
// Each combination is its own build, lazy-loaded depending on the `sourceMap` and `ts` options.
|
||||
//
|
||||
// `require` rather than `import()` because the printer is synchronous.
|
||||
// All builds are ESM, which `require` can load on the Node versions in this package's `engines` field.
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import { State } from "./state.ts";
|
||||
|
||||
import type { Options } from "./print/options.ts";
|
||||
import type * as ESTree from "../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
export type { Mapping, Options, Position, SourceMapGenerator } from "./print/options.ts";
|
||||
|
||||
/**
|
||||
* All printer builds are compiled from `print/index.ts`, so they share its exports.
|
||||
*/
|
||||
type PrintModule = typeof import("./print/index.ts");
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Passed on when the caller supplies no options, so the printer never has to check for their absence.
|
||||
*/
|
||||
const EMPTY_OPTIONS: Options = {};
|
||||
|
||||
/**
|
||||
* Printer builds, indexed by `(ts ? 1 : 0) + (sourceMap ? 2 : 0)`. Lazily loaded as needed.
|
||||
*/
|
||||
const PRINTER_PATHS = [
|
||||
"./print_js.js",
|
||||
"./print_ts.js",
|
||||
"./print_js_maps.js",
|
||||
"./print_ts_maps.js",
|
||||
];
|
||||
|
||||
/**
|
||||
* The loaded builds, in the same order as `PRINTER_PATHS`, and `null` until first used.
|
||||
*/
|
||||
const printers: (PrintModule["printSync"] | null)[] = [null, null, null, null];
|
||||
|
||||
/**
|
||||
* Print `node`, returning the generated code.
|
||||
*
|
||||
* @param node - AST node to print, a `Program` or a single statement
|
||||
* @param options - Printing options (optional)
|
||||
* @returns Generated code
|
||||
*/
|
||||
export function printSync(node: ESTree.Node, options?: Options): string {
|
||||
// The printer is built 4 times, over whether the AST may contain TypeScript and whether
|
||||
// source mappings are wanted. This picks the build the options call for and loads it on first use,
|
||||
// so a caller printing only JavaScript never pays for the TypeScript printers.
|
||||
let index = 0;
|
||||
if (options == null) {
|
||||
options = EMPTY_OPTIONS;
|
||||
} else {
|
||||
if (options.ts === true) index = 1;
|
||||
if (options.sourceMap != null) index |= 2;
|
||||
}
|
||||
|
||||
let print = printers[index];
|
||||
if (print === null) {
|
||||
print = (require(PRINTER_PATHS[index]) as PrintModule).printSync;
|
||||
printers[index] = print;
|
||||
}
|
||||
|
||||
// State is created here, not in the printer, so that all 4 builds share one class
|
||||
// and therefore see one object shape
|
||||
return print(node, new State(options), options);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Assignment targets.
|
||||
|
||||
import { typeAssertIs } from "../asserts.ts";
|
||||
import { printPropertyKey } from "./binding_pattern.ts";
|
||||
import { CAT_IDENT, CAT_OTHER, write, writeWithMap } from "./write.ts";
|
||||
import { printExpression, printMemberExpression } from "./expression.ts";
|
||||
import { printSpaceBeforeIdentifier } from "./space.ts";
|
||||
import { CTX_NONE } from "./operators.ts";
|
||||
import { PREC_COMMA, PREC_COMPARE, PREC_PREFIX } from "./precedence.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Print the left hand side of an assignment.
|
||||
*
|
||||
* Destructuring targets look like object and array literals but are a separate set of node types,
|
||||
* with their own rules - which is why they are printed here rather than by the expression printer.
|
||||
*/
|
||||
export function printAssignmentTarget(node: ESTree.AssignmentTarget, state: State): void {
|
||||
switch (node.type) {
|
||||
// A simple target is only ever a name, a member access, or (in TS) one of the assertion expressions below.
|
||||
// For speed, the two common ones are printed here instead of through the expression printer's own dispatch.
|
||||
case "Identifier":
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, node.name, CAT_IDENT, node);
|
||||
break;
|
||||
case "MemberExpression":
|
||||
printMemberExpression(node, state, CTX_NONE);
|
||||
break;
|
||||
case "ObjectPattern":
|
||||
printObjectAssignmentTarget(node, state);
|
||||
break;
|
||||
case "ArrayPattern":
|
||||
printArrayAssignmentTarget(node, state);
|
||||
break;
|
||||
/* IF TS */
|
||||
case "TSAsExpression":
|
||||
case "TSSatisfiesExpression":
|
||||
// `(x as T) = y` - the target must be parenthesized
|
||||
printExpression(node, state, PREC_COMPARE, CTX_NONE);
|
||||
break;
|
||||
case "TSTypeAssertion":
|
||||
printExpression(node, state, PREC_PREFIX, CTX_NONE);
|
||||
break;
|
||||
/* END_IF */
|
||||
default:
|
||||
printExpression(node, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an object destructuring target, as in `({ a, b: c } = obj)`.
|
||||
*/
|
||||
function printObjectAssignmentTarget(node: ESTree.ObjectAssignmentTarget, state: State): void {
|
||||
writeWithMap(state, "{", CAT_OTHER, node);
|
||||
|
||||
const { properties } = node;
|
||||
const { length } = properties;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
|
||||
const property = properties[i];
|
||||
if (property.type === "RestElement") {
|
||||
write(state, "...", CAT_OTHER);
|
||||
printAssignmentTarget(property.argument, state);
|
||||
} else {
|
||||
printAssignmentTargetProperty(property, state);
|
||||
}
|
||||
}
|
||||
|
||||
write(state, "}", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print one property of an object destructuring target, in shorthand where the AST says so.
|
||||
*/
|
||||
function printAssignmentTargetProperty(node: ESTree.AssignmentTargetProperty, state: State): void {
|
||||
// Shorthand from the AST flag (assignment targets are not re-derived).
|
||||
if (node.shorthand) {
|
||||
// `{ a }` or `{ a = 1 }`
|
||||
const { value } = node;
|
||||
if (value.type === "AssignmentPattern") {
|
||||
typeAssertIs<ESTree.IdentifierReference>(value.left);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, value.left.name, CAT_IDENT, value.left);
|
||||
write(state, " = ", CAT_OTHER);
|
||||
printExpression(value.right, state, PREC_COMMA, CTX_NONE);
|
||||
} else {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, value.name, CAT_IDENT, value);
|
||||
}
|
||||
} else {
|
||||
const { key } = node;
|
||||
|
||||
if (node.computed) {
|
||||
write(state, "[", CAT_OTHER);
|
||||
typeAssertIs<ESTree.Expression>(key);
|
||||
printExpression(key, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, "]", CAT_OTHER);
|
||||
} else {
|
||||
printPropertyKey(key, state);
|
||||
}
|
||||
|
||||
write(state, ": ", CAT_OTHER);
|
||||
printAssignmentTargetMaybeDefault(node.value, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a destructuring target which may carry a default, as the `a = 1` in `({ a = 1 } = obj)`.
|
||||
*/
|
||||
function printAssignmentTargetMaybeDefault(
|
||||
node: ESTree.AssignmentTargetMaybeDefault,
|
||||
state: State,
|
||||
): void {
|
||||
if (node.type === "AssignmentPattern") {
|
||||
printAssignmentTarget(node.left, state);
|
||||
write(state, " = ", CAT_OTHER);
|
||||
printExpression(node.right, state, PREC_COMMA, CTX_NONE);
|
||||
} else {
|
||||
printAssignmentTarget(node, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an array destructuring target, holes and rest element included.
|
||||
*/
|
||||
function printArrayAssignmentTarget(node: ESTree.ArrayAssignmentTarget, state: State): void {
|
||||
const { elements } = node;
|
||||
let { length } = elements;
|
||||
|
||||
let rest: ESTree.BindingRestElement | ESTree.AssignmentTargetRest | null = null;
|
||||
if (length > 0) {
|
||||
const lastElement = elements[length - 1];
|
||||
if (lastElement != null && lastElement.type === "RestElement") {
|
||||
rest = lastElement;
|
||||
length--;
|
||||
}
|
||||
}
|
||||
|
||||
writeWithMap(state, "[", CAT_OTHER, node);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i !== 0) write(state, ", ", CAT_OTHER);
|
||||
const element = elements[i];
|
||||
if (element != null) {
|
||||
typeAssertIs<ESTree.AssignmentTargetMaybeDefault>(element);
|
||||
printAssignmentTargetMaybeDefault(element, state);
|
||||
}
|
||||
if (i === length - 1 && (element == null || rest !== null)) {
|
||||
write(state, ",", CAT_OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
if (rest !== null) {
|
||||
if (length > 0) write(state, " ", CAT_OTHER);
|
||||
write(state, "...", CAT_OTHER);
|
||||
printAssignmentTarget(rest.argument, state);
|
||||
}
|
||||
|
||||
write(state, "]", CAT_OTHER);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Binary/logical expressions (port of `binary_expr_visitor.rs`).
|
||||
|
||||
import { typeAssertIs } from "../asserts.ts";
|
||||
import { CAT_OTHER, write } from "./write.ts";
|
||||
import { printPrivateInExpression, printExpression } from "./expression.ts";
|
||||
import { BIN_PRECEDENCE, CTX_FORBID_IN, PADDED_BIN_OPERATORS } from "./operators.ts";
|
||||
import { withoutParens } from "./parens.ts";
|
||||
import { PREC_CALL, PREC_EXPONENTIATION, PREC_LOWEST, PREC_PREFIX } from "./precedence.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type { LiteralExtras } from "./types.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* One level of the binary/logical expression chain.
|
||||
*
|
||||
* `parent` is the level above, which is waiting for this one to finish before printing
|
||||
* its own operator and right operand.
|
||||
*/
|
||||
interface BinaryVisitor {
|
||||
e: ESTree.BinaryExpression | ESTree.LogicalExpression;
|
||||
precedence: number;
|
||||
ctx: number;
|
||||
leftPrecedence: number;
|
||||
operator: ESTree.BinaryOperator | ESTree.LogicalOperator;
|
||||
wrap: boolean;
|
||||
rightPrecedence: number;
|
||||
parent: BinaryVisitor | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a binary or logical expression, and its whole left-leaning chain, without recursing.
|
||||
*
|
||||
* `a + b + c + d` nests to the left as deep as it is long, so recursion would put the stack
|
||||
* at the mercy of the input. This walks down the left spine iteratively instead, and unwinds
|
||||
* through each level's `parent` to print the operators and right operands on the way back up.
|
||||
*
|
||||
* @param precedence - Precedence of the position this expression sits in, deciding parenthesisation
|
||||
* @param ctx - Context flags, carrying whether `in` is forbidden and calls are
|
||||
*/
|
||||
export function printBinaryish(
|
||||
node: ESTree.BinaryExpression | ESTree.LogicalExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
// The pending outer levels are threaded through `parent` rather than a separate stack array,
|
||||
// so a non-nested operand allocates nothing extra
|
||||
let v: BinaryVisitor | null = {
|
||||
e: node,
|
||||
precedence,
|
||||
ctx,
|
||||
leftPrecedence: PREC_LOWEST,
|
||||
operator: node.operator,
|
||||
wrap: false,
|
||||
rightPrecedence: PREC_LOWEST,
|
||||
parent: null,
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
binCheckAndPrepare(v, state);
|
||||
|
||||
const left = withoutParens(v.e.left);
|
||||
if (left.type === "BinaryExpression" || left.type === "LogicalExpression") {
|
||||
if (left.type === "BinaryExpression" && left.left.type === "PrivateIdentifier") {
|
||||
// Private-in expression as the left operand
|
||||
typeAssertIs<ESTree.PrivateInExpression>(left);
|
||||
printPrivateInExpression(left, state, PREC_LOWEST);
|
||||
binVisitRightAndFinish(v, state);
|
||||
break;
|
||||
}
|
||||
|
||||
typeAssertIs<ESTree.BinaryExpression | ESTree.LogicalExpression>(left);
|
||||
|
||||
v = {
|
||||
e: left,
|
||||
precedence: v.leftPrecedence,
|
||||
ctx: v.ctx,
|
||||
leftPrecedence: PREC_LOWEST,
|
||||
operator: v.operator,
|
||||
wrap: false,
|
||||
rightPrecedence: PREC_LOWEST,
|
||||
parent: v,
|
||||
};
|
||||
} else {
|
||||
// `v.e.left` prints, not the unwrapped `left` - a `ParenthesizedExpression` around a function
|
||||
// expression is how Oxc's `pife` flag reaches this printer, and the arm for it in `printExpression`
|
||||
// writes those parens back. Around anything else the wrapper is transparent, forwarding
|
||||
// this precedence and `ctx` unchanged.
|
||||
printExpression(v.e.left, state, v.leftPrecedence, v.ctx);
|
||||
binVisitRightAndFinish(v, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while ((v = v.parent) !== null) {
|
||||
binVisitRightAndFinish(v, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Work out whether one level of the chain needs parentheses, write the opening one if so,
|
||||
* and settle the precedences its two operands are printed at.
|
||||
*
|
||||
* `**` is right associative, so its left operand binds tighter, and the rest are the other way round.
|
||||
* `??` may not sit unparenthesized beside `&&` or `||`, which is why either operand can be forced up to `PREC_PREFIX`.
|
||||
*/
|
||||
function binCheckAndPrepare(v: BinaryVisitor, state: State): void {
|
||||
const { e } = v;
|
||||
const eOperator = e.operator;
|
||||
const ePrecedence = BIN_PRECEDENCE[eOperator];
|
||||
|
||||
// No parens if both sides use the same logical operator
|
||||
const precedenceCheck =
|
||||
v.precedence >= ePrecedence &&
|
||||
(!isLogicalOperator(v.operator) || v.precedence !== BIN_PRECEDENCE[v.operator]);
|
||||
v.operator = eOperator;
|
||||
v.wrap = precedenceCheck || (eOperator === "in" && (v.ctx & CTX_FORBID_IN) !== 0);
|
||||
|
||||
if (v.wrap) {
|
||||
write(state, "(", CAT_OTHER);
|
||||
v.ctx &= ~CTX_FORBID_IN;
|
||||
}
|
||||
// One level below the operator's own precedence. The precedence scale has no gaps, so this is
|
||||
// subtraction rather than a second table - `BinaryOperator::lower_precedence` in `oxc_syntax`
|
||||
// names the adjacent variant for each operator, which comes to the same thing.
|
||||
const lower = ePrecedence - 1;
|
||||
v.leftPrecedence = lower;
|
||||
v.rightPrecedence = lower;
|
||||
|
||||
if (ePrecedence === PREC_EXPONENTIATION) {
|
||||
// Right-associative
|
||||
v.leftPrecedence = ePrecedence;
|
||||
} else {
|
||||
// All other binary/logical operators are left-associative
|
||||
v.rightPrecedence = ePrecedence;
|
||||
}
|
||||
|
||||
if (eOperator === "??") {
|
||||
// Nullish coalescing cannot mix with && / || unparenthesized
|
||||
const left = withoutParens(e.left);
|
||||
if (left.type === "LogicalExpression" && left.operator !== "??") {
|
||||
v.leftPrecedence = PREC_PREFIX;
|
||||
}
|
||||
|
||||
const right = withoutParens(e.right);
|
||||
if (right.type === "LogicalExpression" && right.operator !== "??") {
|
||||
v.rightPrecedence = PREC_PREFIX;
|
||||
}
|
||||
} else if (eOperator === "**") {
|
||||
// The base of `**` must be an `UpdateExpression`.
|
||||
// Unary/await bases and negative-printing literals must be parenthesized.
|
||||
const left = withoutParens(e.left);
|
||||
typeAssertIs<LiteralExtras>(left);
|
||||
if (
|
||||
left.type === "UnaryExpression" ||
|
||||
left.type === "AwaitExpression" ||
|
||||
(TS && left.type === "TSTypeAssertion") ||
|
||||
(left.type === "Literal" && (typeof left.value === "number" || left.bigint != null))
|
||||
) {
|
||||
v.leftPrecedence = PREC_CALL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an operator is one of the three which `??` may not mix with unparenthesized.
|
||||
*/
|
||||
function isLogicalOperator(operator: string): boolean {
|
||||
return operator === "&&" || operator === "||" || operator === "??";
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish one level of the chain - its operator, its right operand, and its closing parenthesis.
|
||||
*
|
||||
* The operator is written space-padded as one token, which is why nothing here consults `last` -
|
||||
* the spacing checks cannot observe an operator which already has a space either side of it.
|
||||
*/
|
||||
function binVisitRightAndFinish(v: BinaryVisitor, state: State): void {
|
||||
// The operator is always surrounded by spaces here, which makes the token glue checks
|
||||
// (`printSpaceBeforeIdentifier` / `printSpaceBeforeOperator`) unobservable, so the whole token is one write
|
||||
write(state, PADDED_BIN_OPERATORS[v.operator], CAT_OTHER);
|
||||
// Any `ParenthesizedExpression` wrapper is kept, for the same reason as on the left operand
|
||||
printExpression(v.e.right, state, v.rightPrecedence, v.ctx);
|
||||
if (v.wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Binding patterns.
|
||||
|
||||
import { typeAssertIs } from "../asserts.ts";
|
||||
import {
|
||||
CAT_IDENT,
|
||||
CAT_OTHER,
|
||||
CAT_QUESTION,
|
||||
write,
|
||||
writeWithMap,
|
||||
writeWithMapNoLast,
|
||||
} from "./write.ts";
|
||||
import { printExpression } from "./expression.ts";
|
||||
import { printSpaceBeforeIdentifier } from "./space.ts";
|
||||
import { printLiteral } from "./literal.ts";
|
||||
import { CTX_NONE } from "./operators.ts";
|
||||
import { PREC_COMMA } from "./precedence.ts";
|
||||
import { printString } from "./string.ts";
|
||||
import { printTypeAnnotation } from "./typescript.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type { LiteralExtras, UnknownNode } from "./types.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* A binding pattern, or one of the nodes which can stand in for one.
|
||||
*
|
||||
* `RestElement` appears in object and array patterns.
|
||||
*/
|
||||
type BindingPatternNode =
|
||||
| ESTree.BindingPattern
|
||||
| ESTree.BindingRestElement
|
||||
| ESTree.FormalParameterRest;
|
||||
|
||||
/**
|
||||
* Print anything which can be bound to - a name, a destructuring pattern, or one carrying a default.
|
||||
*/
|
||||
export function printBindingPattern(node: BindingPatternNode | UnknownNode, state: State): void {
|
||||
switch (node.type) {
|
||||
case "Identifier":
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, node.name, CAT_IDENT, node);
|
||||
if (TS && node.optional) write(state, "?", CAT_QUESTION);
|
||||
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
||||
break;
|
||||
case "ObjectPattern":
|
||||
printObjectBindingPattern(node, state);
|
||||
if (TS && node.optional) write(state, "?", CAT_QUESTION);
|
||||
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
||||
break;
|
||||
case "ArrayPattern":
|
||||
printArrayBindingPattern(node, state);
|
||||
if (TS && node.optional) write(state, "?", CAT_QUESTION);
|
||||
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
||||
break;
|
||||
case "AssignmentPattern":
|
||||
printBindingPattern(node.left, state);
|
||||
write(state, " = ", CAT_OTHER);
|
||||
printExpression(node.right, state, PREC_COMMA, CTX_NONE);
|
||||
break;
|
||||
case "RestElement":
|
||||
writeWithMap(state, "...", CAT_OTHER, node);
|
||||
printBindingPattern(node.argument, state);
|
||||
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown binding pattern type: ${node.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an object destructuring pattern, as in `const { a, b } = obj`.
|
||||
*/
|
||||
function printObjectBindingPattern(node: ESTree.ObjectPattern, state: State): void {
|
||||
const { properties } = node;
|
||||
const { length } = properties;
|
||||
|
||||
if (length === 0) {
|
||||
writeWithMap(state, "{}", CAT_OTHER, node);
|
||||
return;
|
||||
}
|
||||
|
||||
writeWithMap(state, "{ ", CAT_OTHER, node);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
const property = properties[i];
|
||||
if (property.type === "RestElement") {
|
||||
write(state, "...", CAT_OTHER);
|
||||
printBindingPattern(property.argument, state);
|
||||
} else {
|
||||
printBindingProperty(property, state);
|
||||
}
|
||||
}
|
||||
|
||||
write(state, " }", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print one property of an object pattern, in shorthand where the key and the binding agree.
|
||||
*/
|
||||
function printBindingProperty(node: ESTree.BindingProperty, state: State): void {
|
||||
// Shorthand is re-derived from names (matching Oxc), not from the flag.
|
||||
const { key, value } = node;
|
||||
|
||||
let shorthand = false;
|
||||
if (!node.computed && key.type === "Identifier") {
|
||||
if (value.type === "Identifier" && key.name === value.name) {
|
||||
shorthand = true;
|
||||
} else if (
|
||||
value.type === "AssignmentPattern" &&
|
||||
value.left.type === "Identifier" &&
|
||||
key.name === value.left.name
|
||||
) {
|
||||
shorthand = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shorthand) {
|
||||
if (node.computed) {
|
||||
write(state, "[", CAT_OTHER);
|
||||
typeAssertIs<ESTree.Expression>(key);
|
||||
printExpression(key, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, "]", CAT_OTHER);
|
||||
} else {
|
||||
printPropertyKey(key, state);
|
||||
}
|
||||
write(state, ": ", CAT_OTHER);
|
||||
}
|
||||
|
||||
printBindingPattern(value, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a property key which is not computed - a name, a private name, a string or a number.
|
||||
*
|
||||
* The caller decides between this and printing the key as an expression in brackets,
|
||||
* because whether a key must be computed depends on what it is, not on how the source wrote it.
|
||||
*/
|
||||
export function printPropertyKey(key: ESTree.PropertyKey, state: State): void {
|
||||
switch (key.type) {
|
||||
case "Identifier":
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, key.name, CAT_IDENT, key);
|
||||
break;
|
||||
case "PrivateIdentifier":
|
||||
writeWithMapNoLast(state, "#", key);
|
||||
write(state, key.name, CAT_IDENT);
|
||||
break;
|
||||
case "Literal":
|
||||
typeAssertIs<LiteralExtras>(key);
|
||||
if (typeof key.value === "string") {
|
||||
printString(state, key.value, key);
|
||||
} else {
|
||||
printLiteral(key, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
printExpression(key, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an array destructuring pattern, holes and rest element included.
|
||||
*/
|
||||
function printArrayBindingPattern(node: ESTree.ArrayPattern, state: State): void {
|
||||
// Oxc stores the rest element separately from the other elements.
|
||||
// A trailing comma is printed before it, and a space always precedes it.
|
||||
const { elements } = node;
|
||||
let { length } = elements;
|
||||
|
||||
let rest: ESTree.BindingRestElement | ESTree.AssignmentTargetRest | null = null;
|
||||
if (length > 0) {
|
||||
const lastElement = elements[length - 1];
|
||||
if (lastElement != null && lastElement.type === "RestElement") {
|
||||
rest = lastElement;
|
||||
length--;
|
||||
}
|
||||
}
|
||||
|
||||
writeWithMap(state, "[", CAT_OTHER, node);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i !== 0) write(state, ", ", CAT_OTHER);
|
||||
|
||||
const element = elements[i];
|
||||
if (element != null) printBindingPattern(element, state);
|
||||
|
||||
if (i === length - 1 && (element == null || rest !== null)) write(state, ",", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (rest !== null) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printBindingPattern(rest, state);
|
||||
}
|
||||
|
||||
write(state, "]", CAT_OTHER);
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
// Classes.
|
||||
|
||||
import { typeAssertIs } from "../asserts.ts";
|
||||
import { printPropertyKey } from "./binding_pattern.ts";
|
||||
import {
|
||||
CAT_IDENT,
|
||||
CAT_OP_UN_NOT,
|
||||
CAT_OTHER,
|
||||
CAT_QUESTION,
|
||||
CAT_START_OF_STMT,
|
||||
debugAssertLastFresh,
|
||||
write,
|
||||
writeNoLast,
|
||||
writeWithMap,
|
||||
} from "./write.ts";
|
||||
import { printExpression } from "./expression.ts";
|
||||
import { printFunctionBody, printParenParams } from "./function.ts";
|
||||
import { printSpaceBeforeIdentifier } from "./space.ts";
|
||||
import { printIndent } from "./indent.ts";
|
||||
import { CTX_NONE } from "./operators.ts";
|
||||
import { PREC_CALL, PREC_COMMA, PREC_LOWEST, PREC_POSTFIX } from "./precedence.ts";
|
||||
import { printStatement } from "./statement.ts";
|
||||
import {
|
||||
printTSIndexSignature,
|
||||
printTypeAnnotation,
|
||||
printTypeArguments,
|
||||
printTypeParameters,
|
||||
} from "./typescript.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type {
|
||||
AccessorPropertyNode,
|
||||
ClassBodyNode,
|
||||
MethodDefinitionNode,
|
||||
PropertyDefinitionNode,
|
||||
} from "./types.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Print a class declaration or expression, decorators and heritage clauses included.
|
||||
*
|
||||
* A class expression is parenthesized where the statement or an `export default` starts with it,
|
||||
* as `printFunction` does for functions.
|
||||
*/
|
||||
export function printClass(node: ESTree.Class, state: State): void {
|
||||
let wrap = false;
|
||||
if (node.type === "ClassExpression") {
|
||||
debugAssertLastFresh(state);
|
||||
// `CAT_START_OF_STMT` or `CAT_START_OF_DEFAULT_EXPORT`, which are adjacent - see `write.ts`
|
||||
wrap = (state.last | 1) === CAT_START_OF_STMT;
|
||||
}
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
const { decorators } = node;
|
||||
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
const declare = TS && node.declare;
|
||||
const abstract = TS && node.abstract;
|
||||
|
||||
// The node's mapping goes on whichever of these is written first
|
||||
if (declare) writeWithMap(state, "declare ", CAT_OTHER, node);
|
||||
if (abstract) {
|
||||
if (declare) {
|
||||
write(state, "abstract ", CAT_OTHER);
|
||||
} else {
|
||||
writeWithMap(state, "abstract ", CAT_OTHER, node);
|
||||
}
|
||||
}
|
||||
if (declare || abstract) {
|
||||
write(state, "class", CAT_IDENT);
|
||||
} else {
|
||||
writeWithMap(state, "class", CAT_IDENT, node);
|
||||
}
|
||||
|
||||
if (node.id != null) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
writeWithMap(state, node.id.name, CAT_IDENT, node.id);
|
||||
}
|
||||
|
||||
if (TS) printTypeParameters(node.typeParameters, state);
|
||||
|
||||
if (node.superClass != null) {
|
||||
write(state, " extends ", CAT_OTHER);
|
||||
printExpression(node.superClass, state, PREC_POSTFIX, CTX_NONE);
|
||||
if (TS) printTypeArguments(node.superTypeArguments, state);
|
||||
}
|
||||
|
||||
const { implements: implementsClauses } = node;
|
||||
if (TS && implementsClauses != null && implementsClauses.length > 0) {
|
||||
write(state, " implements ", CAT_OTHER);
|
||||
|
||||
const { length } = implementsClauses;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
const clause = implementsClauses[i];
|
||||
printExpression(clause.expression, state, PREC_CALL, CTX_NONE);
|
||||
printTypeArguments(clause.typeArguments, state);
|
||||
}
|
||||
}
|
||||
|
||||
write(state, " ", CAT_OTHER);
|
||||
|
||||
printClassBody(node.body, state);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a run of decorators, one per line, parenthesizing any whose expression needs it.
|
||||
*/
|
||||
export function printDecorators(decorators: ESTree.Decorator[], state: State): void {
|
||||
const { length } = decorators;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const decorator = decorators[i];
|
||||
|
||||
writeWithMap(state, "@", CAT_OTHER, decorator);
|
||||
|
||||
const { expression } = decorator;
|
||||
const wrap = decoratorNeedsWrap(expression);
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
printExpression(expression, state, PREC_LOWEST, CTX_NONE);
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
|
||||
write(state, " ", CAT_OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a decorator's expression has to be parenthesized.
|
||||
*
|
||||
* The grammar allows a name, a chain of plain member accesses, and a call on either, and nothing
|
||||
* else - so a computed access needs parentheses even though a static one does not.
|
||||
*/
|
||||
function decoratorNeedsWrap(expr: ESTree.Expression): boolean {
|
||||
for (;;) {
|
||||
switch (expr.type) {
|
||||
case "Identifier":
|
||||
return false;
|
||||
case "MemberExpression":
|
||||
// `@a.b` / `@this.b` / `@x!.y` need no parens; computed access does
|
||||
return expr.computed;
|
||||
case "CallExpression":
|
||||
expr = expr.callee;
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the members of a class in braces, empty ones tight.
|
||||
*/
|
||||
function printClassBody(node: ClassBodyNode, state: State): void {
|
||||
const { body } = node;
|
||||
const { length } = body;
|
||||
if (length === 0) {
|
||||
writeWithMap(state, "{}", CAT_OTHER, node);
|
||||
return;
|
||||
}
|
||||
|
||||
writeWithMap(state, "{\n", CAT_OTHER, node);
|
||||
|
||||
state.indentLevel++;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
printIndent(state);
|
||||
|
||||
const element = body[i];
|
||||
switch (element.type) {
|
||||
case "MethodDefinition":
|
||||
/* IF TS */
|
||||
case "TSAbstractMethodDefinition":
|
||||
/* END_IF */
|
||||
printMethodDefinition(element, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "PropertyDefinition":
|
||||
/* IF TS */
|
||||
case "TSAbstractPropertyDefinition":
|
||||
/* END_IF */
|
||||
printPropertyDefinition(element, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "StaticBlock":
|
||||
printStaticBlock(element, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "AccessorProperty":
|
||||
/* IF TS */
|
||||
case "TSAbstractAccessorProperty":
|
||||
/* END_IF */
|
||||
printAccessorProperty(element, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
/* IF TS */
|
||||
case "TSIndexSignature":
|
||||
printTSIndexSignature(element, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
/* END_IF */
|
||||
default:
|
||||
throw new Error(`Unknown class element type: ${element.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
state.indentLevel--;
|
||||
|
||||
printIndent(state);
|
||||
write(state, "}", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a method, including getters, setters, constructors and their modifiers.
|
||||
*/
|
||||
function printMethodDefinition(node: MethodDefinitionNode, state: State): void {
|
||||
const { decorators } = node;
|
||||
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
|
||||
|
||||
if (TS && node.accessibility != null) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeNoLast(state, node.accessibility);
|
||||
write(state, " ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (TS && (node.type === "TSAbstractMethodDefinition" || node.abstract)) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "abstract ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (node.static) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "static ", CAT_OTHER);
|
||||
}
|
||||
if (TS && node.override) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "override ", CAT_OTHER);
|
||||
}
|
||||
|
||||
const { kind } = node;
|
||||
if (kind === "get") {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "get ", CAT_OTHER);
|
||||
} else if (kind === "set") {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "set ", CAT_OTHER);
|
||||
}
|
||||
|
||||
const func = node.value;
|
||||
if (func.async) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "async ", CAT_OTHER);
|
||||
}
|
||||
if (func.generator) write(state, "*", CAT_OTHER);
|
||||
|
||||
if (node.computed) {
|
||||
write(state, "[", CAT_OTHER);
|
||||
typeAssertIs<ESTree.Expression>(node.key);
|
||||
printExpression(node.key, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, "]", CAT_OTHER);
|
||||
} else {
|
||||
printPropertyKey(node.key, state);
|
||||
}
|
||||
|
||||
if (TS && node.optional) write(state, "?", CAT_QUESTION);
|
||||
|
||||
if (TS) printTypeParameters(func.typeParameters, state);
|
||||
|
||||
printParenParams(func.params, state);
|
||||
|
||||
if (TS && func.returnType != null) {
|
||||
printTypeAnnotation(func.returnType, state);
|
||||
}
|
||||
|
||||
if (func.body != null) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printFunctionBody(func.body, state);
|
||||
} else {
|
||||
write(state, ";", CAT_OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a class field, with its modifiers and initializer.
|
||||
*/
|
||||
function printPropertyDefinition(node: PropertyDefinitionNode, state: State): void {
|
||||
const { decorators } = node;
|
||||
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
|
||||
|
||||
if (TS && node.declare) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "declare ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (TS && node.accessibility != null) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeNoLast(state, node.accessibility);
|
||||
write(state, " ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (TS && (node.type === "TSAbstractPropertyDefinition" || node.abstract)) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "abstract ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (node.static) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "static ", CAT_OTHER);
|
||||
}
|
||||
if (TS && node.override) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "override ", CAT_OTHER);
|
||||
}
|
||||
if (TS && node.readonly) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "readonly ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (node.computed) {
|
||||
write(state, "[", CAT_OTHER);
|
||||
typeAssertIs<ESTree.Expression>(node.key);
|
||||
printExpression(node.key, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, "]", CAT_OTHER);
|
||||
} else {
|
||||
printPropertyKey(node.key, state);
|
||||
}
|
||||
|
||||
if (TS && node.optional) write(state, "?", CAT_QUESTION);
|
||||
if (TS && node.definite) write(state, "!", CAT_OP_UN_NOT);
|
||||
|
||||
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
||||
|
||||
if (node.value != null) {
|
||||
write(state, " = ", CAT_OTHER);
|
||||
printExpression(node.value, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a `static { … }` block.
|
||||
*
|
||||
* Its body has no directive prologue, unlike a function body, so a string statement at the top of
|
||||
* one needs no protection from being read back as a directive.
|
||||
*/
|
||||
function printStaticBlock(node: ESTree.StaticBlock, state: State): void {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "static ", CAT_OTHER, node);
|
||||
|
||||
const { body } = node;
|
||||
const { length } = body;
|
||||
if (length === 0) {
|
||||
write(state, "{}", CAT_OTHER);
|
||||
return;
|
||||
}
|
||||
|
||||
write(state, "{\n", CAT_OTHER);
|
||||
|
||||
state.indentLevel++;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
printStatement(body[i], state);
|
||||
}
|
||||
|
||||
state.indentLevel--;
|
||||
|
||||
printIndent(state);
|
||||
write(state, "}", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an `accessor` field.
|
||||
*/
|
||||
function printAccessorProperty(node: AccessorPropertyNode, state: State): void {
|
||||
const { decorators } = node;
|
||||
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
|
||||
|
||||
if (TS && (node.type === "TSAbstractAccessorProperty" || node.abstract)) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "abstract ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (TS && node.accessibility != null) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeNoLast(state, node.accessibility);
|
||||
write(state, " ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (node.static) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "static ", CAT_OTHER);
|
||||
}
|
||||
if (TS && node.override) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "override ", CAT_OTHER);
|
||||
}
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "accessor", CAT_IDENT, node);
|
||||
|
||||
if (node.computed) {
|
||||
write(state, " [", CAT_OTHER);
|
||||
typeAssertIs<ESTree.Expression>(node.key);
|
||||
printExpression(node.key, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, "]", CAT_OTHER);
|
||||
} else {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printPropertyKey(node.key, state);
|
||||
}
|
||||
|
||||
if (TS && node.definite) write(state, "!", CAT_OP_UN_NOT);
|
||||
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
||||
|
||||
if (node.value != null) {
|
||||
write(state, " = ", CAT_OTHER);
|
||||
printExpression(node.value, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,949 @@
|
||||
// Expressions.
|
||||
|
||||
import { typeAssertIs } from "../asserts.ts";
|
||||
import { printAssignmentTarget } from "./assignment_target.ts";
|
||||
import { printBinaryish } from "./binary.ts";
|
||||
import { printPropertyKey } from "./binding_pattern.ts";
|
||||
import {
|
||||
CAT_IDENT,
|
||||
CAT_INT_DIGIT,
|
||||
CAT_LT,
|
||||
CAT_OP_UN_NOT,
|
||||
CAT_OP_UN_NOT_AFTER_LT,
|
||||
CAT_OTHER,
|
||||
CAT_QUESTION,
|
||||
CAT_START_OF_ARROW_EXPR,
|
||||
CAT_START_OF_STMT,
|
||||
} from "./write.ts";
|
||||
import { printClass } from "./class.ts";
|
||||
import {
|
||||
printFunction,
|
||||
printFunctionBody,
|
||||
printParenParams,
|
||||
printParenParamsArrow,
|
||||
} from "./function.ts";
|
||||
import { printSpaceBeforeIdentifier, printSpaceBeforeOperator } from "./space.ts";
|
||||
import { printIndent } from "./indent.ts";
|
||||
import { printJSXElement, printJSXFragment } from "./jsx.ts";
|
||||
import { printLiteral } from "./literal.ts";
|
||||
import {
|
||||
CTX_FORBID_CALL,
|
||||
CTX_FORBID_IN,
|
||||
CTX_NONE,
|
||||
PADDED_ASSIGN_OPERATORS,
|
||||
unaryOperatorCode,
|
||||
updateOperatorCode,
|
||||
} from "./operators.ts";
|
||||
import { withoutParens } from "./parens.ts";
|
||||
import {
|
||||
PREC_ASSIGN,
|
||||
PREC_CALL,
|
||||
PREC_COMMA,
|
||||
PREC_COMPARE,
|
||||
PREC_CONDITIONAL,
|
||||
PREC_EQUALS,
|
||||
PREC_EXPONENTIATION,
|
||||
PREC_LOWEST,
|
||||
PREC_NEW,
|
||||
PREC_POSTFIX,
|
||||
PREC_PREFIX,
|
||||
PREC_YIELD,
|
||||
} from "./precedence.ts";
|
||||
import {
|
||||
debugAssertLastFresh,
|
||||
write,
|
||||
writeNoLast,
|
||||
writeWithMap,
|
||||
writeWithMapNoLast,
|
||||
} from "./write.ts";
|
||||
import { escapeScriptCloseTag } from "./string.ts";
|
||||
import {
|
||||
printTSAsOrSatisfiesExpression,
|
||||
printTSTypeAssertion,
|
||||
printTypeAnnotation,
|
||||
printTypeArguments,
|
||||
printTypeParameters,
|
||||
} from "./typescript.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type { UnknownNode } from "./types.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Dispatch point for expressions. The switch writes the one-token constructs inline
|
||||
* and hands everything else to the printer for that node type, throwing on a type it does not know.
|
||||
*
|
||||
* Parens are not preserved from the input - every printer re-derives its own from `precedence`,
|
||||
* so a `ParenthesizedExpression` mostly prints straight through.
|
||||
*
|
||||
* @param precedence - Precedence of the position this expression sits in.
|
||||
* A printer compares its own precedence against it and parenthesizes itself when it would otherwise bind too loosely.
|
||||
* @param ctx - `CTX_FORBID_IN` / `CTX_FORBID_CALL` flags describing the enclosing position, so that an `in` operator
|
||||
* inside a `for` head, or a call under a `new`, knows to parenthesize itself.
|
||||
*/
|
||||
export function printExpression(
|
||||
node: ESTree.Expression | UnknownNode,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
// Arms are ordered roughly in order of most common nodes.
|
||||
// V8 turns this into (essentially) as chain of `if ... else if ... else if...`,
|
||||
// so making common nodes short-circuit early is a large perf boost.
|
||||
switch (node.type) {
|
||||
case "Identifier":
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, node.name, CAT_IDENT, node);
|
||||
break;
|
||||
case "MemberExpression":
|
||||
printMemberExpression(node, state, ctx);
|
||||
break;
|
||||
case "CallExpression":
|
||||
printCallExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "Literal":
|
||||
printLiteral(node, state, precedence, ctx);
|
||||
break;
|
||||
case "BinaryExpression":
|
||||
if (node.left.type === "PrivateIdentifier") {
|
||||
typeAssertIs<ESTree.PrivateInExpression>(node);
|
||||
printPrivateInExpression(node, state, precedence);
|
||||
} else {
|
||||
typeAssertIs<ESTree.BinaryExpression>(node);
|
||||
printBinaryish(node, state, precedence, ctx);
|
||||
}
|
||||
break;
|
||||
case "LogicalExpression":
|
||||
printBinaryish(node, state, precedence, ctx);
|
||||
break;
|
||||
case "ObjectExpression":
|
||||
printObjectExpression(node, state);
|
||||
break;
|
||||
case "ArrayExpression":
|
||||
printArrayExpression(node, state);
|
||||
break;
|
||||
case "AssignmentExpression":
|
||||
printAssignmentExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "UpdateExpression":
|
||||
printUpdateExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "UnaryExpression":
|
||||
printUnaryExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "ConditionalExpression":
|
||||
printConditionalExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "SequenceExpression":
|
||||
printSequenceExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "ArrowFunctionExpression":
|
||||
printArrowFunctionExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "FunctionExpression":
|
||||
printFunction(node, state);
|
||||
break;
|
||||
case "ThisExpression":
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "this", CAT_IDENT, node);
|
||||
break;
|
||||
case "Super":
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "super", CAT_IDENT, node);
|
||||
break;
|
||||
case "NewExpression":
|
||||
printNewExpression(node, state, precedence);
|
||||
break;
|
||||
case "TemplateLiteral":
|
||||
printTemplateLiteral(node, state);
|
||||
break;
|
||||
case "TaggedTemplateExpression":
|
||||
printExpression(node.tag, state, PREC_POSTFIX, ctx & CTX_FORBID_CALL);
|
||||
if (TS) printTypeArguments(node.typeArguments, state);
|
||||
printTemplateLiteral(node.quasi, state);
|
||||
break;
|
||||
case "ClassExpression":
|
||||
printClass(node, state);
|
||||
break;
|
||||
case "AwaitExpression":
|
||||
printAwaitExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "YieldExpression":
|
||||
printYieldExpression(node, state, precedence);
|
||||
break;
|
||||
case "ImportExpression":
|
||||
printImportExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "MetaProperty":
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMapNoLast(state, node.meta.name, node);
|
||||
writeNoLast(state, ".");
|
||||
write(state, node.property.name, CAT_IDENT);
|
||||
break;
|
||||
case "ChainExpression":
|
||||
printChainExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "ParenthesizedExpression": {
|
||||
// Parens around function/arrow expressions are preserved (Oxc `pife`)
|
||||
const { expression } = node;
|
||||
const inner = withoutParens(expression);
|
||||
if (inner.type === "FunctionExpression" || inner.type === "ArrowFunctionExpression") {
|
||||
write(state, "(", CAT_OTHER);
|
||||
printExpression(inner, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ")", CAT_OTHER);
|
||||
} else {
|
||||
printExpression(expression, state, precedence, ctx);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "JSXElement":
|
||||
printJSXElement(node, state);
|
||||
break;
|
||||
case "JSXFragment":
|
||||
printJSXFragment(node, state);
|
||||
break;
|
||||
/* IF TS */
|
||||
case "TSAsExpression":
|
||||
case "TSSatisfiesExpression":
|
||||
printTSAsOrSatisfiesExpression(node, state, precedence, ctx);
|
||||
break;
|
||||
case "TSNonNullExpression":
|
||||
printExpression(node.expression, state, PREC_POSTFIX, ctx);
|
||||
write(state, "!", CAT_OP_UN_NOT);
|
||||
break;
|
||||
case "TSInstantiationExpression":
|
||||
printExpression(node.expression, state, PREC_PREFIX, ctx);
|
||||
printTypeArguments(node.typeArguments, state);
|
||||
break;
|
||||
case "TSTypeAssertion":
|
||||
printTSTypeAssertion(node, state, precedence, ctx);
|
||||
break;
|
||||
/* END_IF */
|
||||
default:
|
||||
throw new Error(`Unknown expression type: ${node.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The object is printed at `PREC_POSTFIX` and only `CTX_FORBID_CALL` is carried into it, so a member chain
|
||||
* beneath a `new` keeps that restriction while a computed property, printed from `PREC_LOWEST`, starts clean.
|
||||
*
|
||||
* Two shapes need more than precedence to stay valid:
|
||||
* 1. A bare `let` as the object.
|
||||
* 2. Member access straight onto an integer literal.
|
||||
*/
|
||||
export function printMemberExpression(
|
||||
node: ESTree.MemberExpression,
|
||||
state: State,
|
||||
ctx: number,
|
||||
): void {
|
||||
const { object } = node;
|
||||
if (node.computed) {
|
||||
// `(let)[0]` - a bare `let` object must be wrapped
|
||||
const inner = withoutParens(object);
|
||||
const wrap = inner.type === "Identifier" && inner.name === "let";
|
||||
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
printExpression(object, state, PREC_POSTFIX, ctx & CTX_FORBID_CALL);
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
|
||||
if (node.optional) write(state, "?.", CAT_OTHER);
|
||||
|
||||
write(state, "[", CAT_OTHER);
|
||||
printExpression(node.property, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, "]", CAT_OTHER);
|
||||
} else {
|
||||
printExpression(object, state, PREC_POSTFIX, ctx & CTX_FORBID_CALL);
|
||||
|
||||
if (node.optional) {
|
||||
write(state, "?", CAT_QUESTION);
|
||||
} else {
|
||||
debugAssertLastFresh(state);
|
||||
// `0.toExponential()` is invalid; `0 .toExponential()` is valid
|
||||
if (state.last === CAT_INT_DIGIT) write(state, " ", CAT_OTHER);
|
||||
}
|
||||
|
||||
write(state, ".", CAT_OTHER);
|
||||
|
||||
const { property } = node;
|
||||
if (property.type === "PrivateIdentifier") {
|
||||
writeWithMapNoLast(state, "#", property);
|
||||
write(state, property.name, CAT_IDENT);
|
||||
} else {
|
||||
writeWithMap(state, property.name, CAT_IDENT, property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call parenthesizes itself from `PREC_NEW` upwards, or whenever `CTX_FORBID_CALL` is set,
|
||||
* both of which mean an unparenthesized argument list would be read as belonging to an enclosing `new`.
|
||||
*
|
||||
* Writing that paren also re-marks the start of a statement or of an `export default`,
|
||||
* so the callee inside the parens is still read as the leftmost token of the construct.
|
||||
*/
|
||||
function printCallExpression(
|
||||
node: ESTree.CallExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_NEW || (ctx & CTX_FORBID_CALL) !== 0;
|
||||
|
||||
if (wrap) {
|
||||
// The mark read below is the one from before the paren, so it is checked here -
|
||||
// `writeNoLast` marks `last` stale in between, and the last line clears that again
|
||||
debugAssertLastFresh(state);
|
||||
|
||||
// A statement or an `export default` mark is left intact.
|
||||
// A concise arrow body's mark is deliberately left to die at the paren, as `oxc_codegen` does.
|
||||
writeNoLast(state, "(");
|
||||
|
||||
// `CAT_START_OF_STMT` or `CAT_START_OF_DEFAULT_EXPORT`, which are adjacent - see `write.ts`
|
||||
if ((state.last | 1) !== CAT_START_OF_STMT) state.last = CAT_OTHER;
|
||||
if (DEBUG) state.lastIsStale = false;
|
||||
}
|
||||
|
||||
printExpression(node.callee, state, PREC_POSTFIX, CTX_NONE);
|
||||
|
||||
if (node.optional) write(state, "?.", CAT_OTHER);
|
||||
|
||||
if (TS) printTypeArguments(node.typeArguments, state);
|
||||
|
||||
printArguments(node.arguments, state);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* The argument list shared by `printCallExpression` and `printNewExpression`.
|
||||
*
|
||||
* Arguments print at `PREC_COMMA`, so a sequence expression parenthesizes itself instead of reading as more arguments.
|
||||
*/
|
||||
function printArguments(args: ESTree.Argument[], state: State): void {
|
||||
const { length } = args;
|
||||
if (length === 0) {
|
||||
write(state, "()", CAT_OTHER);
|
||||
return;
|
||||
}
|
||||
|
||||
write(state, "(", CAT_OTHER);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
|
||||
const arg = args[i];
|
||||
if (arg.type === "SpreadElement") {
|
||||
write(state, "...", CAT_OTHER);
|
||||
printExpression(arg.argument, state, PREC_COMMA, CTX_NONE);
|
||||
} else {
|
||||
printExpression(arg, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* `#field in obj`, which arrives as a `BinaryExpression` with a `PrivateIdentifier` on the left
|
||||
* rather than as a node type of its own - hence the extra test in `printExpression`.
|
||||
*
|
||||
* It sits at the `in` operator's own level, so it wraps from `PREC_COMPARE` upwards, and the right
|
||||
* operand prints one level tighter with `CTX_FORBID_IN` set.
|
||||
*/
|
||||
export function printPrivateInExpression(
|
||||
node: ESTree.PrivateInExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_COMPARE;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
writeWithMapNoLast(state, "#", node.left);
|
||||
write(state, node.left.name, CAT_IDENT);
|
||||
write(state, " in ", CAT_OTHER);
|
||||
printExpression(node.right, state, PREC_EQUALS, CTX_FORBID_IN);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* An object literal parenthesizes itself when it starts a statement or a concise arrow body,
|
||||
* where a leading `{` would otherwise be read as a block.
|
||||
*
|
||||
* Layout follows the property count alone - two or more break across lines, exactly one prints
|
||||
* inside spaces on a single line, and none gives `{}`.
|
||||
*/
|
||||
function printObjectExpression(node: ESTree.ObjectExpression, state: State): void {
|
||||
debugAssertLastFresh(state);
|
||||
// `CAT_START_OF_STMT` or `CAT_START_OF_ARROW_EXPR`, which are adjacent - see `write.ts`
|
||||
const wrap = ((state.last - 1) | 1) === CAT_START_OF_STMT;
|
||||
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
const { properties } = node;
|
||||
const { length } = properties;
|
||||
const isMultiLine = length > 1;
|
||||
|
||||
writeWithMap(state, "{", CAT_OTHER, node);
|
||||
|
||||
if (isMultiLine) {
|
||||
state.indentLevel++;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
write(state, i > 0 ? ",\n" : "\n", CAT_OTHER);
|
||||
printIndent(state);
|
||||
printObjectProperty(properties[i], state);
|
||||
}
|
||||
|
||||
write(state, "\n", CAT_OTHER);
|
||||
state.indentLevel--;
|
||||
printIndent(state);
|
||||
} else if (length === 1) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printObjectProperty(properties[0], state);
|
||||
write(state, " ", CAT_OTHER);
|
||||
}
|
||||
|
||||
write(state, "}", CAT_OTHER);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* One entry of an object literal - a spread, a method or accessor, or a plain `key: value` pair.
|
||||
*
|
||||
* Shorthand and computed-ness are both worked out here rather than taken from the AST,
|
||||
* so the output stays valid however a transform left those flags.
|
||||
*/
|
||||
function printObjectProperty(node: ESTree.ObjectPropertyKind, state: State): void {
|
||||
if (node.type === "SpreadElement") {
|
||||
writeWithMap(state, "...", CAT_OTHER, node);
|
||||
printExpression(node.argument, state, PREC_COMMA, CTX_NONE);
|
||||
return;
|
||||
}
|
||||
|
||||
const { key, value } = node;
|
||||
if (
|
||||
value.type === "FunctionExpression" ||
|
||||
(TS && value.type === "TSEmptyBodyFunctionExpression")
|
||||
) {
|
||||
const { kind } = node;
|
||||
const isGetter = kind === "get";
|
||||
const isAccessor = isGetter || kind === "set";
|
||||
if (isAccessor) write(state, isGetter ? "get " : "set ", CAT_OTHER);
|
||||
|
||||
if (node.method || isAccessor) {
|
||||
if (value.async) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "async ", CAT_OTHER);
|
||||
}
|
||||
if (value.generator) write(state, "*", CAT_OTHER);
|
||||
|
||||
if (node.computed) {
|
||||
write(state, "[", CAT_OTHER);
|
||||
typeAssertIs<ESTree.Expression>(key);
|
||||
printExpression(key, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, "]", CAT_OTHER);
|
||||
} else {
|
||||
printPropertyKey(key, state);
|
||||
}
|
||||
|
||||
if (TS) printTypeParameters(value.typeParameters, state);
|
||||
|
||||
printParenParams(value.params, state);
|
||||
|
||||
if (TS && value.returnType != null) printTypeAnnotation(value.returnType, state);
|
||||
|
||||
if (value.body != null) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printFunctionBody(value.body, state);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Shorthand is re-derived from names (matching Oxc), except `__proto__`
|
||||
// which keeps its flag (`{ __proto__ }` and `{ __proto__: x }` differ).
|
||||
let shorthand = false;
|
||||
// The identifier a name-derived shorthand prints, which is known here without asking
|
||||
// the expression printer what it is.
|
||||
// A `__proto__` shorthand leaves this null, since its flag says nothing about the value it carries.
|
||||
let shorthandIdentifier: ESTree.IdentifierReference | null = null;
|
||||
if (!node.computed && key.type === "Identifier") {
|
||||
if (key.name === "__proto__") {
|
||||
shorthand = node.shorthand;
|
||||
} else {
|
||||
const inner = withoutParens(value);
|
||||
if (inner.type === "Identifier" && key.name === inner.name) {
|
||||
shorthand = true;
|
||||
shorthandIdentifier = inner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let { computed } = node;
|
||||
// `{ -1: 0 }` and `{ 1/0: 0 }` must print as `{ [-1]: 0 }` / `{ [1 / 0]: 0 }`
|
||||
if (
|
||||
!computed &&
|
||||
key.type === "Literal" &&
|
||||
typeof key.value === "number" &&
|
||||
(key.value < 0 || Object.is(key.value, -0) || !Number.isFinite(key.value))
|
||||
) {
|
||||
computed = true;
|
||||
}
|
||||
|
||||
if (shorthand) {
|
||||
if (shorthandIdentifier !== null) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, shorthandIdentifier.name, CAT_IDENT, shorthandIdentifier);
|
||||
} else {
|
||||
// `__proto__` shorthand, whose value can be anything. Print through any parens around it.
|
||||
printExpression(withoutParens(value), state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
} else {
|
||||
if (computed) {
|
||||
write(state, "[", CAT_OTHER);
|
||||
typeAssertIs<ESTree.Expression>(key);
|
||||
printExpression(key, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, "]", CAT_OTHER);
|
||||
} else {
|
||||
printPropertyKey(key, state);
|
||||
}
|
||||
|
||||
write(state, ": ", CAT_OTHER);
|
||||
|
||||
printExpression(value, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrays break across lines from three elements upwards, where objects break from two.
|
||||
*
|
||||
* Holes print as nothing between the commas, and a hole in final position needs one further comma
|
||||
* written after it, since `[a,]` holds one element and `[a,,]` holds two.
|
||||
*/
|
||||
function printArrayExpression(node: ESTree.ArrayExpression, state: State): void {
|
||||
const { elements } = node;
|
||||
const { length } = elements;
|
||||
const isMultiLine = length > 2;
|
||||
|
||||
writeWithMap(state, "[", CAT_OTHER, node);
|
||||
|
||||
if (isMultiLine) state.indentLevel++;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (isMultiLine) {
|
||||
write(state, i !== 0 ? ",\n" : "\n", CAT_OTHER);
|
||||
printIndent(state);
|
||||
} else if (i !== 0) {
|
||||
write(state, ", ", CAT_OTHER);
|
||||
}
|
||||
|
||||
const element = elements[i];
|
||||
if (element != null) {
|
||||
if (element.type === "SpreadElement") {
|
||||
write(state, "...", CAT_OTHER);
|
||||
printExpression(element.argument, state, PREC_COMMA, CTX_NONE);
|
||||
} else {
|
||||
printExpression(element, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (i === length - 1 && element == null) {
|
||||
write(state, ",", CAT_OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
if (isMultiLine) {
|
||||
write(state, "\n", CAT_OTHER);
|
||||
state.indentLevel--;
|
||||
printIndent(state);
|
||||
}
|
||||
|
||||
write(state, "]", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps from `PREC_ASSIGN` upwards. An object destructuring target forces parens as well at the start of
|
||||
* a statement or a concise arrow body, where the leading `{` would be read as a block.
|
||||
*
|
||||
* @param ctx - Handed to the right operand unchanged, so a `for` head's `CTX_FORBID_IN`
|
||||
* still reaches an `in` operator there.
|
||||
*/
|
||||
function printAssignmentExpression(
|
||||
node: ESTree.AssignmentExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const { left } = node;
|
||||
|
||||
let wrap = precedence >= PREC_ASSIGN;
|
||||
if (!wrap && left.type === "ObjectPattern") {
|
||||
debugAssertLastFresh(state);
|
||||
// `CAT_START_OF_STMT` or `CAT_START_OF_ARROW_EXPR`, which are adjacent - see `write.ts`
|
||||
wrap = ((state.last - 1) | 1) === CAT_START_OF_STMT;
|
||||
}
|
||||
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
printAssignmentTarget(left, state);
|
||||
write(state, PADDED_ASSIGN_OPERATORS[node.operator], CAT_OTHER);
|
||||
printExpression(node.right, state, PREC_COMMA, ctx);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Which precedence decides the parens depends on `node.prefix` - a prefix `++`/`--` binds at `PREC_PREFIX`,
|
||||
* a postfix one at `PREC_POSTFIX`.
|
||||
*
|
||||
* The operator's own category is recorded as it is written, which is how an adjacent `+` or `-`
|
||||
* knows to leave a space rather than glue into `---x`.
|
||||
*/
|
||||
function printUpdateExpression(
|
||||
node: ESTree.UpdateExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const selfPrecedence = node.prefix ? PREC_PREFIX : PREC_POSTFIX;
|
||||
const wrap = precedence >= selfPrecedence;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
const operatorCode = updateOperatorCode(node.operator);
|
||||
|
||||
if (node.prefix) {
|
||||
printSpaceBeforeOperator(state, operatorCode);
|
||||
writeWithMap(state, node.operator, operatorCode, node);
|
||||
printExpression(node.argument, state, PREC_PREFIX, ctx);
|
||||
} else {
|
||||
printExpression(node.argument, state, PREC_POSTFIX, ctx);
|
||||
printSpaceBeforeOperator(state, operatorCode);
|
||||
write(state, node.operator, operatorCode);
|
||||
}
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps from `PREC_PREFIX` upwards and prints its argument at `PREC_EXPONENTIATION`,
|
||||
* so a `**` operand takes parens of its own - `-a ** b` does not parse.
|
||||
*
|
||||
* A `!` written straight after a `<` records a category of its own, so that a `--`
|
||||
* printed next is spaced off it and cannot complete `<!--`.
|
||||
*/
|
||||
function printUnaryExpression(
|
||||
node: ESTree.UnaryExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_PREFIX;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
const { operator } = node;
|
||||
let isDeleteInfinity = false;
|
||||
if (operator.length > 1) {
|
||||
// typeof, void, delete
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, operator, CAT_IDENT, node);
|
||||
write(state, " ", CAT_OTHER);
|
||||
// `delete Infinity` is a syntax error in strict mode
|
||||
isDeleteInfinity =
|
||||
operator === "delete" && node.argument.type === "Literal" && node.argument.value === Infinity;
|
||||
} else {
|
||||
let operatorCode = unaryOperatorCode(operator);
|
||||
printSpaceBeforeOperator(state, operatorCode);
|
||||
debugAssertLastFresh(state);
|
||||
if (operatorCode === CAT_OP_UN_NOT && state.last === CAT_LT) {
|
||||
operatorCode = CAT_OP_UN_NOT_AFTER_LT;
|
||||
}
|
||||
writeWithMap(state, operator, operatorCode, node);
|
||||
}
|
||||
|
||||
if (isDeleteInfinity) write(state, "(0, ", CAT_OTHER);
|
||||
printExpression(node.argument, state, PREC_EXPONENTIATION, ctx);
|
||||
if (isDeleteInfinity) write(state, ")", CAT_OTHER);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps from `PREC_CONDITIONAL` upwards. Both branches print at `PREC_YIELD`, which admits
|
||||
* a bare assignment or `yield` but still parenthesizes a comma sequence.
|
||||
*
|
||||
* The consequent is fenced by the `?` and `:` either side of it, so it alone never needs
|
||||
* the `in` restriction passed down.
|
||||
*/
|
||||
function printConditionalExpression(
|
||||
node: ESTree.ConditionalExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_CONDITIONAL;
|
||||
|
||||
// The parens the wrap adds are themselves enough to escape a `for` head,
|
||||
// so `in` is only forbidden further down when the expression is printed bare
|
||||
let innerCtx = 0;
|
||||
if (wrap) {
|
||||
write(state, "(", CAT_OTHER);
|
||||
} else {
|
||||
innerCtx = ctx & CTX_FORBID_IN;
|
||||
}
|
||||
|
||||
// Keep an `as` or `satisfies` test grouped, by printing it at the precedence which makes it
|
||||
// parenthesize itself. Without those parentheses a regexp consequent does not survive a
|
||||
// round trip: `value as Type ? /x/ : y` re-lexes the `/` as a division.
|
||||
let testPrecedence = PREC_CONDITIONAL;
|
||||
if (TS) {
|
||||
const testType = withoutParens(node.test).type;
|
||||
if (testType === "TSAsExpression" || testType === "TSSatisfiesExpression") {
|
||||
testPrecedence = PREC_COMPARE;
|
||||
}
|
||||
}
|
||||
|
||||
printExpression(node.test, state, testPrecedence, innerCtx);
|
||||
write(state, " ? ", CAT_OTHER);
|
||||
printExpression(node.consequent, state, PREC_YIELD, CTX_NONE);
|
||||
write(state, " : ", CAT_OTHER);
|
||||
printExpression(node.alternate, state, PREC_YIELD, innerCtx);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps from `PREC_COMMA` upwards.
|
||||
* `CTX_FORBID_CALL` is cleared for the elements, since every position which sets that flag
|
||||
* also passes a precedence at or above `PREC_COMMA`, so the parens are already written by then.
|
||||
*/
|
||||
function printSequenceExpression(
|
||||
node: ESTree.SequenceExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_COMMA;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
const innerCtx = ctx & ~CTX_FORBID_CALL;
|
||||
const { expressions } = node;
|
||||
const { length } = expressions;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
printExpression(expressions[i], state, PREC_LOWEST, innerCtx);
|
||||
}
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps from `PREC_ASSIGN` upwards, and when it does the parens fence the body, so `CTX_FORBID_IN`
|
||||
* is dropped on the way in.
|
||||
*
|
||||
* A concise body marks `last`, which is how an object literal body knows to parenthesize itself.
|
||||
* A TS return type has to sit between the parameters and the arrow, so those are printed separately,
|
||||
* instead of through `printParenParamsArrow`.
|
||||
*/
|
||||
function printArrowFunctionExpression(
|
||||
node: ESTree.ArrowFunctionExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_ASSIGN;
|
||||
const bodyCtx = wrap ? ctx & ~CTX_FORBID_IN : ctx;
|
||||
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
if (node.async) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "async ", CAT_OTHER, node);
|
||||
}
|
||||
|
||||
if (TS) printTypeParameters(node.typeParameters, state);
|
||||
|
||||
const { returnType } = node;
|
||||
if (TS && returnType != null) {
|
||||
printParenParams(node.params, state);
|
||||
printTypeAnnotation(returnType, state);
|
||||
write(state, " => ", CAT_OTHER);
|
||||
} else {
|
||||
printParenParamsArrow(node.params, state);
|
||||
}
|
||||
|
||||
const { body } = node;
|
||||
if (body.type === "BlockStatement") {
|
||||
printFunctionBody(body, state);
|
||||
} else {
|
||||
state.last = CAT_START_OF_ARROW_EXPR;
|
||||
printExpression(body, state, PREC_COMMA, bodyCtx);
|
||||
}
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only a call position binds tighter than `new`, so this wraps from `PREC_CALL` upwards.
|
||||
*
|
||||
* The callee prints at `PREC_NEW` with `CTX_FORBID_CALL`, so a call inside it parenthesizes itself,
|
||||
* rather than being taken for this `new`'s own argument list.
|
||||
*/
|
||||
function printNewExpression(node: ESTree.NewExpression, state: State, precedence: number): void {
|
||||
const wrap = precedence >= PREC_CALL;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "new ", CAT_OTHER, node);
|
||||
printExpression(node.callee, state, PREC_NEW, CTX_FORBID_CALL);
|
||||
if (TS) printTypeArguments(node.typeArguments, state);
|
||||
printArguments(node.arguments, state);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quasis and expressions interleave, with one more quasi than there are expressions, so the first chunk
|
||||
* is written before the loop and each iteration ends with the chunk after its substitution.
|
||||
*
|
||||
* Substitutions print from `PREC_LOWEST` with no context flags, since `${` and `}` fence them from everything around.
|
||||
*/
|
||||
function printTemplateLiteral(node: ESTree.TemplateLiteral, state: State): void {
|
||||
writeWithMapNoLast(state, "`", node);
|
||||
|
||||
const { quasis, expressions } = node;
|
||||
const { length } = expressions;
|
||||
const firstQuasi = quasis[0];
|
||||
writeWithMapNoLast(state, templateQuasiRaw(firstQuasi), firstQuasi);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
write(state, "${", CAT_OTHER);
|
||||
printExpression(expressions[i], state, PREC_LOWEST, CTX_NONE);
|
||||
writeNoLast(state, "}");
|
||||
|
||||
const quasi = quasis[i + 1];
|
||||
writeWithMapNoLast(state, templateQuasiRaw(quasi), quasi);
|
||||
}
|
||||
|
||||
write(state, "`", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw text of one template chunk, ready to be written between the backticks.
|
||||
*
|
||||
* It also runs the `</script` escape, since a template can hold that sequence verbatim
|
||||
* and would otherwise close a surrounding script element.
|
||||
*/
|
||||
function templateQuasiRaw(quasi: ESTree.TemplateElement): string {
|
||||
// Line terminators in template raws are normalized to LF at parse time.
|
||||
// TS-ESLint's `raw` keeps the source bytes, so normalize here.
|
||||
let { raw } = quasi.value;
|
||||
if (raw.includes("\r")) raw = raw.replace(/\r\n?/g, "\n");
|
||||
return escapeScriptCloseTag(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps from `PREC_PREFIX` upwards like the other unary operators, and prints its argument
|
||||
* at `PREC_EXPONENTIATION` - `await a ** b` does not parse, so the `**` takes parens.
|
||||
*/
|
||||
function printAwaitExpression(
|
||||
node: ESTree.AwaitExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_PREFIX;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "await ", CAT_OTHER, node);
|
||||
printExpression(node.argument, state, PREC_EXPONENTIATION, ctx);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps from `PREC_ASSIGN` upwards. The argument prints at `PREC_YIELD`, one level below assignment,
|
||||
* so a nested `yield` or an assignment stays bare while a comma sequence parenthesizes.
|
||||
*/
|
||||
function printYieldExpression(
|
||||
node: ESTree.YieldExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_ASSIGN;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "yield", CAT_IDENT, node);
|
||||
|
||||
if (node.delegate) write(state, "*", CAT_OTHER);
|
||||
|
||||
if (node.argument != null) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printExpression(node.argument, state, PREC_YIELD, CTX_NONE);
|
||||
}
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* `import(...)` takes the same wrap rule as a call, since `new import("x")` would otherwise
|
||||
* attach the argument list to the `new`.
|
||||
*
|
||||
* A `phase` prints as `import.<phase>(...)`, and import options follow the source as a second argument.
|
||||
*/
|
||||
function printImportExpression(
|
||||
node: ESTree.ImportExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_NEW || (ctx & CTX_FORBID_CALL) !== 0;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "import", CAT_IDENT, node);
|
||||
|
||||
if (node.phase != null) {
|
||||
writeNoLast(state, ".");
|
||||
write(state, node.phase, CAT_IDENT);
|
||||
}
|
||||
|
||||
write(state, "(", CAT_OTHER);
|
||||
printExpression(node.source, state, PREC_COMMA, CTX_NONE);
|
||||
if (node.options != null) {
|
||||
write(state, ", ", CAT_OTHER);
|
||||
printExpression(node.options, state, PREC_COMMA, CTX_NONE);
|
||||
}
|
||||
write(state, ")", CAT_OTHER);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* A `ChainExpression` only records how far an optional chain reaches, so with nothing pressing on it
|
||||
* the node prints straight through with the outer precedence and context.
|
||||
*
|
||||
* From `PREC_POSTFIX` upwards, or under `CTX_FORBID_CALL`, the whole chain is parenthesized
|
||||
* and printed from `PREC_LOWEST` - the chain has to stop at the paren, instead of running on
|
||||
* into the enclosing member access or `new`.
|
||||
*/
|
||||
function printChainExpression(
|
||||
node: ESTree.ChainExpression,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const wrap = precedence >= PREC_POSTFIX || (ctx & CTX_FORBID_CALL) !== 0;
|
||||
if (wrap) {
|
||||
write(state, "(", CAT_OTHER);
|
||||
printExpression(node.expression, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ")", CAT_OTHER);
|
||||
} else {
|
||||
printExpression(node.expression, state, precedence, ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Functions.
|
||||
|
||||
import { typeAssertIs } from "../asserts.ts";
|
||||
import { printBindingPattern } from "./binding_pattern.ts";
|
||||
import {
|
||||
CAT_IDENT,
|
||||
CAT_OTHER,
|
||||
CAT_START_OF_STMT,
|
||||
debugAssertLastFresh,
|
||||
write,
|
||||
writeNoLast,
|
||||
writeWithMap,
|
||||
} from "./write.ts";
|
||||
import { printDecorators } from "./class.ts";
|
||||
import { printSpaceBeforeIdentifier } from "./space.ts";
|
||||
import { printIndent } from "./indent.ts";
|
||||
import { printDirectivesAndStatements } from "./statement.ts";
|
||||
import { printTypeAnnotation, printTypeParameters } from "./typescript.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Print a function declaration or expression, from `async` through to the closing brace of its body.
|
||||
*
|
||||
* A function expression is parenthesized where the statement or an `export default` starts with it,
|
||||
* since it would otherwise be read as a declaration.
|
||||
*/
|
||||
export function printFunction(node: ESTree.Function, state: State): void {
|
||||
let wrap = false;
|
||||
if (node.type === "FunctionExpression") {
|
||||
debugAssertLastFresh(state);
|
||||
// `CAT_START_OF_STMT` or `CAT_START_OF_DEFAULT_EXPORT`, which are adjacent - see `write.ts`
|
||||
wrap = (state.last | 1) === CAT_START_OF_STMT;
|
||||
}
|
||||
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
// The node's mapping goes on whichever of these is written first
|
||||
const declare = TS && node.declare;
|
||||
if (declare) {
|
||||
writeWithMap(state, "declare ", CAT_OTHER, node);
|
||||
write(state, node.async ? "async function" : "function", CAT_IDENT);
|
||||
} else {
|
||||
writeWithMap(state, node.async ? "async function" : "function", CAT_IDENT, node);
|
||||
}
|
||||
|
||||
if (node.generator) write(state, "* ", CAT_OTHER);
|
||||
|
||||
if (node.id != null) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, node.id.name, CAT_IDENT, node.id);
|
||||
}
|
||||
|
||||
if (TS) printTypeParameters(node.typeParameters, state);
|
||||
|
||||
printParenParams(node.params, state);
|
||||
|
||||
if (TS && node.returnType != null) printTypeAnnotation(node.returnType, state);
|
||||
|
||||
if (node.body != null) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printFunctionBody(node.body, state);
|
||||
} else {
|
||||
write(state, ";", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a parameter list in parentheses.
|
||||
*/
|
||||
export function printParenParams(params: ESTree.ParamPattern[], state: State): void {
|
||||
// `(params)`, as a single write when there are none
|
||||
if (params.length === 0) {
|
||||
write(state, "()", CAT_OTHER);
|
||||
return;
|
||||
}
|
||||
|
||||
write(state, "(", CAT_OTHER);
|
||||
printParams(params, state);
|
||||
write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the parameters themselves, without the parentheses around them.
|
||||
*
|
||||
* TypeScript parameter properties carry the modifiers which make them a property as well as a
|
||||
* parameter, and decorators can appear on either kind.
|
||||
*/
|
||||
function printParams(params: ESTree.ParamPattern[], state: State): void {
|
||||
const { length } = params;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
|
||||
const param = params[i];
|
||||
|
||||
const { decorators } = param;
|
||||
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
|
||||
|
||||
if (TS && param.type === "TSParameterProperty") {
|
||||
if (param.accessibility != null) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeNoLast(state, param.accessibility);
|
||||
write(state, " ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (param.override) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "override ", CAT_OTHER);
|
||||
}
|
||||
if (param.readonly) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
write(state, "readonly ", CAT_OTHER);
|
||||
}
|
||||
|
||||
printBindingPattern(param.parameter, state);
|
||||
} else {
|
||||
// The `TS &&` above defeats narrowing; `TSParameterProperty` was excluded by the check
|
||||
typeAssertIs<Exclude<typeof param, ESTree.TSParameterProperty>>(param);
|
||||
printBindingPattern(param, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a function body in braces, empty ones tight.
|
||||
*
|
||||
* A function body has a directive prologue, so it goes through the same printer as a program does,
|
||||
* rather than through the block statement printer.
|
||||
*/
|
||||
export function printFunctionBody(body: ESTree.FunctionBody, state: State): void {
|
||||
// `body` is a BlockStatement holding directives + statements.
|
||||
const statements = body.body;
|
||||
if (statements.length === 0) {
|
||||
writeWithMap(state, "{}", CAT_OTHER, body);
|
||||
return;
|
||||
}
|
||||
|
||||
writeWithMap(state, "{\n", CAT_OTHER, body);
|
||||
state.indentLevel++;
|
||||
printDirectivesAndStatements(statements, state);
|
||||
state.indentLevel--;
|
||||
printIndent(state);
|
||||
|
||||
write(state, "}", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an arrow function's parameter list along with its `=>`.
|
||||
*
|
||||
* Oxc keeps the parentheses even around a lone parameter, so there is no single-parameter form.
|
||||
*/
|
||||
export function printParenParamsArrow(params: ESTree.ParamPattern[], state: State): void {
|
||||
// `(params) => `, as a single write when there are none
|
||||
if (params.length === 0) {
|
||||
write(state, "() => ", CAT_OTHER);
|
||||
return;
|
||||
}
|
||||
|
||||
write(state, "(", CAT_OTHER);
|
||||
printParams(params, state);
|
||||
write(state, ") => ", CAT_OTHER);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Indentation.
|
||||
//
|
||||
// One level's worth of indentation is a string, and the strings for each level are cached,
|
||||
// since the same handful of levels are printed over and over.
|
||||
//
|
||||
// The cache is not this build's. It is created once per process alongside `State`, which carries it here,
|
||||
// so the levels one build has grown are there for the next one - see `state.ts`.
|
||||
|
||||
import { CAT_OTHER, write } from "./write.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
|
||||
/**
|
||||
* Write the indentation for the current level, or the pending single space in place of it.
|
||||
*
|
||||
* Comment printing and inline statement bodies ask for a space where an indent would go,
|
||||
* which keeps `if (x) foo()` on one line.
|
||||
*/
|
||||
export function printIndent(state: State): void {
|
||||
if (state.pendingIndentAsSpace) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
state.pendingIndentAsSpace = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const level = state.indentLevel;
|
||||
if (level > 0) {
|
||||
// Indentation is validated to contain only spaces and tabs, neither of which any reader
|
||||
// of `last` distinguishes, so the category is a constant
|
||||
const { indents } = state;
|
||||
write(state, level < indents.length ? indents[level] : growIndents(state, level), CAT_OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the indent cache up to `level` and return the indent for it.
|
||||
*
|
||||
* Separate from `printIndent` so the cached lookup there stays small enough to inline.
|
||||
*/
|
||||
function growIndents(state: State, level: number): string {
|
||||
const { indents, indentString } = state;
|
||||
|
||||
let { length } = indents;
|
||||
let indent = indents[length - 1];
|
||||
for (; length <= level; length++) {
|
||||
indent += indentString;
|
||||
// Force the cons string flat.
|
||||
// That costs here, but it is appended to the output many times afterwards.
|
||||
indent.charCodeAt(0);
|
||||
indents.push(indent);
|
||||
}
|
||||
|
||||
return indent;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// A fast JavaScript code generator from an ESTree-compliant AST.
|
||||
//
|
||||
// A faithful port of Oxc's Rust `oxc_codegen` crate (pretty-printing mode).
|
||||
// Output is byte-identical to `oxc_codegen` with default options (tab indentation, double quotes, comments off).
|
||||
//
|
||||
// The printer is one function per construct, split across this directory by area.
|
||||
// `printStatement` in `statement.ts` and `printExpression` in `expression.ts` are the two dispatch
|
||||
// points the rest hangs off.
|
||||
//
|
||||
// Reference: `oxc/crates/oxc_codegen/src/{gen.rs,lib.rs,str.rs,binary_expr_visitor.rs}`
|
||||
|
||||
import { debugAssert, typeAssertIs } from "../asserts.ts";
|
||||
import { emitMappings } from "./source_map.ts";
|
||||
import { printProgram, printStatement } from "./statement.ts";
|
||||
|
||||
import type { Options } from "./options.ts";
|
||||
import type { State } from "../state.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Print `node`, returning the generated code.
|
||||
*
|
||||
* One of these exists per build. The package entry point picks the build and calls it -
|
||||
* callers of the package never reach this directly.
|
||||
*
|
||||
* @param state - Created by the entry point, so that all 4 builds share one class and so one object shape
|
||||
* @param options - The same options `state` was created from, for the parts only this build acts on
|
||||
* @returns Generated code
|
||||
*/
|
||||
export function printSync(node: ESTree.Node, state: State, options: Options): string {
|
||||
if (node.type === "Program") {
|
||||
printProgram(node, state);
|
||||
} else {
|
||||
typeAssertIs<ESTree.Statement>(node);
|
||||
printStatement(node, state);
|
||||
}
|
||||
|
||||
// This is removed by minifier in non-sourcemap builds
|
||||
if (SOURCEMAPS) {
|
||||
debugAssert(
|
||||
options.sourceMap != null,
|
||||
"`options.sourceMap` should be defined when `SOURCEMAPS` is `true`",
|
||||
);
|
||||
|
||||
emitMappings(state, options.sourceMap);
|
||||
}
|
||||
|
||||
return state.output;
|
||||
}
|
||||
|
||||
export type { Mapping, Options, Position, SourceMapGenerator } from "./options.ts";
|
||||
@@ -0,0 +1,213 @@
|
||||
// JSX.
|
||||
// Port of `gen.rs` JSX section.
|
||||
|
||||
import { CAT_OTHER, write, writeNoLast, writeWithMap, writeWithMapNoLast } from "./write.ts";
|
||||
import { printExpression } from "./expression.ts";
|
||||
import { CTX_NONE } from "./operators.ts";
|
||||
import { PREC_COMMA, PREC_LOWEST } from "./precedence.ts";
|
||||
import { printTypeArguments } from "./typescript.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type { UnknownNode } from "./types.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Print a JSX element, its attributes and its children.
|
||||
*
|
||||
* An element with no closing element is self-closing and gets ` />`, which is also the only place
|
||||
* a space is required before the slash.
|
||||
*/
|
||||
export function printJSXElement(node: ESTree.JSXElement, state: State): void {
|
||||
const { openingElement } = node;
|
||||
|
||||
writeWithMapNoLast(state, "<", openingElement);
|
||||
printJSXElementName(openingElement.name, state);
|
||||
|
||||
if (TS) printTypeArguments(openingElement.typeArguments, state);
|
||||
|
||||
const { attributes } = openingElement;
|
||||
const { length } = attributes;
|
||||
for (let i = 0; i < length; i++) {
|
||||
writeNoLast(state, " ");
|
||||
const attribute = attributes[i];
|
||||
if (attribute.type === "JSXSpreadAttribute") {
|
||||
write(state, "{...", CAT_OTHER);
|
||||
printExpression(attribute.argument, state, PREC_COMMA, CTX_NONE);
|
||||
writeNoLast(state, "}");
|
||||
} else {
|
||||
printJSXAttribute(attribute, state);
|
||||
}
|
||||
}
|
||||
|
||||
const { closingElement } = node;
|
||||
if (closingElement == null) {
|
||||
write(state, " />", CAT_OTHER);
|
||||
return;
|
||||
}
|
||||
|
||||
writeNoLast(state, ">");
|
||||
|
||||
const { children } = node;
|
||||
const childCount = children.length;
|
||||
for (let i = 0; i < childCount; i++) {
|
||||
printJSXChild(children[i], state);
|
||||
}
|
||||
|
||||
writeWithMapNoLast(state, "</", closingElement);
|
||||
printJSXElementName(closingElement.name, state);
|
||||
write(state, ">", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the name in an opening or closing tag - a name, a namespaced name, a member chain, or `this`.
|
||||
*/
|
||||
function printJSXElementName(
|
||||
node: ESTree.JSXElementName | ESTree.ThisExpression | UnknownNode,
|
||||
state: State,
|
||||
): void {
|
||||
switch (node.type) {
|
||||
case "JSXIdentifier":
|
||||
writeWithMapNoLast(state, node.name, node);
|
||||
break;
|
||||
case "JSXMemberExpression":
|
||||
printJSXElementName(node.object, state);
|
||||
writeNoLast(state, ".");
|
||||
printJSXElementName(node.property, state);
|
||||
break;
|
||||
case "JSXNamespacedName":
|
||||
writeWithMapNoLast(state, node.namespace.name, node);
|
||||
writeNoLast(state, ":");
|
||||
writeNoLast(state, node.name.name);
|
||||
break;
|
||||
case "ThisExpression":
|
||||
writeWithMapNoLast(state, "this", node);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown JSX name type: ${node.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print one attribute, with its value where it has one - a bare attribute is `true`.
|
||||
*/
|
||||
function printJSXAttribute(node: ESTree.JSXAttribute, state: State): void {
|
||||
// Attribute names never update `last`.
|
||||
// Everything which can follow one - `=`, the next attribute's leading space, `>`, ` />` -
|
||||
// is punctuation needing no separation from what precedes it, and nothing between here
|
||||
// and the next real write reads `last`.
|
||||
const { name } = node;
|
||||
if (name.type === "JSXNamespacedName") {
|
||||
writeWithMapNoLast(state, name.namespace.name, name);
|
||||
writeNoLast(state, ":");
|
||||
writeNoLast(state, name.name.name);
|
||||
} else {
|
||||
writeWithMapNoLast(state, name.name, name);
|
||||
}
|
||||
|
||||
const { value } = node;
|
||||
if (value != null) {
|
||||
writeNoLast(state, "=");
|
||||
printJSXAttributeValue(value, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an attribute's value.
|
||||
*
|
||||
* A string value is printed from its raw text, not through the string escaper, because JSX attribute strings
|
||||
* have no escape sequences - the quote is picked to suit the contents instead.
|
||||
*/
|
||||
function printJSXAttributeValue(node: ESTree.JSXAttributeValue | UnknownNode, state: State): void {
|
||||
switch (node.type) {
|
||||
case "Literal": {
|
||||
// JSX strings have no escape sequences and HTML entities are not decoded by parser at present.
|
||||
// Print the raw source text (matching Oxc), choosing only the quote.
|
||||
//
|
||||
// Once HTML entities are decoded (which they should be), this check stops being enough -
|
||||
// `<Foo bar="'"" />` decodes to a value holding both quote characters, which no choice
|
||||
// of quote mark can contain, so the text will have to be escaped.
|
||||
// The cases in `test/print.test.ts` pin the current output, so they will fail the moment
|
||||
// the parser starts decoding, rather than the change slipping through here unnoticed.
|
||||
const { raw } = node;
|
||||
const text = raw != null ? raw.slice(1, -1) : String(node.value);
|
||||
|
||||
const quote = text.includes('"') ? "'" : '"';
|
||||
writeWithMapNoLast(state, quote, node);
|
||||
writeNoLast(state, text);
|
||||
writeNoLast(state, quote);
|
||||
break;
|
||||
}
|
||||
case "JSXExpressionContainer":
|
||||
printJSXExpressionContainer(node, state);
|
||||
break;
|
||||
case "JSXElement":
|
||||
printJSXElement(node, state);
|
||||
break;
|
||||
case "JSXFragment":
|
||||
printJSXFragment(node, state);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown JSX attribute value type: ${node.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a `{ … }` container, which may hold nothing but a comment.
|
||||
*/
|
||||
function printJSXExpressionContainer(node: ESTree.JSXExpressionContainer, state: State): void {
|
||||
write(state, "{", CAT_OTHER);
|
||||
|
||||
if (node.expression.type !== "JSXEmptyExpression") {
|
||||
printExpression(node.expression, state, PREC_LOWEST, CTX_NONE);
|
||||
}
|
||||
|
||||
// `}` needs no category. What follows it in either caller context (attribute value, child)
|
||||
// is punctuation or JSX text, none of which needs separating from it, and nothing reads
|
||||
// `last` before the next real write.
|
||||
writeNoLast(state, "}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a `<>...</>` fragment.
|
||||
*/
|
||||
export function printJSXFragment(node: ESTree.JSXFragment, state: State): void {
|
||||
writeWithMapNoLast(state, "<>", node.openingFragment);
|
||||
|
||||
const { children } = node;
|
||||
const { length } = children;
|
||||
for (let i = 0; i < length; i++) {
|
||||
printJSXChild(children[i], state);
|
||||
}
|
||||
|
||||
writeWithMap(state, "</>", CAT_OTHER, node.closingFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print one child of an element or fragment.
|
||||
*
|
||||
* Text is written from its raw source, since whitespace between elements is significant
|
||||
* and any entities in it must survive untouched.
|
||||
*/
|
||||
function printJSXChild(node: ESTree.JSXChild | UnknownNode, state: State): void {
|
||||
switch (node.type) {
|
||||
case "JSXText":
|
||||
writeWithMapNoLast(state, node.raw != null ? node.raw : node.value, node);
|
||||
break;
|
||||
case "JSXExpressionContainer":
|
||||
printJSXExpressionContainer(node, state);
|
||||
break;
|
||||
case "JSXElement":
|
||||
printJSXElement(node, state);
|
||||
break;
|
||||
case "JSXFragment":
|
||||
printJSXFragment(node, state);
|
||||
break;
|
||||
case "JSXSpreadChild":
|
||||
write(state, "{...", CAT_OTHER);
|
||||
printExpression(node.expression, state, PREC_LOWEST, CTX_NONE);
|
||||
writeNoLast(state, "}");
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown JSX child type: ${node.type}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// Literals.
|
||||
|
||||
import { typeAssertIs } from "../asserts.ts";
|
||||
import {
|
||||
CAT_IDENT,
|
||||
CAT_INT_DIGIT,
|
||||
CAT_OP_UN_NEG,
|
||||
CAT_OTHER,
|
||||
CAT_REGEX_SLASH,
|
||||
write,
|
||||
writeNoLast,
|
||||
writeWithMap,
|
||||
writeWithMapNoLast,
|
||||
} from "./write.ts";
|
||||
import { printSpaceBeforeIdentifier, printSpaceBeforeOperator } from "./space.ts";
|
||||
import { printNonNegativeFloat } from "./number.ts";
|
||||
import { CTX_TYPESCRIPT } from "./operators.ts";
|
||||
import { PREC_PREFIX } from "./precedence.ts";
|
||||
import { printString } from "./string.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type { LiteralExtras } from "./types.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* The literal types, which all share `type: "Literal"` and are told apart by `typeof value`.
|
||||
*/
|
||||
type LiteralNode =
|
||||
| ESTree.BooleanLiteral
|
||||
| ESTree.NullLiteral
|
||||
| ESTree.NumericLiteral
|
||||
| ESTree.BigIntLiteral
|
||||
| ESTree.RegExpLiteral
|
||||
| ESTree.StringLiteral;
|
||||
|
||||
/**
|
||||
* Print any ESTree `Literal`, which is one node type covering strings, numbers, booleans, `null`,
|
||||
* regexes and bigints.
|
||||
*
|
||||
* The kind is told apart by the type of `value` and by which extra property is present,
|
||||
* since the node itself does not say.
|
||||
*/
|
||||
export function printLiteral(
|
||||
node: LiteralNode,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
// Ordered for the hot path - `value` is a `RegExp` / `null` for regexes and a `BigInt` / `null` for bigints,
|
||||
// so a string or number `value` identifies a plain string/numeric literal without touching `regex` / `bigint` first.
|
||||
const { value } = node;
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
printString(state, value, node);
|
||||
break;
|
||||
case "number":
|
||||
typeAssertIs<ESTree.NumericLiteral>(node);
|
||||
printNumericLiteral(node, state, precedence, ctx);
|
||||
break;
|
||||
case "boolean":
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, value ? "true" : "false", CAT_IDENT, node);
|
||||
break;
|
||||
default:
|
||||
typeAssertIs<LiteralExtras>(node);
|
||||
if (node.regex != null) {
|
||||
typeAssertIs<ESTree.RegExpLiteral>(node);
|
||||
printRegExpLiteral(node, state);
|
||||
} else if (node.bigint != null) {
|
||||
typeAssertIs<ESTree.BigIntLiteral>(node);
|
||||
printBigIntLiteral(node, state, precedence);
|
||||
} else {
|
||||
// `null`
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "null", CAT_IDENT, node);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a numeric literal, parenthesizing a negative one where the position needs it.
|
||||
*
|
||||
* A negative number is a unary minus applied to a positive literal, so it needs a space
|
||||
* or parentheses in the places any other unary minus would, and `-0` has to keep its sign.
|
||||
*
|
||||
* In TypeScript positions the source text is printed back rather than the shortest form,
|
||||
* so a type keeps the digits it was written with.
|
||||
*/
|
||||
function printNumericLiteral(
|
||||
node: ESTree.NumericLiteral,
|
||||
state: State,
|
||||
precedence: number,
|
||||
ctx: number,
|
||||
): void {
|
||||
const { value } = node;
|
||||
if ((ctx & CTX_TYPESCRIPT) !== 0 && node.raw != null) {
|
||||
// A number's raw text can only end 3 ways: a digit, a hex letter, or a trailing `.` as in `1.`.
|
||||
// The first two are identifier characters and the third is not, so testing for the `.` decides it -
|
||||
// no character classification needed.
|
||||
// `CAT_IDENT` rather than `CAT_INT_DIGIT` - raw text only prints inside a TS type,
|
||||
// where no member `.` can follow, so there is no `0 .toExponential()` hazard to separate.
|
||||
const { raw } = node;
|
||||
writeWithMap(state, raw, raw[raw.length - 1] === "." ? CAT_OTHER : CAT_IDENT, node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value > 0 && value < Infinity) {
|
||||
// Finite and positive: the common case, and the same branch the chain
|
||||
// below would reach, without the two builtin calls.
|
||||
printSpaceBeforeIdentifier(state);
|
||||
printNonNegativeFloat(state, value, node);
|
||||
} else if (Number.isNaN(value)) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "NaN", CAT_IDENT, node);
|
||||
} else if (!Number.isFinite(value)) {
|
||||
const negative = value < 0;
|
||||
const wrap = negative && precedence >= PREC_PREFIX;
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
|
||||
if (negative) {
|
||||
printSpaceBeforeOperator(state, CAT_OP_UN_NEG);
|
||||
writeNoLast(state, "-");
|
||||
} else {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
}
|
||||
writeWithMap(state, "Infinity", CAT_IDENT, node);
|
||||
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
} else if (Object.is(value, 0)) {
|
||||
// +0 exactly - `Object.is` rejects `-0`, which the negative paths below print as `-0`
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "0", CAT_INT_DIGIT, node);
|
||||
} else if (precedence >= PREC_PREFIX) {
|
||||
writeNoLast(state, "(-");
|
||||
printNonNegativeFloat(state, -value, node);
|
||||
write(state, ")", CAT_OTHER);
|
||||
} else {
|
||||
printSpaceBeforeOperator(state, CAT_OP_UN_NEG);
|
||||
writeNoLast(state, "-");
|
||||
printNonNegativeFloat(state, -value, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a regex literal from its pattern and flags.
|
||||
*
|
||||
* A regex with no flags ends in `/`, which is recorded as its own category, because a `/`
|
||||
* immediately after it would open a comment.
|
||||
*/
|
||||
function printRegExpLiteral(node: ESTree.RegExpLiteral, state: State): void {
|
||||
// Neither of the separating spaces below can be needed in pretty mode, so the check is not made.
|
||||
// Both guard against a regex being written immediately after something, and nothing can be
|
||||
// immediately before a regex here: every operator which could put one after a `/` or a `<` is
|
||||
// written space padded, so `last` is always the space.
|
||||
//
|
||||
// A minified mode would stop padding operators and make both reachable again, so this code would
|
||||
// need to be restored. `CAT_REGEX_SLASH` is left in place for that - the code is still written
|
||||
// after a flagless regex, and it still sits in the range `printSpaceBeforeIdentifier` tests,
|
||||
// which costs nothing and is what keeps this a 4 line restoration.
|
||||
//
|
||||
// debugAssertLastFresh(state);
|
||||
// const { last } = state;
|
||||
// if (last === CAT_REGEX_SLASH || (last === CAT_LT && /^script/i.test(pattern.slice(0, 6)))) {
|
||||
// write(state, " ", CAT_OTHER);
|
||||
// }
|
||||
//
|
||||
// * `last === CAT_REGEX_SLASH` keeps `/a//b/` from lexing as a line comment
|
||||
// * `CAT_LT` arm keeps `<` followed by `/script...` from closing a host `<script>` element.
|
||||
|
||||
writeWithMapNoLast(state, "/", node);
|
||||
writeNoLast(state, node.regex.pattern);
|
||||
|
||||
// `CAT_REGEX_SLASH` rather than `CAT_OTHER`. It means "a regex just closed", which is what the
|
||||
// commented-out check above would read. With flags, the flags are what `last` describes instead.
|
||||
const { flags } = node.regex;
|
||||
if (flags === "") {
|
||||
write(state, "/", CAT_REGEX_SLASH);
|
||||
} else {
|
||||
writeNoLast(state, "/");
|
||||
write(state, flags, CAT_IDENT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a bigint literal, parenthesizing a negative one where the position needs it,
|
||||
* same as `printNumericLiteral` does.
|
||||
*/
|
||||
function printBigIntLiteral(node: ESTree.BigIntLiteral, state: State, precedence: number): void {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
const value = node.bigint;
|
||||
if (value.startsWith("-") && precedence >= PREC_PREFIX) {
|
||||
writeWithMapNoLast(state, "(", node);
|
||||
writeNoLast(state, value);
|
||||
write(state, "n)", CAT_OTHER);
|
||||
} else {
|
||||
writeWithMapNoLast(state, value, node);
|
||||
write(state, "n", CAT_IDENT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// Modules.
|
||||
|
||||
import {
|
||||
CAT_IDENT,
|
||||
CAT_OTHER,
|
||||
CAT_START_OF_DEFAULT_EXPORT,
|
||||
write,
|
||||
writeNoLast,
|
||||
writeWithMap,
|
||||
} from "./write.ts";
|
||||
import { printClass } from "./class.ts";
|
||||
import { printExpression } from "./expression.ts";
|
||||
import { printFunction } from "./function.ts";
|
||||
import { printSpaceBeforeIdentifier } from "./space.ts";
|
||||
import { printIndent } from "./indent.ts";
|
||||
import { CTX_NONE } from "./operators.ts";
|
||||
import { PREC_COMMA } from "./precedence.ts";
|
||||
import { printVariableDeclaration } from "./statement.ts";
|
||||
import { printString } from "./string.ts";
|
||||
import {
|
||||
printTSEnumDeclaration,
|
||||
printTSImportEqualsDeclaration,
|
||||
printTSInterfaceDeclaration,
|
||||
printTSModuleDeclaration,
|
||||
printTSTypeAliasDeclaration,
|
||||
} from "./typescript.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type { ExportNamedDeclarationNode } from "./types.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Print an `import` statement, in any of its forms.
|
||||
*
|
||||
* Default, namespace and named specifiers can be combined, and the named ones are the only kind in
|
||||
* braces, so the braces are opened and closed around a run rather than around the whole list.
|
||||
*/
|
||||
export function printImportDeclaration(node: ESTree.ImportDeclaration, state: State): void {
|
||||
printIndent(state);
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "import", CAT_IDENT, node);
|
||||
|
||||
if (TS && node.importKind === "type") write(state, " type", CAT_IDENT);
|
||||
|
||||
if (node.phase != null) {
|
||||
writeNoLast(state, " ");
|
||||
write(state, node.phase, CAT_IDENT);
|
||||
}
|
||||
|
||||
const { specifiers } = node;
|
||||
const { length } = specifiers;
|
||||
if (length === 0) {
|
||||
// `import "source";`
|
||||
write(state, " ", CAT_OTHER);
|
||||
printString(state, node.source.value, node.source);
|
||||
printImportAttributes(node.attributes, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
return;
|
||||
}
|
||||
|
||||
let inBlock = false;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const specifier = specifiers[i];
|
||||
switch (specifier.type) {
|
||||
case "ImportDefaultSpecifier":
|
||||
if (inBlock) {
|
||||
write(state, " },", CAT_OTHER);
|
||||
inBlock = false;
|
||||
} else if (i === 0) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
} else {
|
||||
write(state, ", ", CAT_OTHER);
|
||||
}
|
||||
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, specifier.local.name, CAT_IDENT, specifier);
|
||||
|
||||
if (i === length - 1) write(state, " ", CAT_OTHER);
|
||||
|
||||
break;
|
||||
case "ImportNamespaceSpecifier":
|
||||
if (inBlock) {
|
||||
write(state, " },", CAT_OTHER);
|
||||
inBlock = false;
|
||||
} else if (i === 0) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
} else {
|
||||
write(state, ", ", CAT_OTHER);
|
||||
}
|
||||
|
||||
writeWithMap(state, "* as ", CAT_OTHER, specifier);
|
||||
writeWithMap(state, specifier.local.name, CAT_IDENT, specifier.local);
|
||||
write(state, " ", CAT_OTHER);
|
||||
break;
|
||||
default: {
|
||||
// ImportSpecifier
|
||||
if (inBlock) {
|
||||
write(state, ", ", CAT_OTHER);
|
||||
} else {
|
||||
if (i !== 0) {
|
||||
write(state, ",", CAT_OTHER);
|
||||
}
|
||||
inBlock = true;
|
||||
write(state, " { ", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (TS && specifier.importKind === "type") write(state, "type ", CAT_OTHER);
|
||||
|
||||
const importedName = moduleExportName(specifier.imported, state);
|
||||
const { local } = specifier;
|
||||
if (importedName !== local.name) {
|
||||
write(state, " as ", CAT_OTHER);
|
||||
writeWithMap(state, local.name, CAT_IDENT, local);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write(state, inBlock ? " } from " : "from ", CAT_OTHER);
|
||||
printString(state, node.source.value, node.source);
|
||||
printImportAttributes(node.attributes, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a `with { … }` clause, and nothing at all when there are no attributes.
|
||||
*
|
||||
* An empty clause is written where the AST says the source had one, since `with {}`
|
||||
* and no clause are different programs.
|
||||
*/
|
||||
function printImportAttributes(
|
||||
attributes: ESTree.ImportAttribute[] | null | undefined,
|
||||
state: State,
|
||||
): void {
|
||||
if (attributes == null) return;
|
||||
|
||||
const { length } = attributes;
|
||||
if (length === 0) return;
|
||||
|
||||
write(state, " with { ", CAT_OTHER);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
|
||||
const attribute = attributes[i];
|
||||
const { key } = attribute;
|
||||
if (key.type === "Identifier") {
|
||||
writeWithMap(state, key.name, CAT_IDENT, key);
|
||||
} else {
|
||||
printString(state, key.value, key);
|
||||
}
|
||||
|
||||
write(state, ": ", CAT_OTHER);
|
||||
|
||||
printString(state, attribute.value.value, attribute.value);
|
||||
}
|
||||
|
||||
write(state, " }", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the name on one side of an import or export specifier, and return it.
|
||||
*
|
||||
* The name comes back because the caller compares the two sides to decide whether an `as` clause
|
||||
* is needed at all, and it is cheaper to return it than to work it out twice.
|
||||
*
|
||||
* @returns The name printed
|
||||
*/
|
||||
function moduleExportName(node: ESTree.ModuleExportName, state: State): string {
|
||||
if (node.type === "Identifier") {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, node.name, CAT_IDENT, node);
|
||||
return node.name;
|
||||
}
|
||||
|
||||
printString(state, node.value, node);
|
||||
return node.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an `export` statement which names what it exports, whether that is a declaration,
|
||||
* a list of specifiers, or a list re-exported from another module.
|
||||
*/
|
||||
export function printExportNamedDeclaration(node: ExportNamedDeclarationNode, state: State): void {
|
||||
printIndent(state);
|
||||
|
||||
writeWithMap(state, "export ", CAT_OTHER, node);
|
||||
|
||||
const { declaration } = node;
|
||||
if (declaration != null) {
|
||||
switch (declaration.type) {
|
||||
case "VariableDeclaration":
|
||||
printVariableDeclaration(declaration, state, CTX_NONE);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "FunctionDeclaration":
|
||||
/* IF TS */
|
||||
case "TSDeclareFunction":
|
||||
/* END_IF */
|
||||
printFunction(declaration, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "ClassDeclaration":
|
||||
printClass(declaration, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
/* IF TS */
|
||||
case "TSModuleDeclaration":
|
||||
printTSModuleDeclaration(declaration, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSInterfaceDeclaration":
|
||||
printTSInterfaceDeclaration(declaration, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSEnumDeclaration":
|
||||
printTSEnumDeclaration(declaration, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSTypeAliasDeclaration":
|
||||
printTSTypeAliasDeclaration(declaration, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSImportEqualsDeclaration":
|
||||
printTSImportEqualsDeclaration(declaration, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
/* END_IF */
|
||||
default:
|
||||
throw new Error(`Unknown export declaration type: ${declaration.type}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (TS && node.exportKind === "type") write(state, "type ", CAT_OTHER);
|
||||
|
||||
write(state, "{", CAT_OTHER);
|
||||
|
||||
const { specifiers } = node;
|
||||
const { length } = specifiers;
|
||||
if (length > 0) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
const specifier = specifiers[i];
|
||||
if (TS && specifier.exportKind === "type") {
|
||||
write(state, "type ", CAT_OTHER);
|
||||
}
|
||||
const localName = moduleExportName(specifier.local, state);
|
||||
const exportedName =
|
||||
specifier.exported.type === "Identifier"
|
||||
? specifier.exported.name
|
||||
: specifier.exported.value;
|
||||
if (localName !== exportedName) {
|
||||
write(state, " as ", CAT_OTHER);
|
||||
moduleExportName(specifier.exported, state);
|
||||
}
|
||||
}
|
||||
write(state, " ", CAT_OTHER);
|
||||
}
|
||||
|
||||
write(state, "}", CAT_OTHER);
|
||||
|
||||
if (node.source != null) {
|
||||
write(state, " from ", CAT_OTHER);
|
||||
printString(state, node.source.value, node.source);
|
||||
printImportAttributes(node.attributes, state);
|
||||
}
|
||||
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print `export * from "…"`, with the `as name` form where the AST has one.
|
||||
*/
|
||||
export function printExportAllDeclaration(node: ESTree.ExportAllDeclaration, state: State): void {
|
||||
printIndent(state);
|
||||
|
||||
writeWithMap(
|
||||
state,
|
||||
TS && node.exportKind === "type" ? "export type *" : "export *",
|
||||
CAT_OTHER,
|
||||
node,
|
||||
);
|
||||
|
||||
if (node.exported != null) {
|
||||
write(state, " as ", CAT_OTHER);
|
||||
moduleExportName(node.exported, state);
|
||||
}
|
||||
|
||||
write(state, " from ", CAT_OTHER);
|
||||
printString(state, node.source.value, node.source);
|
||||
printImportAttributes(node.attributes, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print `export default ...`.
|
||||
*
|
||||
* The output position is recorded before the value is printed, so that a function or class expression
|
||||
* starting there knows to parenthesize itself.
|
||||
*/
|
||||
export function printExportDefaultDeclaration(
|
||||
node: ESTree.ExportDefaultDeclaration,
|
||||
state: State,
|
||||
): void {
|
||||
printIndent(state);
|
||||
|
||||
writeWithMap(state, "export default ", CAT_OTHER, node);
|
||||
|
||||
const { declaration } = node;
|
||||
switch (declaration.type) {
|
||||
case "FunctionDeclaration":
|
||||
/* IF TS */
|
||||
case "TSDeclareFunction":
|
||||
/* END_IF */
|
||||
printFunction(declaration, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "ClassDeclaration":
|
||||
printClass(declaration, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
/* IF TS */
|
||||
case "TSInterfaceDeclaration":
|
||||
printTSInterfaceDeclaration(declaration, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
/* END_IF */
|
||||
default:
|
||||
state.last = CAT_START_OF_DEFAULT_EXPORT;
|
||||
printExpression(declaration, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Number printing (port of `oxc_ecmascript::number_literal`, Terser-derived).
|
||||
//
|
||||
// The shortest text for a number is the shortest of a handful of forms, and which of them are even possible
|
||||
// follows from what `String` gave - an integer can go hexadecimal or trade trailing zeros for an exponent,
|
||||
// a fraction can drop its leading zero or trade leading zeros for one, and only a number `String` already wrote
|
||||
// in exponent notation can fold its point into the exponent. So the forms are tried by separate functions,
|
||||
// rather than by one which tries all of them on every number.
|
||||
//
|
||||
// Each form is written to the output in pieces, so no candidate string is ever built.
|
||||
|
||||
import { debugAssert } from "../asserts.ts";
|
||||
import {
|
||||
CAT_IDENT,
|
||||
CAT_INT_DIGIT,
|
||||
write,
|
||||
writeNoLast,
|
||||
writeWithMap,
|
||||
writeWithMapNoLast,
|
||||
} from "./write.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Print a non-negative finite number in its shortest form.
|
||||
*
|
||||
* Integers below 1000 are the overwhelming majority, and 3 digits are too few for any other form
|
||||
* to be shorter, so they print straight from `String` with none of the shortening work in the way.
|
||||
*
|
||||
* `CAT_INT_DIGIT` goes only on plain digits. It is what forces the space in `0 .toExponential()`,
|
||||
* and every other form ends in, or contains, a `.`, an `e` or an `x` which separates it
|
||||
* from a following `.` already.
|
||||
*/
|
||||
export function printNonNegativeFloat(state: State, value: number, node: ESTree.Node): void {
|
||||
if (Number.isInteger(value)) {
|
||||
if (value < 1000) {
|
||||
writeWithMap(state, String(value), CAT_INT_DIGIT, node);
|
||||
} else {
|
||||
printShortestInteger(state, value, node);
|
||||
}
|
||||
} else {
|
||||
printShortestFraction(state, value, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an integer of 1000 or more in its shortest form.
|
||||
*
|
||||
* `String` gives plain digits below 1e21 and exponent notation from there up, which have different forms
|
||||
* available to them, so they are separate cases. Both can go hexadecimal.
|
||||
*/
|
||||
function printShortestInteger(state: State, value: number, node: ESTree.Node): void {
|
||||
const formatted = String(value);
|
||||
|
||||
if (value >= 1e21) {
|
||||
debugAssert(formatted.includes("e+"), "`String` gives `e+` notation from 1e21 up");
|
||||
// The `+` always goes, so the text is one shorter than `formatted`
|
||||
if (printHexIfShorter(state, value, formatted.length - 1, node)) return;
|
||||
printExponent(state, formatted, formatted.indexOf("e"), node);
|
||||
return;
|
||||
}
|
||||
|
||||
const { length } = formatted;
|
||||
if (printHexIfShorter(state, value, length, node)) return;
|
||||
|
||||
// A run of trailing zeros as an exponent: `1000` -> `1e3`
|
||||
if (formatted.charCodeAt(length - 1) === 48 /* 0 */) {
|
||||
// The first digit is never a zero, so the run always stops
|
||||
let zeros = 1;
|
||||
while (formatted.charCodeAt(length - 1 - zeros) === 48 /* 0 */) zeros++;
|
||||
|
||||
// Worth it when the `e` and the exponent cost less than the zeros they replace
|
||||
const exponent = String(zeros);
|
||||
if (exponent.length + 1 < zeros) {
|
||||
writeWithMapNoLast(state, formatted.slice(0, length - zeros), node);
|
||||
writeNoLast(state, "e");
|
||||
write(state, exponent, CAT_IDENT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
writeWithMap(state, formatted, CAT_INT_DIGIT, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a non-integer in its shortest form.
|
||||
*
|
||||
* `String` gives `0.<digits>` from 1e-7 up to 1, `<digits>.<digits>` from 1 up, and exponent notation
|
||||
* below 1e-7. The first always loses its leading zero, and may trade a run of zeros after the point
|
||||
* for a negative exponent. The second is already as short as it gets.
|
||||
*/
|
||||
function printShortestFraction(state: State, value: number, node: ESTree.Node): void {
|
||||
const formatted = String(value);
|
||||
|
||||
// The trailing-zero form `printShortestInteger` uses can never win here, so it is not tried.
|
||||
// The shortest text for a fraction never ends in a zero digit, and an exponent of at most 3 digits
|
||||
// never ends in the 3 zeros it would take to pay for the `e`.
|
||||
if (DEBUG) {
|
||||
let zeros = 0;
|
||||
while (formatted.charCodeAt(formatted.length - 1 - zeros) === 48 /* 0 */) zeros++;
|
||||
debugAssert(zeros < 3, () => `Fraction text ends in ${zeros} zeros: ${formatted}`);
|
||||
}
|
||||
|
||||
if (formatted.charCodeAt(0) === "0".charCodeAt(0)) {
|
||||
// `0.<digits>`, whose leading zero always goes
|
||||
if (formatted.charCodeAt(2) === "0".charCodeAt(0)) {
|
||||
// A run of zeros straight after the point as a negative exponent: `0.0001` -> `1e-4`.
|
||||
// There is always a non-zero digit for the run to stop at, since the text is the shortest
|
||||
// which reads back as `value`.
|
||||
let start = 3;
|
||||
while (formatted.charCodeAt(start) === "0".charCodeAt(0)) start++;
|
||||
|
||||
// The exponent is the count of digits after the point, which is what shifting the point
|
||||
// past all of them costs, zeros included
|
||||
const exponent = String(formatted.length - 2);
|
||||
|
||||
// Both forms keep the digits, so it comes down to the `e-` and the exponent against the `.`
|
||||
// and the zeros they replace
|
||||
if (exponent.length + 2 < start - 1) {
|
||||
writeWithMapNoLast(state, formatted.slice(start), node);
|
||||
writeNoLast(state, "e-");
|
||||
write(state, exponent, CAT_IDENT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
writeWithMap(state, formatted.slice(1), CAT_IDENT, node);
|
||||
return;
|
||||
}
|
||||
|
||||
const exponentIndex = formatted.indexOf("e");
|
||||
if (exponentIndex === -1) {
|
||||
writeWithMap(state, formatted, CAT_IDENT, node);
|
||||
return;
|
||||
}
|
||||
|
||||
printExponent(state, formatted, exponentIndex, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a number `String` gave in exponent notation, in its shortest form.
|
||||
*
|
||||
* `String` writes exactly one digit before the point, so the digits after it can be folded into the exponent -
|
||||
* `1.5e+21` -> `15e20`. That drops the `.`, so it wins whenever the exponent it leaves is no longer to write
|
||||
* than the one it replaces. The `+` of a positive exponent always goes.
|
||||
*
|
||||
* @param formatted - `String(value)`, which is in exponent notation
|
||||
* @param exponentIndex - Index of the `e` in `formatted`
|
||||
*/
|
||||
function printExponent(
|
||||
state: State,
|
||||
formatted: string,
|
||||
exponentIndex: number,
|
||||
node: ESTree.Node,
|
||||
): void {
|
||||
const exponent =
|
||||
formatted.charCodeAt(exponentIndex + 1) === "+".charCodeAt(0)
|
||||
? formatted.slice(exponentIndex + 2)
|
||||
: formatted.slice(exponentIndex + 1);
|
||||
|
||||
if (exponentIndex > 1) {
|
||||
debugAssert(
|
||||
formatted.charCodeAt(1) === ".".charCodeAt(0),
|
||||
"Exponent notation has 1 digit up front",
|
||||
);
|
||||
|
||||
const folded = String(Number(exponent) - (exponentIndex - 2));
|
||||
if (folded.length <= exponent.length) {
|
||||
writeWithMapNoLast(state, formatted[0], node);
|
||||
writeNoLast(state, formatted.slice(2, exponentIndex));
|
||||
writeNoLast(state, "e");
|
||||
write(state, folded, CAT_IDENT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
writeWithMapNoLast(state, formatted.slice(0, exponentIndex + 1), node);
|
||||
write(state, exponent, CAT_IDENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print `value` in hexadecimal if that is shorter than `length` characters, and report whether it was.
|
||||
*
|
||||
* Hexadecimal has to win back the `0x` in front of it, which takes 13 digits at the very least,
|
||||
* so below that the conversion is not attempted at all.
|
||||
*
|
||||
* That bound is in decimal digits, and the caller with a number in exponent notation passes the length
|
||||
* of that instead, which is shorter - but such a number is at least 1e21, whose 18 hex digits make 20 characters,
|
||||
* so anything the early return skips there could not have won either.
|
||||
*
|
||||
* `BigInt` rather than `value.toString(16)`, whose result the specification leaves implementation-approximated
|
||||
* for every radix but 10, where the output here has to be exact.
|
||||
*
|
||||
* @param length - Length of the shortest text found for `value` so far
|
||||
*/
|
||||
function printHexIfShorter(
|
||||
state: State,
|
||||
value: number,
|
||||
length: number,
|
||||
node: ESTree.Node,
|
||||
): boolean {
|
||||
if (length < 13) return false;
|
||||
|
||||
const hex = BigInt(value).toString(16);
|
||||
if (hex.length + 2 >= length) return false;
|
||||
|
||||
writeWithMapNoLast(state, "0x", node);
|
||||
write(state, hex, CAT_IDENT);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Operator tables.
|
||||
//
|
||||
// The context flags a printer is given about the position it is printing into, the precedence of
|
||||
// each binary and assignment operator, those operators pre-padded with the spaces pretty mode
|
||||
// always puts around them, and the category a unary or update operator leaves in `state.last`.
|
||||
|
||||
import {
|
||||
CAT_OP_UN_NEG,
|
||||
CAT_OP_UN_NOT,
|
||||
CAT_OP_UN_PLUS,
|
||||
CAT_OP_UPD_DEC,
|
||||
CAT_OP_UPD_INC,
|
||||
CAT_OTHER,
|
||||
} from "./write.ts";
|
||||
import { debugAssert } from "../asserts.ts";
|
||||
|
||||
import type { Category } from "./write.ts";
|
||||
|
||||
// Context flags (`oxc_codegen::Context`).
|
||||
|
||||
/** No context. */
|
||||
export const CTX_NONE = 0;
|
||||
|
||||
/** Set in a `for` statement's head, where an unparenthesized `in` would end the head. */
|
||||
export const CTX_FORBID_IN = 1;
|
||||
|
||||
/** Set under a `new`, where an unparenthesized call would be taken as `new`'s own argument list. */
|
||||
export const CTX_FORBID_CALL = 2;
|
||||
|
||||
/** Set inside a TS type, where a numeric literal prints its raw text rather than the shortest form. */
|
||||
export const CTX_TYPESCRIPT = 4;
|
||||
|
||||
/**
|
||||
* Binary/logical operator precedences on the Oxc scale.
|
||||
*/
|
||||
export const BIN_PRECEDENCE = {
|
||||
__proto__: null,
|
||||
"**": 17,
|
||||
"*": 16,
|
||||
"/": 16,
|
||||
"%": 16,
|
||||
"+": 15,
|
||||
"-": 15,
|
||||
"<<": 14,
|
||||
">>": 14,
|
||||
">>>": 14,
|
||||
"<": 13,
|
||||
">": 13,
|
||||
"<=": 13,
|
||||
">=": 13,
|
||||
instanceof: 13,
|
||||
in: 13,
|
||||
"==": 12,
|
||||
"!=": 12,
|
||||
"===": 12,
|
||||
"!==": 12,
|
||||
"&": 11,
|
||||
"^": 10,
|
||||
"|": 9,
|
||||
"&&": 8,
|
||||
"||": 7,
|
||||
"??": 6,
|
||||
};
|
||||
|
||||
/**
|
||||
* Space-padded forms of every binary/logical operator.
|
||||
*
|
||||
* In pretty-print mode an operator is always surrounded by single spaces,
|
||||
* so the whole token is one constant string and needs a single `write` call.
|
||||
*/
|
||||
export const PADDED_BIN_OPERATORS = {
|
||||
__proto__: null,
|
||||
"**": " ** ",
|
||||
"*": " * ",
|
||||
"/": " / ",
|
||||
"%": " % ",
|
||||
"+": " + ",
|
||||
"-": " - ",
|
||||
"<<": " << ",
|
||||
">>": " >> ",
|
||||
">>>": " >>> ",
|
||||
"<": " < ",
|
||||
">": " > ",
|
||||
"<=": " <= ",
|
||||
">=": " >= ",
|
||||
instanceof: " instanceof ",
|
||||
in: " in ",
|
||||
"==": " == ",
|
||||
"!=": " != ",
|
||||
"===": " === ",
|
||||
"!==": " !== ",
|
||||
"&": " & ",
|
||||
"^": " ^ ",
|
||||
"|": " | ",
|
||||
"&&": " && ",
|
||||
"||": " || ",
|
||||
"??": " ?? ",
|
||||
};
|
||||
|
||||
/**
|
||||
* Space-padded forms of every assignment operator.
|
||||
*
|
||||
* In pretty-print mode an operator is always surrounded by single spaces,
|
||||
* so the whole token is one constant string and needs a single `write` call.
|
||||
*/
|
||||
export const PADDED_ASSIGN_OPERATORS = {
|
||||
__proto__: null,
|
||||
"=": " = ",
|
||||
"+=": " += ",
|
||||
"-=": " -= ",
|
||||
"*=": " *= ",
|
||||
"/=": " /= ",
|
||||
"%=": " %= ",
|
||||
"**=": " **= ",
|
||||
"<<=": " <<= ",
|
||||
">>=": " >>= ",
|
||||
">>>=": " >>>= ",
|
||||
"&=": " &= ",
|
||||
"^=": " ^= ",
|
||||
"|=": " |= ",
|
||||
"&&=": " &&= ",
|
||||
"||=": " ||= ",
|
||||
"??=": " ??= ",
|
||||
};
|
||||
|
||||
/**
|
||||
* The category a unary operator leaves in `last`, which is what `printSpaceBeforeOperator` reads
|
||||
* to decide whether the next operator needs separating.
|
||||
*
|
||||
* `typeof`, `void` and `delete` never reach here - they end in an identifier character and are handled as words.
|
||||
*/
|
||||
export function unaryOperatorCode(operator: string): Category {
|
||||
debugAssert(!["typeof", "void", "delete"].includes(operator));
|
||||
|
||||
switch (operator) {
|
||||
case "+":
|
||||
return CAT_OP_UN_PLUS;
|
||||
case "-":
|
||||
return CAT_OP_UN_NEG;
|
||||
case "!":
|
||||
return CAT_OP_UN_NOT;
|
||||
default:
|
||||
return CAT_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The category `++` or `--` leaves in `last`, as `unaryOperatorCode` does for unary operators.
|
||||
*/
|
||||
export function updateOperatorCode(operator: string): Category {
|
||||
return operator === "++" ? CAT_OP_UPD_INC : CAT_OP_UPD_DEC;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Printing options, and the source map types the `sourceMap` option brings with it.
|
||||
|
||||
/**
|
||||
* Source map generator, compatible with `SourceMapGenerator` from the `source-map` package.
|
||||
*/
|
||||
export interface SourceMapGenerator {
|
||||
addMapping(mapping: Mapping): void;
|
||||
file?: string;
|
||||
_file?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Source map mapping emitted through `Options["sourceMap"]`.
|
||||
*
|
||||
* Note: the `Mapping` objects passed to `addMapping` are reused across calls;
|
||||
* implementations must copy any values they need to retain.
|
||||
*/
|
||||
export interface Mapping {
|
||||
original: Position;
|
||||
generated: Position;
|
||||
name: string | undefined;
|
||||
source: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Mapping` before the per-mapping fields are filled in.
|
||||
*/
|
||||
export type MutableMapping = Omit<Mapping, "original" | "source"> & {
|
||||
original: Position | null;
|
||||
source: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Position in the original source, as found on a node's `loc.start`.
|
||||
*/
|
||||
export interface Position {
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Code generator options.
|
||||
*/
|
||||
export interface Options {
|
||||
/**
|
||||
* String to use for indentation, defaults to `"\t"`.
|
||||
* Must consist only of spaces and/or tabs - anything else falls back to a tab.
|
||||
*/
|
||||
indent?: string;
|
||||
|
||||
/**
|
||||
* Indent level to start from, defaults to `0`.
|
||||
*/
|
||||
startingIndentLevel?: number;
|
||||
|
||||
/**
|
||||
* `.tsx` mode if `true`, in which a lone type parameter prints as `<T,>`.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
jsx?: boolean;
|
||||
|
||||
/**
|
||||
* `true` if the AST may contain TypeScript syntax (TS-ESLint dialect).
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
ts?: boolean;
|
||||
|
||||
/**
|
||||
* If present, source mappings are emitted through it.
|
||||
*/
|
||||
sourceMap?: SourceMapGenerator;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Looking through the parentheses an AST may carry, to the expression they wrap.
|
||||
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Look through any `ParenthesizedExpression` wrappers to the expression inside.
|
||||
*
|
||||
* Parsing with `preserveParens: false` produces none of these, but the printer accepts an AST
|
||||
* which has them, and decides parenthesisation from precedence rather than from what the source had.
|
||||
*/
|
||||
export function withoutParens(node: ESTree.Expression): ESTree.Expression {
|
||||
while (node.type === "ParenthesizedExpression") {
|
||||
node = node.expression;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Operator precedence levels, on the scale `oxc_syntax::precedence::Precedence` uses.
|
||||
//
|
||||
// A printer is told the precedence of the position it is printing into, and parenthesizes itself
|
||||
// when its own precedence is lower. The numbering is Oxc's, so the two agree on what needs parens.
|
||||
|
||||
/** No precedence at all - nothing parenthesizes itself against this. */
|
||||
export const PREC_LOWEST = 0;
|
||||
/** The comma operator, as in a sequence expression. */
|
||||
export const PREC_COMMA = 1;
|
||||
/** `yield` and `yield*`. */
|
||||
export const PREC_YIELD = 3;
|
||||
/** Assignment, including the compound and logical forms, and arrow functions. */
|
||||
export const PREC_ASSIGN = 4;
|
||||
/** The `?:` conditional operator. */
|
||||
export const PREC_CONDITIONAL = 5;
|
||||
/** The equality operators, `==` through `!==`. */
|
||||
export const PREC_EQUALS = 12;
|
||||
/** The relational operators, including `in` and `instanceof`. */
|
||||
export const PREC_COMPARE = 13;
|
||||
/** `**`, the one right-associative binary operator. */
|
||||
export const PREC_EXPONENTIATION = 17;
|
||||
/** The prefix unary and update operators, `await`, and `!`. */
|
||||
export const PREC_PREFIX = 18;
|
||||
/** The postfix update operators, and the position a member expression's object prints at. */
|
||||
export const PREC_POSTFIX = 19;
|
||||
/** `new` without an argument list, which binds tighter than a call. */
|
||||
export const PREC_NEW = 20;
|
||||
/** Calls, member access and tagged templates - the tightest binding there is. */
|
||||
export const PREC_CALL = 21;
|
||||
@@ -0,0 +1,55 @@
|
||||
// Source map generation.
|
||||
//
|
||||
// `write` records the output offset and original position of every mapped node as it goes,
|
||||
// and this converts those offsets to generated line/column in one pass at the end.
|
||||
|
||||
import { debugAssert, typeAssertIs } from "../asserts.ts";
|
||||
|
||||
import type { Mapping, MutableMapping, SourceMapGenerator } from "./options.ts";
|
||||
import type { State } from "../state.ts";
|
||||
|
||||
/**
|
||||
* Convert the offsets recorded during printing into mappings, and hand them to the generator.
|
||||
*
|
||||
* Printing records only an output offset per mapped node, which is why this walks the output once
|
||||
* at the end counting newlines, rather than tracking a line and column throughout.
|
||||
*
|
||||
* @param sourceMap - Generator the mappings are added to, which the caller was given in its options
|
||||
*/
|
||||
export function emitMappings(state: State, sourceMap: SourceMapGenerator): void {
|
||||
debugAssert(
|
||||
state.mapOffsets !== null && state.mapPositions !== null && state.mapNames !== null,
|
||||
"Source map arrays should exist when a `sourceMap` was given",
|
||||
);
|
||||
|
||||
const { output, mapOffsets, mapPositions, mapNames } = state;
|
||||
const source = sourceMap.file || sourceMap._file;
|
||||
|
||||
// The `generated` and `mapping` objects are reused across `addMapping` calls to avoid generating ephemeral objects
|
||||
const generated = { line: 1, column: 0 };
|
||||
const mapping: MutableMapping = { original: null, generated, name: undefined, source };
|
||||
|
||||
let line = 1;
|
||||
let lineStart = 0;
|
||||
let nextNewline = output.indexOf("\n");
|
||||
if (nextNewline === -1) nextNewline = Infinity;
|
||||
|
||||
const { length } = mapOffsets;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const offset = mapOffsets[i];
|
||||
while (offset > nextNewline) {
|
||||
line++;
|
||||
lineStart = nextNewline + 1;
|
||||
const next = output.indexOf("\n", lineStart);
|
||||
nextNewline = next === -1 ? Infinity : next;
|
||||
}
|
||||
|
||||
generated.line = line;
|
||||
generated.column = offset - lineStart;
|
||||
mapping.original = mapPositions[i];
|
||||
mapping.name = mapNames[i];
|
||||
|
||||
typeAssertIs<Mapping>(mapping);
|
||||
sourceMap.addMapping(mapping);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Spacing between tokens.
|
||||
//
|
||||
// Two adjacent tokens can merge into a different token when written straight up against each other -
|
||||
// `in` after an identifier, `+` after `+`. Each of these is called before writing the second one,
|
||||
// and writes a space only where it would otherwise happen.
|
||||
|
||||
import { debugAssert } from "../asserts.ts";
|
||||
import {
|
||||
ALL_CATEGORIES,
|
||||
CAT_IDENT,
|
||||
CAT_INT_DIGIT,
|
||||
CAT_OP_UN_NEG,
|
||||
CAT_OP_UN_NOT_AFTER_LT,
|
||||
CAT_OP_UN_PLUS,
|
||||
CAT_OP_UPD_DEC,
|
||||
CAT_OP_UPD_INC,
|
||||
CAT_OTHER,
|
||||
CAT_REGEX_SLASH,
|
||||
debugAssertLastFresh,
|
||||
write,
|
||||
} from "./write.ts";
|
||||
|
||||
import type { Category } from "./write.ts";
|
||||
import type { State } from "../state.ts";
|
||||
|
||||
// `printSpaceBeforeIdentifier` selects the categories needing a space by their position in `Category` numbering.
|
||||
// Check `category <= CAT_REGEX_SLASH` matches the intended categories, and no others.
|
||||
if (DEBUG) {
|
||||
for (const category of ALL_CATEGORIES) {
|
||||
const expected = [CAT_IDENT, CAT_INT_DIGIT, CAT_REGEX_SLASH].includes(category);
|
||||
const actual = category <= CAT_REGEX_SLASH;
|
||||
debugAssert(
|
||||
actual === expected,
|
||||
`Category ${category} disagrees with \`last <= CAT_REGEX_SLASH\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a space, if what was written last would otherwise run into an identifier.
|
||||
*
|
||||
* Call it before writing anything starting with an identifier character - a name, a keyword, or a
|
||||
* number. The categories needing the space are the lowest codes in the numbering, so the test is
|
||||
* one compare.
|
||||
*/
|
||||
export function printSpaceBeforeIdentifier(state: State): void {
|
||||
debugAssertLastFresh(state);
|
||||
|
||||
// `last` starts as `CAT_OTHER` (start of output behaves like after whitespace),
|
||||
// so no empty-output check is needed.
|
||||
// Everything needing a space before an identifier is one of the lowest codes, so this is one compare -
|
||||
// identifier characters, a plain-digit number, and a regex closed with no flags.
|
||||
if (state.last <= CAT_REGEX_SLASH) write(state, " ", CAT_OTHER);
|
||||
}
|
||||
|
||||
// `printSpaceBeforeOperator` selects the categories needing a space by their position in `Category` numbering.
|
||||
// Check `category >= CAT_OP_UN_NOT_AFTER_LT` matches the intended categories, and no others.
|
||||
// `CAT_OP_UN_NOT` is deliberately absent - `printSpaceBeforeOperatorSlow` has no clause for a
|
||||
// plain `!`, so it sits below the range and storing it costs the slow path nothing.
|
||||
if (DEBUG) {
|
||||
for (const category of ALL_CATEGORIES) {
|
||||
const expected = [
|
||||
CAT_OP_UN_NOT_AFTER_LT,
|
||||
CAT_OP_UN_PLUS,
|
||||
CAT_OP_UPD_INC,
|
||||
CAT_OP_UN_NEG,
|
||||
CAT_OP_UPD_DEC,
|
||||
].includes(category);
|
||||
const actual = category >= CAT_OP_UN_NOT_AFTER_LT;
|
||||
debugAssert(
|
||||
actual === expected,
|
||||
`Category ${category} disagrees with \`last >= CAT_OP_UN_NOT_AFTER_LT\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a space, if the operator about to be written would otherwise merge with the one before it.
|
||||
*
|
||||
* `+ +y` and `- -y` are the cases - `++y` and `--y` would be a different operator.
|
||||
* An operator category in `last` proves that operator was the immediately preceding token,
|
||||
* because every write replaces `last`, so no position has to be recorded alongside it.
|
||||
*
|
||||
* @param next - Category of the operator about to be written
|
||||
*/
|
||||
export function printSpaceBeforeOperator(state: State, next: Category): void {
|
||||
debugAssertLastFresh(state);
|
||||
|
||||
// The slow path only runs when an operator it distinguishes was the immediately preceding token,
|
||||
// which is rare in pretty output. Keep the hot check inlinable.
|
||||
const prev = state.last;
|
||||
if (prev >= CAT_OP_UN_NOT_AFTER_LT) printSpaceBeforeOperatorSlow(state, prev, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* The pairs of adjacent operators which need separating, kept out of `printSpaceBeforeOperator`
|
||||
* so the common path stays small enough to inline.
|
||||
*
|
||||
* In pretty mode binary operators are written space-padded, so they never leave an operator code in `last`
|
||||
* and are never passed as `next` - only unary and update operators reach here.
|
||||
* Oxc's `print_space_before_operator` also has clauses for the binary cases, but those were already
|
||||
* unreachable here, and there is now no code which could produce a binary operator's category.
|
||||
*
|
||||
* Only prefix operators appear as `next` - `+ +y`, `- --y` - which is why `prev` being `++` or
|
||||
* `--` matches no clause. That is not a gap: in pretty output a postfix `++`/`--` is always
|
||||
* followed by punctuation or a padded binary operator, never directly by another operator. (The
|
||||
* asymmetry is the Rust original's, where it is live in minified mode - unpadded `(x++)+y` must
|
||||
* print `x+++y` with no space, while unary `+ +y` must keep one.)
|
||||
*
|
||||
* @param prev - Category of the operator written last
|
||||
* @param next - Category of the operator about to be written
|
||||
*/
|
||||
function printSpaceBeforeOperatorSlow(state: State, prev: Category, next: Category): void {
|
||||
if (
|
||||
(prev === CAT_OP_UN_PLUS && (next === CAT_OP_UN_PLUS || next === CAT_OP_UPD_INC)) ||
|
||||
(prev === CAT_OP_UN_NEG && (next === CAT_OP_UN_NEG || next === CAT_OP_UPD_DEC)) ||
|
||||
(prev === CAT_OP_UN_NOT_AFTER_LT && next === CAT_OP_UPD_DEC)
|
||||
) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,796 @@
|
||||
// Statements.
|
||||
|
||||
import { typeAssertIs } from "../asserts.ts";
|
||||
import { printAssignmentTarget } from "./assignment_target.ts";
|
||||
import { printBindingPattern } from "./binding_pattern.ts";
|
||||
import {
|
||||
CAT_IDENT,
|
||||
CAT_OP_UN_NOT,
|
||||
CAT_OTHER,
|
||||
CAT_START_OF_STMT,
|
||||
write,
|
||||
writeNoLast,
|
||||
writeWithMap,
|
||||
writeWithMapNoLast,
|
||||
} from "./write.ts";
|
||||
import { printClass } from "./class.ts";
|
||||
import { printExpression } from "./expression.ts";
|
||||
import { printFunction } from "./function.ts";
|
||||
import { printSpaceBeforeIdentifier } from "./space.ts";
|
||||
import { printIndent } from "./indent.ts";
|
||||
import {
|
||||
printExportAllDeclaration,
|
||||
printExportDefaultDeclaration,
|
||||
printExportNamedDeclaration,
|
||||
printImportDeclaration,
|
||||
} from "./module.ts";
|
||||
import { CTX_FORBID_IN, CTX_NONE } from "./operators.ts";
|
||||
import { withoutParens } from "./parens.ts";
|
||||
import { PREC_COMMA, PREC_LOWEST } from "./precedence.ts";
|
||||
import { printString } from "./string.ts";
|
||||
import {
|
||||
printTSEnumDeclaration,
|
||||
printTSImportEqualsDeclaration,
|
||||
printTSInterfaceDeclaration,
|
||||
printTSModuleDeclaration,
|
||||
printTSTypeAliasDeclaration,
|
||||
printTypeAnnotation,
|
||||
} from "./typescript.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type { LiteralExtras, UnknownNode } from "./types.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Entry point for a whole file.
|
||||
* A hashbang is written ahead of everything else, since it is only legal on the first line.
|
||||
*/
|
||||
export function printProgram(node: ESTree.Program, state: State): void {
|
||||
if (node.hashbang != null) {
|
||||
writeNoLast(state, "#!");
|
||||
writeNoLast(state, node.hashbang.value);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
printDirectivesAndStatements(node.body, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* A directive prologue followed by ordinary statements. Directives are taken from the front for as long
|
||||
* as they keep coming, and everything from the first non-directive onwards prints normally.
|
||||
*
|
||||
* The handover is the delicate part - a string literal statement arriving right after the prologue
|
||||
* has to be kept from being read back as one more directive.
|
||||
*/
|
||||
export function printDirectivesAndStatements(
|
||||
body: Array<ESTree.Directive | ESTree.Statement>,
|
||||
state: State,
|
||||
): void {
|
||||
const { length } = body;
|
||||
if (length === 0) return;
|
||||
|
||||
let i = 0;
|
||||
let stmt = body[0];
|
||||
while (stmt.type === "ExpressionStatement") {
|
||||
if (stmt.directive != null) {
|
||||
typeAssertIs<ESTree.Directive>(stmt);
|
||||
printDirective(stmt, state);
|
||||
if (++i >= length) return;
|
||||
stmt = body[i];
|
||||
} else {
|
||||
// Ensure a string literal (only possible via parentheses, since a bare one would be a directive)
|
||||
// as 1st statement or after other real directives, is not re-parsed as a directive
|
||||
const inner = withoutParens(stmt.expression);
|
||||
typeAssertIs<LiteralExtras>(inner);
|
||||
if (inner.type === "Literal" && typeof inner.value === "string") {
|
||||
printIndent(state);
|
||||
writeNoLast(state, "(");
|
||||
printString(state, inner.value, inner);
|
||||
write(state, ");\n", CAT_OTHER);
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < length; i++) {
|
||||
printStatement(body[i], state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directives print from their raw source text, so the quote has to be chosen to suit the text
|
||||
* rather than the other way round - the scan takes the first unescaped `"` or `'` and quotes with
|
||||
* the other one.
|
||||
*/
|
||||
function printDirective(stmt: ESTree.Directive, state: State): void {
|
||||
printIndent(state);
|
||||
|
||||
const { directive } = stmt;
|
||||
|
||||
// A directive may not contain an escape sequence or line continuation,
|
||||
// so print the raw `directive` value with a quote character it doesn't contain.
|
||||
let quote = '"';
|
||||
const { length } = directive;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const char = directive[i];
|
||||
if (char === '"') {
|
||||
quote = "'";
|
||||
break;
|
||||
} else if (char === "'") {
|
||||
break;
|
||||
} else if (char === "\\") {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
writeWithMapNoLast(state, quote, stmt);
|
||||
writeNoLast(state, directive);
|
||||
writeNoLast(state, quote);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch point for statements, and the counterpart to `printExpression`. Unknown types throw.
|
||||
*
|
||||
* Each case owns its own leading indent and its trailing newline, so a statement always leaves
|
||||
* the output at the start of a fresh line for the next one.
|
||||
*/
|
||||
export function printStatement(node: ESTree.Statement | UnknownNode, state: State): void {
|
||||
// Arms are ordered roughly in order of most common nodes.
|
||||
// V8 turns this into (essentially) as chain of `if ... else if ... else if...`,
|
||||
// so making common nodes short-circuit early is a large perf boost.
|
||||
switch (node.type) {
|
||||
case "ExpressionStatement":
|
||||
printExpressionStatement(node, state);
|
||||
break;
|
||||
case "VariableDeclaration":
|
||||
printIndent(state);
|
||||
printVariableDeclaration(node, state, CTX_NONE);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "BlockStatement":
|
||||
printIndent(state);
|
||||
printBlockStatement(node, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "IfStatement":
|
||||
printIndent(state);
|
||||
printIf(node, state);
|
||||
break;
|
||||
case "ReturnStatement":
|
||||
printReturnStatement(node, state);
|
||||
break;
|
||||
case "FunctionDeclaration":
|
||||
printIndent(state);
|
||||
printFunction(node, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "ForStatement":
|
||||
printForStatement(node, state);
|
||||
break;
|
||||
case "WhileStatement":
|
||||
printWhileStatement(node, state);
|
||||
break;
|
||||
case "DoWhileStatement":
|
||||
printDoWhileStatement(node, state);
|
||||
break;
|
||||
case "SwitchStatement":
|
||||
printSwitchStatement(node, state);
|
||||
break;
|
||||
case "BreakStatement":
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "break", CAT_IDENT, node);
|
||||
if (node.label != null) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
writeWithMap(state, node.label.name, CAT_IDENT, node.label);
|
||||
}
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "ContinueStatement":
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "continue", CAT_IDENT, node);
|
||||
if (node.label != null) {
|
||||
write(state, " ", CAT_OTHER);
|
||||
writeWithMap(state, node.label.name, CAT_IDENT, node.label);
|
||||
}
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "TryStatement":
|
||||
printTryStatement(node, state);
|
||||
break;
|
||||
case "ThrowStatement":
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "throw ", CAT_OTHER, node);
|
||||
printExpression(node.argument, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "ForInStatement":
|
||||
printForInStatement(node, state);
|
||||
break;
|
||||
case "ForOfStatement":
|
||||
printForOfStatement(node, state);
|
||||
break;
|
||||
case "ClassDeclaration":
|
||||
printIndent(state);
|
||||
printClass(node, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "LabeledStatement":
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, node.label.name, CAT_IDENT, node.label);
|
||||
write(state, ":", CAT_OTHER);
|
||||
printBody(node.body, state);
|
||||
break;
|
||||
case "EmptyStatement":
|
||||
printIndent(state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "ImportDeclaration":
|
||||
printImportDeclaration(node, state);
|
||||
break;
|
||||
case "ExportNamedDeclaration":
|
||||
printExportNamedDeclaration(node, state);
|
||||
break;
|
||||
case "ExportDefaultDeclaration":
|
||||
printExportDefaultDeclaration(node, state);
|
||||
break;
|
||||
case "ExportAllDeclaration":
|
||||
printExportAllDeclaration(node, state);
|
||||
break;
|
||||
case "WithStatement":
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "with(", CAT_OTHER, node);
|
||||
printExpression(node.object, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ")", CAT_OTHER);
|
||||
printBody(node.body, state);
|
||||
break;
|
||||
case "DebuggerStatement":
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, "debugger;\n", CAT_OTHER, node);
|
||||
break;
|
||||
/* IF TS */
|
||||
case "TSModuleDeclaration":
|
||||
printIndent(state);
|
||||
printTSModuleDeclaration(node, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSInterfaceDeclaration":
|
||||
printIndent(state);
|
||||
printTSInterfaceDeclaration(node, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSTypeAliasDeclaration":
|
||||
printIndent(state);
|
||||
printTSTypeAliasDeclaration(node, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSEnumDeclaration":
|
||||
printIndent(state);
|
||||
printTSEnumDeclaration(node, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSImportEqualsDeclaration":
|
||||
printIndent(state);
|
||||
printTSImportEqualsDeclaration(node, state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSDeclareFunction":
|
||||
printIndent(state);
|
||||
printFunction(node, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSExportAssignment":
|
||||
printIndent(state);
|
||||
writeWithMap(state, "export = ", CAT_OTHER, node);
|
||||
printExpression(node.expression, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
case "TSNamespaceExportDeclaration":
|
||||
printIndent(state);
|
||||
writeWithMap(state, "export as namespace ", CAT_OTHER, node);
|
||||
writeWithMap(state, node.id.name, CAT_IDENT, node.id);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
break;
|
||||
/* END_IF */
|
||||
default:
|
||||
throw new Error(`Unknown statement type: ${node.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks `last` as the start of a statement, which is how an object literal or an object destructuring assignment
|
||||
* printed there knows to parenthesize itself.
|
||||
*
|
||||
* The indent is written first, so the mark is the last thing to touch `last` before the expression.
|
||||
*/
|
||||
function printExpressionStatement(node: ESTree.ExpressionStatement, state: State): void {
|
||||
printIndent(state);
|
||||
state.last = CAT_START_OF_STMT;
|
||||
printExpression(node.expression, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared by the standalone declaration and the `for` and `for-in` heads, which is why it writes no
|
||||
* indent and no terminator - the caller supplies both.
|
||||
*
|
||||
* @param ctx - Passed to every initializer, so a `for` head can hand down `CTX_FORBID_IN` and stop an
|
||||
* `in` operator there being read as the head's own.
|
||||
*/
|
||||
export function printVariableDeclaration(
|
||||
node: ESTree.VariableDeclaration,
|
||||
state: State,
|
||||
ctx: number,
|
||||
): void {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
// The node's mapping goes on whichever of these is written first
|
||||
const declare = TS && node.declare;
|
||||
if (declare) {
|
||||
writeWithMap(state, "declare ", CAT_OTHER, node);
|
||||
write(state, node.kind, CAT_IDENT);
|
||||
} else {
|
||||
writeWithMap(state, node.kind, CAT_IDENT, node);
|
||||
}
|
||||
|
||||
const { declarations } = node;
|
||||
const { length } = declarations;
|
||||
if (length > 0) write(state, " ", CAT_OTHER);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) write(state, ", ", CAT_OTHER);
|
||||
|
||||
const declarator = declarations[i];
|
||||
const { id } = declarator;
|
||||
if (TS && declarator.definite) {
|
||||
// `let x!: T` - the `!` sits between the name and its annotation
|
||||
typeAssertIs<ESTree.BindingIdentifier>(id);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
writeWithMap(state, id.name, CAT_IDENT, id);
|
||||
write(state, "!", CAT_OP_UN_NOT);
|
||||
if (id.typeAnnotation != null) {
|
||||
printTypeAnnotation(id.typeAnnotation, state);
|
||||
}
|
||||
} else {
|
||||
printBindingPattern(id, state);
|
||||
}
|
||||
|
||||
if (declarator.init != null) {
|
||||
write(state, " = ", CAT_OTHER);
|
||||
printExpression(declarator.init, state, PREC_COMMA, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty block prints as `{}` with no line break inside. Otherwise the body prints one statement
|
||||
* per line, a level deeper, with the closing brace back at the caller's level.
|
||||
*
|
||||
* Nothing is written before the opening brace or after the closing one, so callers can attach
|
||||
* ` else`, ` catch` or a newline themselves.
|
||||
*/
|
||||
function printBlockStatement(block: ESTree.BlockStatement, state: State): void {
|
||||
const { body } = block;
|
||||
const { length } = body;
|
||||
if (length === 0) {
|
||||
writeWithMap(state, "{}", CAT_OTHER, block);
|
||||
return;
|
||||
}
|
||||
|
||||
writeWithMap(state, "{\n", CAT_OTHER, block);
|
||||
state.indentLevel++;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
printStatement(body[i], state);
|
||||
}
|
||||
|
||||
state.indentLevel--;
|
||||
printIndent(state);
|
||||
write(state, "}", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* An `else if` chain prints flat, by recursing on an `IfStatement` alternate rather than treating
|
||||
* it as a nested body.
|
||||
*
|
||||
* A non-block consequent which trails off into a dangling `if` is given braces, so a following
|
||||
* `else` cannot attach to the wrong one - see `wrapToAvoidAmbiguousElse`.
|
||||
*/
|
||||
function printIf(node: ESTree.IfStatement, state: State): void {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "if (", CAT_OTHER, node);
|
||||
|
||||
printExpression(node.test, state, PREC_LOWEST, CTX_NONE);
|
||||
|
||||
const { consequent, alternate } = node;
|
||||
if (consequent.type === "BlockStatement") {
|
||||
write(state, ") ", CAT_OTHER);
|
||||
printBlockStatement(consequent, state);
|
||||
write(state, alternate != null ? " " : "\n", CAT_OTHER);
|
||||
} else if (wrapToAvoidAmbiguousElse(consequent)) {
|
||||
write(state, ") {\n", CAT_OTHER);
|
||||
state.indentLevel++;
|
||||
printStatement(consequent, state);
|
||||
state.indentLevel--;
|
||||
printIndent(state);
|
||||
write(state, "}", CAT_OTHER);
|
||||
write(state, alternate != null ? " " : "\n", CAT_OTHER);
|
||||
} else {
|
||||
write(state, ")", CAT_OTHER);
|
||||
printBody(consequent, state);
|
||||
if (alternate != null) printIndent(state);
|
||||
}
|
||||
|
||||
if (alternate != null) {
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
write(state, "else", CAT_IDENT);
|
||||
|
||||
if (alternate.type === "BlockStatement") {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printBlockStatement(alternate, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
} else if (alternate.type === "IfStatement") {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printIf(alternate, state);
|
||||
} else {
|
||||
printBody(alternate, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a statement trails off into an `if` with no `else`, which would swallow an `else` written after it.
|
||||
*
|
||||
* Only the trailing position is followed - a loop, `with` or label body, and an `if` alternate -
|
||||
* since that is where a following `else` would land.
|
||||
*/
|
||||
function wrapToAvoidAmbiguousElse(stmt: ESTree.Statement): boolean {
|
||||
for (;;) {
|
||||
switch (stmt.type) {
|
||||
case "IfStatement":
|
||||
if (stmt.alternate == null) return true;
|
||||
stmt = stmt.alternate;
|
||||
break;
|
||||
case "ForStatement":
|
||||
case "ForOfStatement":
|
||||
case "ForInStatement":
|
||||
case "WhileStatement":
|
||||
case "WithStatement":
|
||||
case "LabeledStatement":
|
||||
stmt = stmt.body;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* With an argument the space after `return` is part of the same write.
|
||||
* With none, the keyword is recorded as an identifier, so anything printed after it keeps a separating space.
|
||||
*/
|
||||
function printReturnStatement(node: ESTree.ReturnStatement, state: State): void {
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
const { argument } = node;
|
||||
if (argument != null) {
|
||||
writeWithMap(state, "return ", CAT_OTHER, node);
|
||||
printExpression(argument, state, PREC_LOWEST, CTX_NONE);
|
||||
} else {
|
||||
writeWithMap(state, "return", CAT_IDENT, node);
|
||||
}
|
||||
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* The catch binding is optional, so an absent `param` prints a bare `catch` block rather than an empty pair of parens.
|
||||
* The handler and the finalizer are each written only if present.
|
||||
*/
|
||||
function printTryStatement(node: ESTree.TryStatement, state: State): void {
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "try ", CAT_OTHER, node);
|
||||
|
||||
printBlockStatement(node.block, state);
|
||||
|
||||
const { handler } = node;
|
||||
if (handler != null) {
|
||||
write(state, " catch", CAT_IDENT);
|
||||
|
||||
if (handler.param != null) {
|
||||
write(state, " (", CAT_OTHER);
|
||||
printBindingPattern(handler.param, state);
|
||||
write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
write(state, " ", CAT_OTHER);
|
||||
printBlockStatement(handler.body, state);
|
||||
}
|
||||
|
||||
if (node.finalizer != null) {
|
||||
write(state, " finally ", CAT_OTHER);
|
||||
printBlockStatement(node.finalizer, state);
|
||||
}
|
||||
|
||||
write(state, "\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* A switch with no cases prints as `{}` on the same line. Otherwise the cases sit one level in,
|
||||
* and each indents its own statements again - see `printSwitchCase`.
|
||||
*/
|
||||
function printSwitchStatement(node: ESTree.SwitchStatement, state: State): void {
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "switch (", CAT_OTHER, node);
|
||||
printExpression(node.discriminant, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ") ", CAT_OTHER);
|
||||
|
||||
const { cases } = node;
|
||||
const { length } = cases;
|
||||
if (length === 0) {
|
||||
write(state, "{}\n", CAT_OTHER);
|
||||
return;
|
||||
}
|
||||
|
||||
write(state, "{\n", CAT_OTHER);
|
||||
state.indentLevel++;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
printSwitchCase(cases[i], state);
|
||||
}
|
||||
|
||||
state.indentLevel--;
|
||||
printIndent(state);
|
||||
write(state, "}\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* A case with exactly one statement hands it to `printBody`, which keeps it on the line of the `case`.
|
||||
* Any other count goes onto separate lines a level deeper, so an empty case prints as just its label.
|
||||
*/
|
||||
function printSwitchCase(node: ESTree.SwitchCase, state: State): void {
|
||||
printIndent(state);
|
||||
|
||||
if (node.test != null) {
|
||||
writeWithMap(state, "case ", CAT_OTHER, node);
|
||||
printExpression(node.test, state, PREC_LOWEST, CTX_NONE);
|
||||
} else {
|
||||
writeWithMap(state, "default", CAT_IDENT, node);
|
||||
}
|
||||
|
||||
write(state, ":", CAT_OTHER);
|
||||
|
||||
const { consequent } = node;
|
||||
const { length } = consequent;
|
||||
if (length === 1) {
|
||||
printBody(consequent[0], state);
|
||||
return;
|
||||
}
|
||||
|
||||
write(state, "\n", CAT_OTHER);
|
||||
state.indentLevel++;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
printStatement(consequent[i], state);
|
||||
}
|
||||
|
||||
state.indentLevel--;
|
||||
}
|
||||
|
||||
/**
|
||||
* The test is fenced by its own parens, so it prints from `PREC_LOWEST` with no context flags,
|
||||
* and the body goes through `printBody` like every other loop here.
|
||||
*/
|
||||
function printWhileStatement(node: ESTree.WhileStatement, state: State): void {
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "while (", CAT_OTHER, node);
|
||||
printExpression(node.test, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ")", CAT_OTHER);
|
||||
|
||||
printBody(node.body, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Three body shapes with their own spacing - a block sits inline between `do` and `while`,
|
||||
* an empty body prints as a bare `;`, and anything else takes its own indented line
|
||||
* with the `while` returned to the outer level.
|
||||
*
|
||||
* The closing `);` is always written out rather than left to ASI.
|
||||
*/
|
||||
function printDoWhileStatement(node: ESTree.DoWhileStatement, state: State): void {
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "do", CAT_IDENT, node);
|
||||
|
||||
const { body } = node;
|
||||
if (body.type === "BlockStatement") {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printBlockStatement(body, state);
|
||||
write(state, " ", CAT_OTHER);
|
||||
} else if (body.type === "EmptyStatement") {
|
||||
printIndent(state);
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
} else {
|
||||
write(state, "\n", CAT_OTHER);
|
||||
state.indentLevel++;
|
||||
printStatement(body, state);
|
||||
state.indentLevel--;
|
||||
printIndent(state);
|
||||
}
|
||||
|
||||
write(state, "while (", CAT_OTHER);
|
||||
printExpression(node.test, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ");\n", CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* The init prints with `CTX_FORBID_IN`, so an `in` operator there parenthesizes itself instead of
|
||||
* turning the head into a `for-in`.
|
||||
*
|
||||
* An absent test or update still writes its `;`, which is why those cases fold the separator,
|
||||
* and for the update the closing paren too, into a single write.
|
||||
*/
|
||||
function printForStatement(node: ESTree.ForStatement, state: State): void {
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "for (", CAT_OTHER, node);
|
||||
|
||||
const { init } = node;
|
||||
if (init != null) {
|
||||
if (init.type === "VariableDeclaration") {
|
||||
printVariableDeclaration(init, state, CTX_FORBID_IN);
|
||||
} else {
|
||||
printExpression(init, state, PREC_LOWEST, CTX_FORBID_IN);
|
||||
}
|
||||
}
|
||||
|
||||
const { test, update } = node;
|
||||
if (test != null) {
|
||||
write(state, "; ", CAT_OTHER);
|
||||
printExpression(test, state, PREC_LOWEST, CTX_NONE);
|
||||
} else {
|
||||
write(state, ";", CAT_OTHER);
|
||||
}
|
||||
|
||||
if (update != null) {
|
||||
write(state, "; ", CAT_OTHER);
|
||||
printExpression(update, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ")", CAT_OTHER);
|
||||
} else {
|
||||
write(state, ";)", CAT_OTHER);
|
||||
}
|
||||
|
||||
printBody(node.body, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* The left side is either a declaration or an assignment target. A declaration prints with `CTX_FORBID_IN`,
|
||||
* so an `in` inside its initializer parenthesizes itself instead of ending the head early.
|
||||
*/
|
||||
function printForInStatement(node: ESTree.ForInStatement, state: State): void {
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "for (", CAT_OTHER, node);
|
||||
|
||||
const { left } = node;
|
||||
if (left.type === "VariableDeclaration") {
|
||||
printVariableDeclaration(left, state, CTX_FORBID_IN);
|
||||
} else {
|
||||
printAssignmentTarget(left, state);
|
||||
}
|
||||
|
||||
write(state, " in ", CAT_OTHER);
|
||||
printExpression(node.right, state, PREC_LOWEST, CTX_NONE);
|
||||
write(state, ")", CAT_OTHER);
|
||||
|
||||
printBody(node.body, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Two targets have to be parenthesized in a `for-of` head - one beginning with `let`,
|
||||
* which would read as a declaration, and a bare `async` in a loop without `await`,
|
||||
* which would start `async of => {}`.
|
||||
*
|
||||
* Nothing here needs `CTX_FORBID_IN`, unlike the `for` and `for-in` heads, and the right side
|
||||
* prints at `PREC_COMMA` so a sequence takes parens.
|
||||
*/
|
||||
function printForOfStatement(node: ESTree.ForOfStatement, state: State): void {
|
||||
printIndent(state);
|
||||
printSpaceBeforeIdentifier(state);
|
||||
|
||||
writeWithMap(state, "for", CAT_IDENT, node);
|
||||
|
||||
if (node.await) write(state, " await", CAT_IDENT);
|
||||
|
||||
write(state, " (", CAT_OTHER);
|
||||
|
||||
const { left } = node;
|
||||
if (left.type === "VariableDeclaration") {
|
||||
printVariableDeclaration(left, state, CTX_NONE);
|
||||
} else {
|
||||
typeAssertIs<ESTree.Expression>(left);
|
||||
const bare = withoutParens(left);
|
||||
const wrap =
|
||||
forOfHeadStartsWithLet(left) ||
|
||||
(!node.await && bare.type === "Identifier" && bare.name === "async");
|
||||
if (wrap) write(state, "(", CAT_OTHER);
|
||||
printAssignmentTarget(left, state);
|
||||
if (wrap) write(state, ")", CAT_OTHER);
|
||||
}
|
||||
|
||||
write(state, " of ", CAT_OTHER);
|
||||
printExpression(node.right, state, PREC_COMMA, CTX_NONE);
|
||||
write(state, ")", CAT_OTHER);
|
||||
|
||||
printBody(node.body, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* The restriction is on the first token of the head rather than on the target as a whole,
|
||||
* so this walks down the leftmost spine - through member objects and parens -
|
||||
* to whatever prints first.
|
||||
*/
|
||||
function forOfHeadStartsWithLet(left: ESTree.Expression): boolean {
|
||||
// Whether the leading token would be `let`.
|
||||
for (;;) {
|
||||
switch (left.type) {
|
||||
case "Identifier":
|
||||
return left.name === "let";
|
||||
case "MemberExpression":
|
||||
if (left.computed) {
|
||||
const inner = withoutParens(left.object);
|
||||
if (inner.type === "Identifier" && inner.name === "let") {
|
||||
// `let[...]` is emitted as `(let)[...]`, so no longer starts with `let`
|
||||
return false;
|
||||
}
|
||||
}
|
||||
left = left.object;
|
||||
break;
|
||||
case "ParenthesizedExpression":
|
||||
left = left.expression;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A statement in body position - an `if` branch, a loop or `with` body, a label's statement,
|
||||
* or a lone `case` statement.
|
||||
*
|
||||
* A block is attached with a space and closed with a newline, an empty statement collapses to `;`,
|
||||
* and anything else sets `pendingIndentAsSpace`, so its indent prints as a single space and it stays
|
||||
* on the current line.
|
||||
*/
|
||||
function printBody(stmt: ESTree.Statement, state: State): void {
|
||||
if (stmt.type === "BlockStatement") {
|
||||
write(state, " ", CAT_OTHER);
|
||||
printBlockStatement(stmt, state);
|
||||
write(state, "\n", CAT_OTHER);
|
||||
} else if (stmt.type === "EmptyStatement") {
|
||||
write(state, ";\n", CAT_OTHER);
|
||||
} else {
|
||||
state.pendingIndentAsSpace = true;
|
||||
printStatement(stmt, state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// String literal printing (port of `str.rs`, pretty mode: fixed quote).
|
||||
|
||||
import { CAT_OTHER, write, writeNoLast, writeWithMapNoLast } from "./write.ts";
|
||||
import { debugAssert } from "../asserts.ts";
|
||||
|
||||
import type { State } from "../state.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Characters which may need escaping or special handling.
|
||||
*
|
||||
* The quote is fixed to `"` in pretty mode, so `'` and `` ` `` are deliberately absent -
|
||||
* they are never escaped, and must not drag a string onto the slow path.
|
||||
*/
|
||||
const STRING_ESCAPE_REGEX =
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/[\0\x07\b\v\f\n\r\x1B\\"<\u2028\u2029\xA0\uD800-\uDFFF]/;
|
||||
|
||||
/**
|
||||
* Print a string literal, quotes and all.
|
||||
*
|
||||
* Pretty mode always uses double quotes, matching Oxc's default options, so there is no quote to choose.
|
||||
* Almost no string needs escaping, so the scan for characters which do is all that happens on the common path.
|
||||
*/
|
||||
export function printString(state: State, value: string, node: ESTree.Node): void {
|
||||
// Quote is fixed - double, matching `oxc_codegen`'s default option
|
||||
writeWithMapNoLast(state, '"', node);
|
||||
|
||||
// Almost no string needs escaping, so the loop which performs escaping sits in its own function
|
||||
if (!STRING_ESCAPE_REGEX.test(value)) {
|
||||
writeNoLast(state, value);
|
||||
} else {
|
||||
printEscapedStringContents(state, value);
|
||||
}
|
||||
|
||||
write(state, '"', CAT_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the contents of a string which contains at least one character needing an escape.
|
||||
*
|
||||
* Runs of ordinary characters are copied in chunks rather than one at a time, so the cost is in
|
||||
* the escapes rather than the length. Kept out of `printString` because it almost never runs.
|
||||
*/
|
||||
function printEscapedStringContents(state: State, value: string): void {
|
||||
let chunkStart = 0;
|
||||
|
||||
const { length } = value;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let escape: string | null = null;
|
||||
|
||||
const code = value.charCodeAt(i);
|
||||
switch (code) {
|
||||
case 0:
|
||||
if (i + 1 < length) {
|
||||
const next = value.charCodeAt(i + 1);
|
||||
if (next >= "0".charCodeAt(0) && next <= "9".charCodeAt(0)) {
|
||||
escape = "\\x00";
|
||||
break;
|
||||
}
|
||||
}
|
||||
escape = "\\0";
|
||||
break;
|
||||
case 7:
|
||||
escape = "\\x07";
|
||||
break;
|
||||
case 8:
|
||||
escape = "\\b";
|
||||
break;
|
||||
case 11:
|
||||
escape = "\\v";
|
||||
break;
|
||||
case 12:
|
||||
escape = "\\f";
|
||||
break;
|
||||
case 10:
|
||||
escape = "\\n";
|
||||
break;
|
||||
case 13:
|
||||
escape = "\\r";
|
||||
break;
|
||||
case 27:
|
||||
escape = "\\x1B";
|
||||
break;
|
||||
case 92:
|
||||
escape = "\\\\";
|
||||
break;
|
||||
case 34: // "
|
||||
escape = '\\"';
|
||||
break;
|
||||
case 60: // <
|
||||
// `</script` -> `<\/script`
|
||||
if (/^<\/script/i.test(value.slice(i, i + 8))) {
|
||||
escape = "<\\";
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
case 0x2028:
|
||||
escape = "\\u2028";
|
||||
break;
|
||||
case 0x2029:
|
||||
escape = "\\u2029";
|
||||
break;
|
||||
case 0xa0:
|
||||
escape = "\\xA0";
|
||||
break;
|
||||
default:
|
||||
if (code >= 0xd800 && code <= 0xdfff) {
|
||||
// Escape lone surrogates. Paired surrogates pass through.
|
||||
if (code <= 0xdbff && i + 1 < length) {
|
||||
const next = value.charCodeAt(i + 1);
|
||||
if (next >= 0xdc00 && next <= 0xdfff) {
|
||||
// Valid pair, both halves stay in the chunk
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
escape = "\\u" + code.toString(16);
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
debugAssert(escape != null);
|
||||
|
||||
writeNoLast(state, value.slice(chunkStart, i));
|
||||
writeNoLast(state, escape);
|
||||
chunkStart = i + 1;
|
||||
}
|
||||
|
||||
writeNoLast(state, value.slice(chunkStart));
|
||||
}
|
||||
|
||||
// `</script` -> `<\/script` in template literal quasis.
|
||||
/**
|
||||
* Break up any `</script` in text which is printed raw, so the output cannot end an inline script
|
||||
* element early.
|
||||
*
|
||||
* For template literal quasis, which are printed from their raw text and so never pass through the
|
||||
* string escaper.
|
||||
*/
|
||||
export function escapeScriptCloseTag(text: string): string {
|
||||
if (!text.includes("</")) return text;
|
||||
return text.replace(/<\/(script)/gi, "<\\/$1");
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Node types.
|
||||
//
|
||||
// AST nodes are typed with Oxc's own ESTree type definitions.
|
||||
// The printer is more permissive than they are in 2 respects, which the types below cover:
|
||||
//
|
||||
// 1. Its dispatch functions throw on node types they don't know.
|
||||
// `UnknownNode` keeps those branches reachable - without it the type-checker narrows the node to `never`
|
||||
// and rejects the `node.type` read in the error message.
|
||||
//
|
||||
// 2. It also accepts ASTs from other ESTree producers (Acorn, TS-ESLint), which differ from Oxc's AST
|
||||
// in a handful of places. The `*Node` aliases widen the properties where they do, and the branches
|
||||
// which read a property Oxc's AST doesn't have at all assert its type at the point of use.
|
||||
|
||||
import type { Position } from "./options.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/** A node whose type the printer doesn't handle. */
|
||||
export interface UnknownNode {
|
||||
type: "<unknown node>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties which only some literals carry. `printLiteral` reads them on any literal to tell
|
||||
* regexes and bigints apart from the rest.
|
||||
*/
|
||||
export interface LiteralExtras {
|
||||
regex?: ESTree.RegExpLiteral["regex"];
|
||||
bigint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Oxc marks abstract class members with a `TSAbstract*` node type, where others use an `abstract` flag.
|
||||
*/
|
||||
export type MethodDefinitionNode = ESTree.MethodDefinition & { abstract?: boolean };
|
||||
|
||||
/**
|
||||
* A class field, which Oxc may mark abstract with a flag rather than a `TSAbstract*` type.
|
||||
*/
|
||||
export type PropertyDefinitionNode = ESTree.PropertyDefinition & { abstract?: boolean };
|
||||
|
||||
/**
|
||||
* An `accessor` field, marked abstract the same way as `PropertyDefinitionNode`.
|
||||
*/
|
||||
export type AccessorPropertyNode = ESTree.AccessorProperty & { abstract?: boolean };
|
||||
|
||||
/**
|
||||
* A class body whose members may include a node type the printer does not know.
|
||||
*/
|
||||
export type ClassBodyNode = Omit<ESTree.ClassBody, "body"> & {
|
||||
body: Array<ESTree.ClassElement | UnknownNode>;
|
||||
};
|
||||
|
||||
/**
|
||||
* An `export` whose declaration may be a node type the printer does not know.
|
||||
*/
|
||||
export type ExportNamedDeclarationNode = Omit<ESTree.ExportNamedDeclaration, "declaration"> & {
|
||||
declaration: ESTree.Declaration | UnknownNode | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Oxc keeps a module's kind in `kind`, using `global: true` for `declare global`.
|
||||
* Older producers have no `kind`, and only the `global` flag distinguishes the two.
|
||||
*/
|
||||
export type TSModuleDeclarationNode = Omit<
|
||||
ESTree.TSModuleDeclaration | ESTree.TSGlobalDeclaration,
|
||||
"kind"
|
||||
> & {
|
||||
kind?: ESTree.TSModuleDeclarationKind | "global" | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Oxc puts a mapped type's parameter in `key` and `constraint`, where TS-ESLint < 6 used `typeParameter`.
|
||||
*/
|
||||
export type TSMappedTypeNode = Omit<ESTree.TSMappedType, "key"> & {
|
||||
key?: ESTree.BindingIdentifier | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The pre-TS-ESLint-6 shape `TSMappedTypeNode` falls back to reading.
|
||||
*/
|
||||
export interface TSMappedTypeLegacyParameter {
|
||||
typeParameter: Omit<ESTree.TSTypeParameter, "constraint"> & { constraint: ESTree.TSType };
|
||||
}
|
||||
|
||||
/**
|
||||
* Oxc's `TSImportType` names the module in `source`, where TS-ESLint < 8 used `argument`.
|
||||
*/
|
||||
export type TSImportTypeNode = Omit<ESTree.TSImportType, "source"> & {
|
||||
source?: ESTree.StringLiteral | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The pre-TS-ESLint-8 shape `TSImportTypeNode` falls back to reading.
|
||||
*/
|
||||
export interface TSImportTypeLegacyArgument {
|
||||
argument: ESTree.StringLiteral | ESTree.TSLiteralType | ESTree.TSType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Oxc wraps enum members in a `TSEnumBody`, where TS-ESLint < 8 put them on the declaration itself.
|
||||
*/
|
||||
export type TSEnumDeclarationNode = Omit<ESTree.TSEnumDeclaration, "body"> & {
|
||||
body?: ESTree.TSEnumBody | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The pre-TS-ESLint-8 shape `TSEnumDeclarationNode` falls back to reading.
|
||||
*/
|
||||
export interface TSEnumDeclarationLegacyMembers {
|
||||
members: ESTree.TSEnumMember[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A node, as passed to `write`, which only records where the node started for source maps.
|
||||
*
|
||||
* `loc` is not part of Oxc's AST - that records byte spans - but producers such as Acorn add it.
|
||||
*/
|
||||
export interface MappableNode {
|
||||
type: string;
|
||||
loc?: { start: Position } | null;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,405 @@
|
||||
// Writing to the output, and the categories which describe what was written last.
|
||||
//
|
||||
// These live together because the categories exist only to be stored by `write` -
|
||||
// `state.last` records what was written last by category, not the last character itself.
|
||||
|
||||
import { debugAssert, typeAssertIs } from "../asserts.ts";
|
||||
|
||||
import type { MappableNode } from "./types.ts";
|
||||
import type { State } from "../state.ts";
|
||||
|
||||
// `state.last` records what was written last, by category, not the last character itself.
|
||||
//
|
||||
// Reading last character of `state.output` is disastrous for performance. `state.output` is a "cons string",
|
||||
// a concatenation of many smaller strings. Reading any character of it causes the string to be flattened,
|
||||
// which is costly. Given how often it's required to know what was printed last, this produces a 4x slow-down.
|
||||
//
|
||||
// No reader needs the character - only its category - so the category is defined statically in `write` calls
|
||||
// and stored as a small integer, which costs no write barrier to store and one compare to test.
|
||||
//
|
||||
// Three properties of the layout are load bearing, so keep them if you add a code:
|
||||
//
|
||||
// 1. `CAT_IDENT` through `CAT_REGEX_SLASH` are the classes needing a space before a following identifier,
|
||||
// and they are the lowest codes, so `printSpaceBeforeIdentifier` is one compare against `CAT_REGEX_SLASH`.
|
||||
// The unary and update operators are likewise contiguous, and the highest codes, so `printSpaceBeforeOperator`
|
||||
// is one compare too.
|
||||
//
|
||||
// 2. The `CAT_START_OF_*` codes say where the output has reached, not what was written last, and they sit between
|
||||
// those two ranges. Both space checks therefore read them as "nothing to separate", which is what the `CAT_OTHER`
|
||||
// they displace meant - every position one of them marks follows whitespace or the start of the output.
|
||||
// See `state.ts` for what they are for.
|
||||
//
|
||||
// 3. `CAT_START_OF_STMT` is odd, with the other two marks either side of it.
|
||||
// Each of the two pairs a reader asks about is then one `|`-and-compare instead of two compares.
|
||||
//
|
||||
// The whole numbering:
|
||||
//
|
||||
// Group 0 to 2: A following identifier needs a space (`printSpaceBeforeIdentifier` checks `last <= CAT_REGEX_SLASH`)
|
||||
// 0 CAT_IDENT Identifier part - letters, digits, `_`, `$`, ID_Continue
|
||||
// 1 CAT_INT_DIGIT Numeric literal of plain digits (`0 .toExponential()`)
|
||||
// 2 CAT_REGEX_SLASH Regex closed with no flags
|
||||
//
|
||||
// Group 3 to 9: Checked individually
|
||||
// 3 CAT_OTHER Anything else not covered by another category - punctuation, whitespace
|
||||
// 4 CAT_LT `<`
|
||||
// 5 CAT_QUESTION `?`
|
||||
// 6 CAT_START_OF_DEFAULT_EXPORT `export default` expression is about to be printed
|
||||
// 7 CAT_START_OF_STMT Expression statement's expression is about to be printed
|
||||
// 8 CAT_START_OF_ARROW_EXPR Concise arrow body is about to be printed
|
||||
// 9 CAT_OP_UN_NOT `!`
|
||||
//
|
||||
// Group 10 to 14: Operators `printSpaceBeforeOperatorSlow` needs to tell apart (`last >= CAT_OP_UN_NOT_AFTER_LT`)
|
||||
// 10 CAT_OP_UN_NOT_AFTER_LT `!` written straight after a `<`
|
||||
// 11 CAT_OP_UN_PLUS `+`
|
||||
// 12 CAT_OP_UPD_INC `++`
|
||||
// 13 CAT_OP_UN_NEG `-`
|
||||
// 14 CAT_OP_UPD_DEC `--`
|
||||
|
||||
/**
|
||||
* A category code. One of the `CAT_*` constants below, and nothing else -
|
||||
* the range checks which read `state.last` are only sound over exactly this set.
|
||||
*/
|
||||
export type Category =
|
||||
| typeof CAT_IDENT
|
||||
| typeof CAT_INT_DIGIT
|
||||
| typeof CAT_REGEX_SLASH
|
||||
| typeof CAT_OTHER
|
||||
| typeof CAT_LT
|
||||
| typeof CAT_QUESTION
|
||||
| typeof CAT_START_OF_STMT
|
||||
| typeof CAT_START_OF_ARROW_EXPR
|
||||
| typeof CAT_START_OF_DEFAULT_EXPORT
|
||||
| typeof CAT_OP_UN_NOT
|
||||
| typeof CAT_OP_UN_NOT_AFTER_LT
|
||||
| typeof CAT_OP_UN_PLUS
|
||||
| typeof CAT_OP_UPD_INC
|
||||
| typeof CAT_OP_UN_NEG
|
||||
| typeof CAT_OP_UPD_DEC;
|
||||
|
||||
/** Identifier part - letters, digits, `_`, `$`, Unicode `ID_Continue`. */
|
||||
export const CAT_IDENT = 0;
|
||||
|
||||
/**
|
||||
* A numeric literal whose text is plain digits, so a following `.` would be read as part of the number -
|
||||
* `0 .toExponential()` needs the space, `1e3.x` and `.5.x` do not.
|
||||
*/
|
||||
export const CAT_INT_DIGIT = 1;
|
||||
|
||||
/**
|
||||
* A regex's closing `/`, written only where the regex has no flags.
|
||||
*
|
||||
* In the identifier range because `/a/ in x` needs the space just as much as `x in y` does -
|
||||
* without it the `in` would be read as regex flags. With flags, `CAT_IDENT` says the same thing.
|
||||
*/
|
||||
export const CAT_REGEX_SLASH = 2;
|
||||
|
||||
/** Anything not covered by another category - punctuation, whitespace, a quote. */
|
||||
export const CAT_OTHER = 3;
|
||||
|
||||
/** `<`, which a following `!` must not merge with into `<!--`. */
|
||||
export const CAT_LT = 4;
|
||||
|
||||
/** `?`, which must not merge with a following `?` into `??` - see `TSJSDocNullableType`. */
|
||||
export const CAT_QUESTION = 5;
|
||||
|
||||
/**
|
||||
* An `export default` expression is about to be printed, and nothing has been written since.
|
||||
*
|
||||
* Read by the function and class printers, which parenthesize themselves here so the declaration forms
|
||||
* (`export default function f() {}`) stay distinguishable from the expression ones.
|
||||
*
|
||||
* Immediately below `CAT_START_OF_STMT`, which is what makes their shared test one `|`.
|
||||
*/
|
||||
export const CAT_START_OF_DEFAULT_EXPORT = 6;
|
||||
|
||||
/**
|
||||
* An expression statement's expression is about to be printed, and nothing has been written since.
|
||||
*
|
||||
* A node which finds this in `last` is the leftmost token of the statement, however deeply nested it is,
|
||||
* so an object literal or an object destructuring assignment there parenthesizes itself.
|
||||
*
|
||||
* The only mark both readers ask about, so it is the odd one, with the other two either side.
|
||||
*/
|
||||
export const CAT_START_OF_STMT = 7;
|
||||
|
||||
/**
|
||||
* A concise arrow body is about to be printed, and nothing has been written since.
|
||||
*
|
||||
* Read by the same two printers as `CAT_START_OF_STMT` - `x => ({ a: 1 })` needs the parens for
|
||||
* the same reason a statement does, because a leading `{` would be read as a block.
|
||||
*
|
||||
* Immediately above `CAT_START_OF_STMT`, which is what makes their shared test one `|`.
|
||||
*/
|
||||
export const CAT_START_OF_ARROW_EXPR = 8;
|
||||
|
||||
/**
|
||||
* `!`, written anywhere other than straight after a `<` - both the unary operator and TS's
|
||||
* postfix `!` (non-null assertion, definite assignment), which postfix position keeps off a `<`.
|
||||
*
|
||||
* An operator code, but deliberately below the range `printSpaceBeforeOperator` gates on -
|
||||
* no following operator merges with a plain `!`, so storing this never costs the slow path a call.
|
||||
*/
|
||||
export const CAT_OP_UN_NOT = 9;
|
||||
|
||||
/**
|
||||
* `!` written immediately after a `<`, which is the `<!--` hazard.
|
||||
* Folding the check on the preceding character into the code saves tracking the second-last character.
|
||||
*
|
||||
* The first of the operators `printSpaceBeforeOperator` gates on - writing one of these
|
||||
* is what records it, so no separate field tracks which operator came last.
|
||||
*/
|
||||
export const CAT_OP_UN_NOT_AFTER_LT = 10;
|
||||
|
||||
/** `+`, which must not merge with a following `+` or `++`. */
|
||||
export const CAT_OP_UN_PLUS = 11;
|
||||
|
||||
/** `++`, which must not follow a `+` without a space. */
|
||||
export const CAT_OP_UPD_INC = 12;
|
||||
|
||||
/** `-`, which must not merge with a following `-` or `--`. */
|
||||
export const CAT_OP_UN_NEG = 13;
|
||||
|
||||
/** `--`, which must not follow a `-`, nor the `!` of a `<!`. */
|
||||
export const CAT_OP_UPD_DEC = 14;
|
||||
|
||||
/**
|
||||
* Every category, in numbering order.
|
||||
*
|
||||
* Only the debug checks which prove the range compares still match the sets they mean read this,
|
||||
* so release builds drop it entirely.
|
||||
*/
|
||||
export const ALL_CATEGORIES: Category[] = [
|
||||
CAT_IDENT,
|
||||
CAT_INT_DIGIT,
|
||||
CAT_REGEX_SLASH,
|
||||
CAT_OTHER,
|
||||
CAT_LT,
|
||||
CAT_QUESTION,
|
||||
CAT_START_OF_DEFAULT_EXPORT,
|
||||
CAT_START_OF_STMT,
|
||||
CAT_START_OF_ARROW_EXPR,
|
||||
CAT_OP_UN_NOT,
|
||||
CAT_OP_UN_NOT_AFTER_LT,
|
||||
CAT_OP_UN_PLUS,
|
||||
CAT_OP_UPD_INC,
|
||||
CAT_OP_UN_NEG,
|
||||
CAT_OP_UPD_DEC,
|
||||
];
|
||||
|
||||
// The five marker readers select a pair of marks by position rather than comparing each in turn.
|
||||
// `printFunction`, `printClass` and `printCallExpression` ask "statement or `export default`",
|
||||
// `printObjectExpression` and `printAssignmentExpression` ask "statement or concise arrow body":
|
||||
//
|
||||
// (last | 1) === CAT_START_OF_STMT // Statement or `export default`
|
||||
// ((last - 1) | 1) === CAT_START_OF_STMT // Statement or concise arrow body
|
||||
//
|
||||
// Both hold only while `CAT_START_OF_STMT` is odd and its two neighbours sit either side of it.
|
||||
// Check each against the set it is meant to select, over every category and no others.
|
||||
if (DEBUG) {
|
||||
for (const category of ALL_CATEGORIES) {
|
||||
debugAssert(
|
||||
((category | 1) === CAT_START_OF_STMT) ===
|
||||
(category === CAT_START_OF_STMT || category === CAT_START_OF_DEFAULT_EXPORT),
|
||||
`Category ${category} disagrees with \`(last | 1) === CAT_START_OF_STMT\``,
|
||||
);
|
||||
debugAssert(
|
||||
(((category - 1) | 1) === CAT_START_OF_STMT) ===
|
||||
(category === CAT_START_OF_STMT || category === CAT_START_OF_ARROW_EXPR),
|
||||
`Category ${category} disagrees with \`((last - 1) | 1) === CAT_START_OF_STMT\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `code` to the output, and record what it ends with.
|
||||
*
|
||||
* @param code - Text to append, never empty
|
||||
* @param last - Category of the last character of `code`
|
||||
*/
|
||||
export function write(state: State, code: string, last: Category): void {
|
||||
if (DEBUG) {
|
||||
debugAssertCategoryMatches(state, code, last);
|
||||
state.lastIsStale = false;
|
||||
state.lastCharWritten = code[code.length - 1];
|
||||
}
|
||||
|
||||
state.last = last;
|
||||
state.output += code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `code` to the output, leaving `state.last` describing whatever came before it.
|
||||
*
|
||||
* Only sound where the value of `last` is provably dead - another `write` must follow before anything reads it.
|
||||
* The readers are `printSpaceBeforeIdentifier` and the `CAT_LT`, `CAT_REGEX_SLASH` and `CAT_QUESTION`
|
||||
* adjacency checks, all of which run at the start of printing a construct.
|
||||
*
|
||||
* In practice that means using it for all but the final fragment when one token is written in pieces.
|
||||
* Debug builds track the rule and throw if a reader sees a stale `last`.
|
||||
*
|
||||
* @param code - Text to append, which unlike `write` may be empty
|
||||
*/
|
||||
export function writeNoLast(state: State, code: string): void {
|
||||
if (DEBUG) {
|
||||
state.lastIsStale = true;
|
||||
if (code.length > 0) state.lastCharWritten = code[code.length - 1];
|
||||
}
|
||||
|
||||
state.output += code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `code` to the output, record what it ends with, and record a source mapping for `node`.
|
||||
*
|
||||
* The mapping is only recorded where the caller asked for source maps and `node` carries a `loc` -
|
||||
* Oxc's own AST records byte spans instead, so the check is on the node, not the build.
|
||||
*
|
||||
* Builds without source map support have no use for this. For those builds, TSDown plugin rewrites every call
|
||||
* into `write` and drops the `node` argument, leaving this unreferenced for the minifier to remove.
|
||||
*
|
||||
* @param code - Text to append, never empty
|
||||
* @param last - Category of the last character of `code`
|
||||
* @param node - Node this text came from
|
||||
*/
|
||||
export function writeWithMap(state: State, code: string, last: Category, node: MappableNode): void {
|
||||
if (DEBUG) {
|
||||
debugAssertCategoryMatches(state, code, last);
|
||||
state.lastIsStale = false;
|
||||
state.lastCharWritten = code[code.length - 1];
|
||||
}
|
||||
|
||||
if (SOURCEMAPS && node.loc != null) {
|
||||
debugAssert(
|
||||
state.mapOffsets !== null && state.mapPositions !== null && state.mapNames !== null,
|
||||
"Source map arrays should exist when source maps are enabled",
|
||||
);
|
||||
|
||||
typeAssertIs<{ name?: string }>(node);
|
||||
state.mapOffsets.push(state.output.length);
|
||||
state.mapPositions.push(node.loc.start);
|
||||
state.mapNames.push(node.name);
|
||||
}
|
||||
|
||||
state.last = last;
|
||||
state.output += code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `code` and record a source mapping for `node`, leaving `state.last` alone.
|
||||
*
|
||||
* `writeNoLast`'s rule about `last` applies here too - another write must follow before anything reads it.
|
||||
*
|
||||
* Builds without source map support have no use for this. For those builds, TSDown plugin rewrites every call
|
||||
* into `write` and drops the `node` argument, leaving this unreferenced for the minifier to remove.
|
||||
*
|
||||
* @param code - Text to append, which unlike `writeWithMap` may be empty
|
||||
* @param node - Node this text came from
|
||||
*/
|
||||
export function writeWithMapNoLast(state: State, code: string, node: MappableNode): void {
|
||||
if (SOURCEMAPS && node.loc != null) {
|
||||
debugAssert(
|
||||
state.mapOffsets !== null && state.mapPositions !== null && state.mapNames !== null,
|
||||
"Source map arrays should exist when source maps are enabled",
|
||||
);
|
||||
|
||||
typeAssertIs<{ name?: string }>(node);
|
||||
state.mapOffsets.push(state.output.length);
|
||||
state.mapPositions.push(node.loc.start);
|
||||
state.mapNames.push(node.name);
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
state.lastIsStale = true;
|
||||
if (code.length > 0) state.lastCharWritten = code[code.length - 1];
|
||||
}
|
||||
|
||||
state.output += code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that `state.last` still describes what was written last.
|
||||
*
|
||||
* Every reader of `last` calls this, so a `writeNoLast` whose caller broke the rule is caught by
|
||||
* the conformance suites rather than silently incorrectly spacing one construct.
|
||||
*
|
||||
* Debug builds only - the call and its argument are removed from release builds entirely.
|
||||
*/
|
||||
export function debugAssertLastFresh(state: State): void {
|
||||
debugAssert(!state.lastIsStale, "`last` was read after `writeNoLast` left it stale");
|
||||
}
|
||||
|
||||
/** Matches a character which can continue an identifier. */
|
||||
const IDENT_CONTINUE_REGEX = /[\p{ID_Continue}$\u200C\u200D]/u;
|
||||
|
||||
/**
|
||||
* Assert that `last` truthfully describes the end of `code`.
|
||||
*
|
||||
* A category encodes the merge hazard of the trailing token, not the trailing character -
|
||||
* a digit legitimately ends both a `CAT_IDENT` and a `CAT_INT_DIGIT` write - so each final character
|
||||
* has a set of permitted categories rather than one right answer.
|
||||
*
|
||||
* Debug builds only. Removed by minifier in release builds.
|
||||
*/
|
||||
function debugAssertCategoryMatches(state: State, code: string, last: Category): void {
|
||||
if (!DEBUG) return;
|
||||
|
||||
debugAssert(code.length > 0, "`code` should not be an empty string");
|
||||
|
||||
const ch = code.at(-1)!;
|
||||
|
||||
let ok;
|
||||
if (ch >= "0" && ch <= "9") {
|
||||
// A digit ends an identifier (`x1`), a number some other character already separates
|
||||
// (`1e3`, `0x1f`, `.5`) or a written fragment of one (the digits of `15e-8`) - all `CAT_IDENT`.
|
||||
// Only a numeric literal which is plain digits throughout is `CAT_INT_DIGIT`.
|
||||
// The dangerous converse - a whole plain-digit literal claiming `CAT_IDENT`, which loses the space
|
||||
// in `0 .toExponential()` - looks exactly like a fragment write, so it cannot be caught here.
|
||||
// The AST fixtures pin the number printer's category decisions instead.
|
||||
ok = last === CAT_IDENT || (last === CAT_INT_DIGIT && /^[0-9]+$/.test(code));
|
||||
} else if (ch === "+") {
|
||||
if (code.endsWith("++")) {
|
||||
ok = last === CAT_OP_UPD_INC;
|
||||
} else {
|
||||
ok = last === CAT_OP_UN_PLUS;
|
||||
}
|
||||
} else if (ch === "-") {
|
||||
if (code.endsWith("--")) {
|
||||
ok = last === CAT_OP_UPD_DEC;
|
||||
} else {
|
||||
ok = last === CAT_OP_UN_NEG;
|
||||
}
|
||||
} else if (ch === "!") {
|
||||
// The two operator categories are told apart by the character before the `!`, which also proves
|
||||
// the `CAT_OP_UN_NOT_AFTER_LT` branch in `printUnaryExpression` fires exactly when it should
|
||||
if (last === CAT_OP_UN_NOT_AFTER_LT) {
|
||||
ok = state.lastCharWritten === "<";
|
||||
} else {
|
||||
ok = last === CAT_OP_UN_NOT && state.lastCharWritten !== "<";
|
||||
}
|
||||
} else if (ch === "<") {
|
||||
ok = last === CAT_LT;
|
||||
} else if (ch === "?") {
|
||||
ok = last === CAT_QUESTION;
|
||||
} else if (ch === "/") {
|
||||
ok = last === CAT_REGEX_SLASH;
|
||||
} else {
|
||||
// The final character, whole - for an astral character `ch` is only its low surrogate,
|
||||
// which no Unicode property matches
|
||||
const isLowSurrogate = ch >= "\uDC00" && ch <= "\uDFFF";
|
||||
const finalChar = isLowSurrogate && code.length >= 2 ? code.slice(-2) : ch;
|
||||
|
||||
if (IDENT_CONTINUE_REGEX.test(finalChar)) {
|
||||
ok = last === CAT_IDENT;
|
||||
} else if (ch <= "\x7F") {
|
||||
// ASCII
|
||||
ok = last === CAT_OTHER;
|
||||
} else {
|
||||
// Not an identifier character by this runtime's Unicode tables, but the parser's tables
|
||||
// may be a newer Unicode version which does count it, so both claims are believed -
|
||||
// test262 holds identifiers whose final characters are new enough to hit this
|
||||
ok = last === CAT_IDENT || last === CAT_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
debugAssert(ok, () => `Category ${last} does not describe the end of ${JSON.stringify(code)}`);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Printer state.
|
||||
//
|
||||
// One object, created here and handed to whichever printer build `index.ts` selects.
|
||||
// It is deliberately outside `print/`, so that all 4 builds share this one class and therefore one
|
||||
// object shape - a build-time flag must never add or remove a field, or the printers would each
|
||||
// see a different hidden class for the object they thread through every function.
|
||||
//
|
||||
// The printers only ever receive it, so they import it as a type.
|
||||
// Nothing in `print/` constructs one, and none of the 4 printer builds bundles this file.
|
||||
|
||||
import { CAT_OTHER } from "./print/write.ts";
|
||||
|
||||
import type { Category } from "./print/write.ts";
|
||||
import type { Options, Position } from "./print/options.ts";
|
||||
|
||||
/**
|
||||
* The string one level of indentation prints as. Set from the `indent` option, a tab by default.
|
||||
*/
|
||||
let indentString = "\t";
|
||||
|
||||
/**
|
||||
* Indentation cache, holding the string for each level - `indents[2]` is two levels' worth.
|
||||
*
|
||||
* There is one per process, and every `State` carries it, so all 4 printer builds grow and read
|
||||
* the same array - the cache a build fills is there for the next build to use.
|
||||
*
|
||||
* It survives across prints and is discarded only when `indentString` changes.
|
||||
*/
|
||||
const indents = [""];
|
||||
|
||||
/**
|
||||
* The `indent` option must be made up of only spaces and tabs, and not empty string.
|
||||
* Anything else falls back to a tab.
|
||||
*/
|
||||
const INDENT_REGEX = /^[ \t]+$/;
|
||||
|
||||
export class State {
|
||||
// Current output.
|
||||
// A string which is appended to as the printing process proceeds.
|
||||
declare output: string;
|
||||
|
||||
// Current indentation level.
|
||||
declare indentLevel: number;
|
||||
|
||||
// The process-wide indent cache and the string one level of it prints as.
|
||||
// `printIndent` reads the cache through here, and `growIndents` extends it.
|
||||
declare indents: string[];
|
||||
declare indentString: string;
|
||||
|
||||
// `true` if printing a JSX / TSX file. Only affects printing of TSX.
|
||||
declare isJsx: boolean;
|
||||
|
||||
// Operator/token glue tracking.
|
||||
declare pendingIndentAsSpace: boolean;
|
||||
|
||||
// Category of the last thing written - one of the `CAT_*` constants, never a character.
|
||||
//
|
||||
// It also carries the three `CAT_START_OF_*` markers, which say that an expression statement,
|
||||
// a concise arrow body or an `export default` expression is about to be printed, and nothing
|
||||
// has been written since.
|
||||
//
|
||||
// A node seeing one of those is the leftmost token of that construct, however deeply nested it is,
|
||||
// which is how an object literal, a function or a class knows to parenthesize itself.
|
||||
//
|
||||
// They work because the next write replaces `last`, so "nothing written since" needs nothing else recorded -
|
||||
// and because every position they mark follows whitespace, which is what the `CAT_OTHER` they displace
|
||||
// means for spacing.
|
||||
declare last: Category;
|
||||
|
||||
// `true` between a `writeNoLast` and the write which follows it,
|
||||
// i.e. while `last` describes something other than what was written last.
|
||||
// Only used in debug builds. See `debugAssertLastFresh`.
|
||||
declare lastIsStale: boolean;
|
||||
|
||||
// The character the output currently ends with.
|
||||
// Only used in debug builds. See `debugAssertCategoryMatches`.
|
||||
declare lastCharWritten: string;
|
||||
|
||||
// Deferred source mappings.
|
||||
// Either all 3 are arrays, or all 3 are `null` (when the printer was given no `sourceMap`).
|
||||
declare mapOffsets: number[] | null;
|
||||
declare mapPositions: Position[] | null;
|
||||
declare mapNames: (string | undefined)[] | null;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.output = "";
|
||||
|
||||
let indentLevel = options.startingIndentLevel;
|
||||
if (typeof indentLevel !== "number") indentLevel = 0;
|
||||
this.indentLevel = indentLevel;
|
||||
|
||||
// The `indent` option is validated here, not in the printer, and changing of it discards
|
||||
// the cache grown for the old `indentString`.
|
||||
// That should be rare - most users have an indent style they prefer, and use it consistently.
|
||||
let { indent } = options;
|
||||
if (typeof indent !== "string" || !INDENT_REGEX.test(indent)) indent = "\t";
|
||||
if (indent !== indentString) {
|
||||
indentString = indent;
|
||||
indents.length = 1;
|
||||
}
|
||||
this.indents = indents;
|
||||
this.indentString = indent;
|
||||
|
||||
// `.tsx` mode: lone type parameters print as `<T,>`
|
||||
this.isJsx = options.jsx === true;
|
||||
|
||||
// Operator/token glue tracking
|
||||
this.pendingIndentAsSpace = false;
|
||||
|
||||
// Start of output behaves like after whitespace. Reading the last character from `output`
|
||||
// directly would flatten V8's rope representation on every append-then-read (quadratic),
|
||||
// which is why the category is tracked rather than derived.
|
||||
this.last = CAT_OTHER;
|
||||
|
||||
// Debug-only fields for checking `last` is correct on both writes and reads
|
||||
if (DEBUG) {
|
||||
this.lastIsStale = false;
|
||||
this.lastCharWritten = "";
|
||||
}
|
||||
|
||||
// `writeWithMap` records the output offset and original position of every mapped node,
|
||||
// and `emitMappings` converts the offsets to generated line/column in one pass at the end
|
||||
if (options.sourceMap == null) {
|
||||
this.mapOffsets = null;
|
||||
this.mapPositions = null;
|
||||
this.mapNames = null;
|
||||
} else {
|
||||
this.mapOffsets = [];
|
||||
this.mapPositions = [];
|
||||
this.mapNames = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Acorn-JSX conformance.
|
||||
//
|
||||
// Tests this package prints every JSX fixture exactly as Rust `oxc_codegen` prints it.
|
||||
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { join as pathJoin } from "node:path";
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import { checkFixture, JSX_DIR_PATH } from "./utils/common.ts";
|
||||
|
||||
// The directory holds the recorded ASTs and tokens alongside the sources.
|
||||
// Only the sources matter here - both printers work from a fresh parse.
|
||||
const fixturePaths = (await readdir(JSX_DIR_PATH, { recursive: true }))
|
||||
.filter((path) => path.endsWith(".jsx"))
|
||||
.sort();
|
||||
|
||||
describe.concurrent("JSX", () => {
|
||||
for (const path of fixturePaths) {
|
||||
// oxlint-disable-next-line vitest/valid-title, vitest/expect-expect
|
||||
it(path, async (ctx) => {
|
||||
const sourceText = await readFile(pathJoin(JSX_DIR_PATH, path), "utf8");
|
||||
if (!checkFixture(path, sourceText, "jsx", "unambiguous")) ctx.skip();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,406 @@
|
||||
// Leftmost-position parenthesization.
|
||||
//
|
||||
// Three constructs record the output offset at which their child expression begins - an expression
|
||||
// statement, a concise arrow body, and `export default`. A node printed at exactly that offset is
|
||||
// the leftmost token of the construct, and several node types parenthesize themselves on finding
|
||||
// that they are: an object literal or an object-destructuring assignment would otherwise open what
|
||||
// reads as a block, and a function or class expression would read as a declaration.
|
||||
//
|
||||
// This suite pins that observable behaviour, not the mechanism which produces it. Every case is a
|
||||
// source snippet printed by Rust `oxc_codegen` and by this package, and the two must agree - so
|
||||
// nothing here states where the parens belong, only that both printers put them in the same places.
|
||||
// It is meant to survive a rewrite of the position-marker machinery, and to be the gate on one.
|
||||
//
|
||||
// The matrix is every expression which can be the leftmost token of a construct, crossed with the
|
||||
// three constructs which record an offset. A combination which cannot be written at all - `yield`
|
||||
// in an arrow body, or anything needing an enclosing function after `export default` - is skipped
|
||||
// with its reason, rather than quietly dropped.
|
||||
//
|
||||
// A snippet which does not parse is a fault in the snippet, not a reason to skip it, so every case
|
||||
// requires `checkFixture` to have compared something.
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { checkFixture } from "./utils/common.ts";
|
||||
|
||||
import type { Lang } from "./utils/common.ts";
|
||||
|
||||
// --- Cases ------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* What an expression needs around it to be legal.
|
||||
*
|
||||
* Each context turns this into the enclosing code, or into the reason it cannot supply it.
|
||||
*/
|
||||
type Needs =
|
||||
| "nothing"
|
||||
/** `yield` */
|
||||
| "generator"
|
||||
/** `await` */
|
||||
| "async"
|
||||
/** `new.target` */
|
||||
| "function"
|
||||
/** A private name */
|
||||
| "class"
|
||||
/** `super` */
|
||||
| "derived-class";
|
||||
|
||||
/**
|
||||
* Where the bare source of an expression reads as something other than an expression.
|
||||
*
|
||||
* - `"block"` - a leading `{` opens a block at a statement start and a block body in an arrow, so
|
||||
* both of those contexts parenthesize the source. `export default {}` is unambiguous, and takes it
|
||||
* bare.
|
||||
* - `"comma"` - a top-level `,` ends both an arrow body and an `export default`, so both of those
|
||||
* contexts parenthesize the source. A statement takes it bare.
|
||||
* - `"declaration"` - a leading `function` or `class` is a declaration at a statement start and after
|
||||
* `export default`, so neither can host it. The `function-parenthesized` and `class-parenthesized`
|
||||
* cases reach the marker in those two contexts instead.
|
||||
*/
|
||||
type Ambiguity = "block" | "comma" | "declaration";
|
||||
|
||||
interface Case {
|
||||
/** Test name, unique within the matrix */
|
||||
name: string;
|
||||
/** Source of the expression whose leftmost token is under test. Must be a single line. */
|
||||
expr: string;
|
||||
/** What the expression needs around it. `"nothing"` when absent. */
|
||||
needs?: Needs;
|
||||
/** Where `expr` does not read as an expression */
|
||||
ambiguity?: Ambiguity;
|
||||
/** Language to parse as. `"js"` when absent. */
|
||||
lang?: Lang;
|
||||
/** `true` where the snippet is only legal in a module */
|
||||
module?: boolean;
|
||||
}
|
||||
|
||||
const CASES: Case[] = [
|
||||
// Identifiers, `this`, and every literal kind
|
||||
{ name: "identifier", expr: "x" },
|
||||
{ name: "this", expr: "this" },
|
||||
{ name: "string", expr: `"str"` },
|
||||
{ name: "number", expr: "1" },
|
||||
{ name: "number-decimal", expr: "1.5" },
|
||||
{ name: "number-negative", expr: "-1" },
|
||||
{ name: "bigint", expr: "1n" },
|
||||
{ name: "bigint-negative", expr: "-1n" },
|
||||
{ name: "regex", expr: "/a/" },
|
||||
{ name: "regex-flags", expr: "/a/g" },
|
||||
{ name: "boolean-true", expr: "true" },
|
||||
{ name: "boolean-false", expr: "false" },
|
||||
{ name: "null", expr: "null" },
|
||||
|
||||
// Member access, calls and `new`
|
||||
{ name: "member-dot", expr: "x.y" },
|
||||
{ name: "member-computed", expr: "x[0]" },
|
||||
{ name: "member-deep", expr: "x.y.z.w" },
|
||||
{ name: "member-private", expr: "this.#x", needs: "class" },
|
||||
{ name: "call", expr: "f()" },
|
||||
{ name: "call-arguments", expr: "f(1, 2)" },
|
||||
{ name: "call-of-call", expr: "f()()" },
|
||||
{ name: "call-of-member", expr: "x.y()" },
|
||||
{ name: "call-parenthesized-callee", expr: "(f())()" },
|
||||
{ name: "new", expr: "new C()" },
|
||||
{ name: "new-no-arguments", expr: "new C" },
|
||||
{ name: "new-member-callee", expr: "new x.C()" },
|
||||
{ name: "new-call-callee", expr: "new (f())()" },
|
||||
{ name: "template", expr: "`t`" },
|
||||
{ name: "template-substitution", expr: "`a${b}c`" },
|
||||
{ name: "tagged-template", expr: "tag`t`" },
|
||||
{ name: "tagged-template-member-tag", expr: "x.tag`t`" },
|
||||
|
||||
// Optional chaining, which arrives wrapped in a `ChainExpression`
|
||||
{ name: "chain-member", expr: "x?.y" },
|
||||
{ name: "chain-computed", expr: "x?.[0]" },
|
||||
{ name: "chain-call", expr: "x?.()" },
|
||||
{ name: "chain-deep", expr: "x?.y.z" },
|
||||
|
||||
// Operators
|
||||
{ name: "binary-add", expr: "x + y" },
|
||||
{ name: "binary-exponent", expr: "x ** y" },
|
||||
{ name: "binary-in", expr: "x in y" },
|
||||
{ name: "binary-instanceof", expr: "x instanceof y" },
|
||||
{ name: "logical-and", expr: "x && y" },
|
||||
{ name: "logical-or", expr: "x || y" },
|
||||
{ name: "logical-nullish", expr: "x ?? y" },
|
||||
{ name: "private-in", expr: "#x in obj", needs: "class" },
|
||||
{ name: "conditional", expr: "x ? y : z" },
|
||||
{ name: "sequence", expr: "x, y", ambiguity: "comma" },
|
||||
{ name: "unary-negate", expr: "-x" },
|
||||
{ name: "unary-plus", expr: "+x" },
|
||||
{ name: "unary-not", expr: "!x" },
|
||||
{ name: "unary-bitwise-not", expr: "~x" },
|
||||
{ name: "unary-typeof", expr: "typeof x" },
|
||||
{ name: "unary-void", expr: "void 0" },
|
||||
{ name: "unary-delete", expr: "delete x.y" },
|
||||
{ name: "update-prefix-increment", expr: "++x" },
|
||||
{ name: "update-prefix-decrement", expr: "--x" },
|
||||
{ name: "update-postfix-increment", expr: "x++" },
|
||||
{ name: "update-postfix-decrement", expr: "x--" },
|
||||
|
||||
// Assignment, including the two destructuring targets
|
||||
{ name: "assign", expr: "x = 1" },
|
||||
{ name: "assign-compound", expr: "x += 1" },
|
||||
{ name: "assign-logical", expr: "x ||= 1" },
|
||||
{ name: "assign-member-target", expr: "x.y = 1" },
|
||||
{ name: "assign-object-pattern", expr: "{ a } = b", ambiguity: "block" },
|
||||
{ name: "assign-object-pattern-nested", expr: "{ a: { b } } = c", ambiguity: "block" },
|
||||
{ name: "assign-object-pattern-default", expr: "{ a = 1 } = b", ambiguity: "block" },
|
||||
{ name: "assign-object-pattern-rest", expr: "{ ...a } = b", ambiguity: "block" },
|
||||
{ name: "assign-array-pattern", expr: "[a] = b" },
|
||||
{ name: "assign-array-pattern-hole", expr: "[, a] = b" },
|
||||
{ name: "assign-array-pattern-rest", expr: "[...a] = b" },
|
||||
|
||||
// Object and array literals
|
||||
{ name: "object-empty", expr: "{}", ambiguity: "block" },
|
||||
{ name: "object-one-property", expr: "{ a: 1 }", ambiguity: "block" },
|
||||
{ name: "object-two-properties", expr: "{ a: 1, b: 2 }", ambiguity: "block" },
|
||||
{ name: "object-shorthand", expr: "{ a }", ambiguity: "block" },
|
||||
{ name: "object-method", expr: "{ m() {} }", ambiguity: "block" },
|
||||
{ name: "object-spread", expr: "{ ...x }", ambiguity: "block" },
|
||||
{ name: "array-empty", expr: "[]" },
|
||||
{ name: "array-elements", expr: "[1, 2]" },
|
||||
{ name: "array-holding-object", expr: "[{}]" },
|
||||
|
||||
// Functions, classes and arrows
|
||||
{ name: "function", expr: "function () {}", ambiguity: "declaration" },
|
||||
{ name: "function-named", expr: "function f() {}", ambiguity: "declaration" },
|
||||
{ name: "function-async", expr: "async function () {}", ambiguity: "declaration" },
|
||||
{ name: "function-generator", expr: "function* () {}", ambiguity: "declaration" },
|
||||
{ name: "function-parenthesized", expr: "(function () {})" },
|
||||
{ name: "function-parenthesized-named", expr: "(function f() {})" },
|
||||
{ name: "class", expr: "class {}", ambiguity: "declaration" },
|
||||
{ name: "class-named", expr: "class C {}", ambiguity: "declaration" },
|
||||
{ name: "class-extends", expr: "class extends D {}", ambiguity: "declaration" },
|
||||
{ name: "class-parenthesized", expr: "(class {})" },
|
||||
{ name: "class-parenthesized-named", expr: "(class C {})" },
|
||||
{ name: "arrow", expr: "() => 1" },
|
||||
{ name: "arrow-async", expr: "async () => 1" },
|
||||
{ name: "arrow-single-parameter", expr: "x => x" },
|
||||
{ name: "arrow-block-body", expr: "() => {}" },
|
||||
{ name: "arrow-object-body", expr: "() => ({ a: 1 })" },
|
||||
{ name: "arrow-of-arrow-object-body", expr: "() => () => ({ a: 1 })" },
|
||||
|
||||
// `await`, `yield`, `import()`, and the meta properties
|
||||
{ name: "await", expr: "await x", needs: "async" },
|
||||
{ name: "await-member", expr: "await x.y", needs: "async" },
|
||||
{ name: "yield", expr: "yield x", needs: "generator" },
|
||||
{ name: "yield-bare", expr: "yield", needs: "generator" },
|
||||
{ name: "yield-delegate", expr: "yield* x", needs: "generator" },
|
||||
{ name: "import-call", expr: `import("mod")` },
|
||||
{ name: "import-call-member", expr: `import("mod").then` },
|
||||
{ name: "import-meta", expr: "import.meta", module: true },
|
||||
{ name: "import-meta-member", expr: "import.meta.url", module: true },
|
||||
{ name: "new-target", expr: "new.target", needs: "function" },
|
||||
|
||||
// `super`
|
||||
{ name: "super-call", expr: "super()", needs: "derived-class" },
|
||||
{ name: "super-property", expr: "super.x", needs: "derived-class" },
|
||||
{ name: "super-computed", expr: "super[0]", needs: "derived-class" },
|
||||
|
||||
// Sources whose parens survive into the AST in the preserve-parens mode
|
||||
{ name: "parenthesized-identifier", expr: "(x)" },
|
||||
{ name: "parenthesized-object", expr: "({})" },
|
||||
{ name: "parenthesized-object-twice", expr: "(({}))" },
|
||||
{ name: "parenthesized-sequence", expr: "(x, y)" },
|
||||
{ name: "parenthesized-arrow", expr: "(() => 1)" },
|
||||
|
||||
// Nested leftmost chains - the node at the recorded offset is arbitrarily deep
|
||||
{ name: "nested-object-member", expr: "({}).x" },
|
||||
{ name: "nested-object-member-deep", expr: "({}).a.b.c" },
|
||||
{ name: "nested-object-computed", expr: "({})[0]" },
|
||||
{ name: "nested-object-call", expr: "({}).x()" },
|
||||
{ name: "nested-object-optional", expr: "({})?.x" },
|
||||
{ name: "nested-object-assign", expr: "({ a: 1 }).b = 2" },
|
||||
{ name: "nested-object-update", expr: "({}).x++" },
|
||||
{ name: "nested-object-binary", expr: "({}) + 1" },
|
||||
{ name: "nested-object-instanceof", expr: "({}) instanceof Object" },
|
||||
{ name: "nested-object-logical", expr: "({}) && x" },
|
||||
{ name: "nested-object-conditional", expr: "({}) ? a : b" },
|
||||
{ name: "nested-object-sequence", expr: "({}), 1", ambiguity: "comma" },
|
||||
{ name: "nested-object-tagged-template", expr: "({}).tag`t`" },
|
||||
{ name: "nested-object-new-callee", expr: "new (({}).C)()" },
|
||||
{ name: "nested-object-pattern-assign-member", expr: "({ a } = b).c" },
|
||||
{ name: "nested-function-call", expr: "(function () {})()" },
|
||||
{ name: "nested-function-member", expr: "(function () {}).call()" },
|
||||
{ name: "nested-function-assign", expr: "(function () {}).x = 1" },
|
||||
{ name: "nested-function-binary", expr: "(function () {}) + 1" },
|
||||
{ name: "nested-function-tagged-template", expr: "(function () {})`t`" },
|
||||
{ name: "nested-function-async-call", expr: "(async function () {})()" },
|
||||
{ name: "nested-function-generator-call", expr: "(function* () {})()" },
|
||||
{ name: "nested-class-member", expr: "(class {}).x" },
|
||||
{ name: "nested-class-call", expr: "(class {})()" },
|
||||
{ name: "nested-class-binary", expr: "(class {}) + 1" },
|
||||
{ name: "nested-arrow-call", expr: "(() => 1)()" },
|
||||
|
||||
// Shapes which must NOT be parenthesized - the node sits past the recorded offset
|
||||
{ name: "object-as-argument", expr: "f({})" },
|
||||
{ name: "object-as-assignment-value", expr: "x = {}" },
|
||||
{ name: "object-as-property-value", expr: "({ a: {} })" },
|
||||
{ name: "object-as-right-operand", expr: "1 + {}" },
|
||||
{ name: "object-after-unary-not", expr: "!{}" },
|
||||
{ name: "object-after-typeof", expr: "typeof {}" },
|
||||
{ name: "function-as-argument", expr: "f(function () {})" },
|
||||
{ name: "function-as-assignment-value", expr: "x = function () {}" },
|
||||
{ name: "class-as-assignment-value", expr: "x = class {}" },
|
||||
|
||||
// JSX
|
||||
{ name: "jsx-element", expr: "<div />", lang: "jsx" },
|
||||
{ name: "jsx-element-children", expr: "<div>text</div>", lang: "jsx" },
|
||||
{ name: "jsx-element-member-name", expr: "<a.b />", lang: "jsx" },
|
||||
{ name: "jsx-fragment", expr: "<></>", lang: "jsx" },
|
||||
{ name: "nested-jsx-member", expr: "(<div />).props", lang: "jsx" },
|
||||
|
||||
// TypeScript
|
||||
{ name: "ts-as", expr: "x as any", lang: "ts" },
|
||||
{ name: "ts-satisfies", expr: "x satisfies object", lang: "ts" },
|
||||
{ name: "ts-non-null", expr: "x!", lang: "ts" },
|
||||
{ name: "ts-type-assertion", expr: "<string> x", lang: "ts" },
|
||||
{ name: "ts-instantiation", expr: "f<string>", lang: "ts" },
|
||||
{ name: "ts-as-object", expr: "{} as any", lang: "ts", ambiguity: "block" },
|
||||
{ name: "ts-satisfies-object", expr: "{} satisfies object", lang: "ts", ambiguity: "block" },
|
||||
{ name: "ts-type-assertion-object", expr: "<any> {}", lang: "ts" },
|
||||
{ name: "nested-ts-as-object", expr: "({} as any).x", lang: "ts" },
|
||||
{ name: "nested-ts-non-null-object", expr: "({})!", lang: "ts" },
|
||||
{ name: "nested-ts-non-null-object-member", expr: "({})!.x", lang: "ts" },
|
||||
{ name: "nested-ts-as-function", expr: "(function () {}) as any", lang: "ts" },
|
||||
// `TSParenthesizedType`, the type-level counterpart, which the preserve-parens mode keeps
|
||||
{ name: "ts-as-parenthesized-type", expr: "x as (string)", lang: "ts" },
|
||||
{ name: "ts-type-assertion-parenthesized-type", expr: "<(string)> x", lang: "ts" },
|
||||
{ name: "nested-ts-as-parenthesized-type-object", expr: "({} as (any)).x", lang: "ts" },
|
||||
];
|
||||
|
||||
// --- Contexts ---------------------------------------------------------------------------------
|
||||
|
||||
/** A built snippet, or the reason a context cannot host the expression. */
|
||||
type Built = { source: string } | { skip: string };
|
||||
|
||||
interface Context {
|
||||
/** Name of the construct which records the offset */
|
||||
name: string;
|
||||
/** `true` where the construct is only legal in a module */
|
||||
module: boolean;
|
||||
/** Build the snippet for a case, or give the reason this construct cannot host it */
|
||||
build: (testCase: Case) => Built;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a statement inside whatever it needs around it.
|
||||
*
|
||||
* Every snippet opens with a throwaway `null;` immediately before the construct under test, so that
|
||||
* the construct never starts at output offset 0 - where a marker still holding its initial value
|
||||
* would match by accident - and so that a leading string literal is an expression statement rather
|
||||
* than a directive.
|
||||
*
|
||||
* @param needs - What the statement needs around it
|
||||
* @param statement - The statement, which must be a single line
|
||||
* @returns Snippet source
|
||||
*/
|
||||
function enclose(needs: Needs, statement: string): string {
|
||||
switch (needs) {
|
||||
case "async":
|
||||
return `async function f() {\n\tnull;\n\t${statement}\n}\n`;
|
||||
case "generator":
|
||||
return `function* g() {\n\tnull;\n\t${statement}\n}\n`;
|
||||
case "function":
|
||||
return `function f() {\n\tnull;\n\t${statement}\n}\n`;
|
||||
case "class":
|
||||
return `class C {\n\t#x;\n\tm(obj) {\n\t\tnull;\n\t\t${statement}\n\t}\n}\n`;
|
||||
case "derived-class":
|
||||
return `class C extends D {\n\tconstructor() {\n\t\tnull;\n\t\t${statement}\n\t}\n}\n`;
|
||||
default:
|
||||
return `null;\n${statement}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
/** The reason `export default` cannot host an expression needing an enclosing construct. */
|
||||
const EXPORT_DEFAULT_NEEDS_SKIPS: Partial<Record<Needs, string>> = {
|
||||
generator:
|
||||
"`yield` needs an enclosing generator, and `export default` is only legal at the top level of a module",
|
||||
function:
|
||||
"`new.target` needs an enclosing function, and `export default` is only legal at the top level of a module",
|
||||
class:
|
||||
"a private name needs an enclosing class body, and `export default` is only legal at the top level of a module",
|
||||
"derived-class":
|
||||
"`super` needs an enclosing class method, and `export default` is only legal at the top level of a module",
|
||||
};
|
||||
|
||||
const CONTEXTS: Context[] = [
|
||||
{
|
||||
name: "expression statement",
|
||||
module: false,
|
||||
build({ expr, needs = "nothing", ambiguity }) {
|
||||
if (ambiguity === "declaration") {
|
||||
return {
|
||||
skip:
|
||||
"a statement starting `function` or `class` is a declaration, not an expression - " +
|
||||
"the `function-parenthesized` and `class-parenthesized` cases cover this construct instead",
|
||||
};
|
||||
}
|
||||
return { source: enclose(needs, `${ambiguity === "block" ? `(${expr})` : expr};`) };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arrow body",
|
||||
module: false,
|
||||
build({ expr, needs = "nothing", ambiguity }) {
|
||||
if (needs === "generator") {
|
||||
return {
|
||||
skip: "an arrow body is parsed without the enclosing generator's `yield`, so `yield` there is a syntax error",
|
||||
};
|
||||
}
|
||||
const body = ambiguity === "block" || ambiguity === "comma" ? `(${expr})` : expr;
|
||||
// An `await` body needs the arrow itself to be async, rather than an enclosing async function
|
||||
if (needs === "async") return { source: enclose("nothing", `async () => ${body};`) };
|
||||
return { source: enclose(needs, `() => ${body};`) };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "export default",
|
||||
module: true,
|
||||
build({ expr, needs = "nothing", ambiguity }) {
|
||||
if (ambiguity === "declaration") {
|
||||
return {
|
||||
skip:
|
||||
"`export default function () {}` is a `FunctionDeclaration` and `export default class {}` a " +
|
||||
"`ClassDeclaration`, neither of which records an offset - the `function-parenthesized` and " +
|
||||
"`class-parenthesized` cases cover this construct instead",
|
||||
};
|
||||
}
|
||||
// `await` needs nothing: a module allows it at the top level
|
||||
const needsSkip = EXPORT_DEFAULT_NEEDS_SKIPS[needs];
|
||||
if (needsSkip !== undefined) return { skip: needsSkip };
|
||||
|
||||
return { source: `null;\nexport default ${ambiguity === "comma" ? `(${expr})` : expr};\n` };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// --- Tests ------------------------------------------------------------------------------------
|
||||
|
||||
describe.concurrent("leftmost position", () => {
|
||||
for (const context of CONTEXTS) {
|
||||
// oxlint-disable-next-line vitest/valid-title
|
||||
describe(context.name, () => {
|
||||
for (const testCase of CASES) {
|
||||
const built = context.build(testCase);
|
||||
|
||||
// Reported as skipped, carrying the reason, rather than left out of the run
|
||||
// oxlint-disable-next-line vitest/no-conditional-tests
|
||||
if ("skip" in built) {
|
||||
// oxlint-disable-next-line vitest/valid-title, vitest/expect-expect
|
||||
it(testCase.name, (ctx) => ctx.skip(built.skip));
|
||||
continue;
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line vitest/valid-title
|
||||
it(testCase.name, () => {
|
||||
const lang = testCase.lang ?? "js";
|
||||
const sourceType = context.module || testCase.module ? "module" : "script";
|
||||
const checked = checkFixture(`leftmost.${lang}`, built.source, lang, sourceType);
|
||||
expect(checked, `snippet does not parse:\n${built.source}`).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,403 @@
|
||||
// Hand-built AST tests, for the shapes the conformance suites cannot reach.
|
||||
//
|
||||
// The suites print ASTs produced by a parser, so anything a parser never produces is invisible to
|
||||
// them. The clearest case is a negative numeric literal: `-1` parses as a `UnaryExpression` around
|
||||
// `Literal(1)`, so `Literal(-1)` never appears, and the printer's whole negative-number path is
|
||||
// unreachable from any fixture. A stale identifier on that path once survived all 58,609 suite
|
||||
// fixtures and only turned up in `node --run lint`.
|
||||
//
|
||||
// The ASTs here are written by hand to reach exactly those places, and each case states the output
|
||||
// the printer is meant to produce for it. Anything which changes an expected string is either a bug
|
||||
// or a deliberate change which needs the string updated with it.
|
||||
|
||||
import { parseSync } from "oxc-parser";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { printSync } from "../dist/index.js";
|
||||
|
||||
import type * as ESTree from "../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
// Parse options for the cases which give source rather than an AST.
|
||||
//
|
||||
// `preserveParens: false` because this printer deliberately does not support the redundant
|
||||
// `ParenthesizedExpression` node. `experimentalRawTransfer` is how the printer is meant to be used,
|
||||
// so the ASTs are the shape it really sees.
|
||||
//
|
||||
// `experimentalRawTransfer` is not in the published `ParserOptions` type - it is experimental and
|
||||
// untyped. Declaring the object here rather than inline at the call site keeps TypeScript's excess
|
||||
// property check (which only applies to object literals) from rejecting it, without a cast.
|
||||
const PARSE_OPTIONS = {
|
||||
preserveParens: false,
|
||||
experimentalRawTransfer: true,
|
||||
};
|
||||
|
||||
// --- AST builders ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build an AST node.
|
||||
*
|
||||
* The printer reads only the fields these builders set, so the nodes below carry none of the
|
||||
* `start` / `end` positions a parser would attach. This puts the cast that says so in one place,
|
||||
* rather than at every builder.
|
||||
*
|
||||
* @param fields - Fields the node needs
|
||||
* @returns The node
|
||||
*/
|
||||
const node = <T extends ESTree.Node>(fields: Partial<T>): T => fields as T;
|
||||
|
||||
const program = (...body: ESTree.Statement[]) =>
|
||||
node<ESTree.Program>({ type: "Program", sourceType: "module", body });
|
||||
const stmt = (expression: ESTree.Expression) =>
|
||||
node<ESTree.ExpressionStatement>({ type: "ExpressionStatement", expression });
|
||||
const num = (value: number) => node<ESTree.NumericLiteral>({ type: "Literal", value });
|
||||
const str = (value: string) => node<ESTree.StringLiteral>({ type: "Literal", value });
|
||||
const id = (name: string) => node<ESTree.IdentifierReference>({ type: "Identifier", name });
|
||||
const regex = (pattern: string, flags: string) =>
|
||||
node<ESTree.RegExpLiteral>({ type: "Literal", value: null, regex: { pattern, flags } });
|
||||
const big = (value: string) => node<ESTree.BigIntLiteral>({ type: "Literal", bigint: value });
|
||||
const member = (object: ESTree.Expression, property: ESTree.IdentifierName) =>
|
||||
node<ESTree.StaticMemberExpression>({
|
||||
type: "MemberExpression",
|
||||
object,
|
||||
property,
|
||||
computed: false,
|
||||
optional: false,
|
||||
});
|
||||
const call = (callee: ESTree.Expression, args: ESTree.Argument[] = []) =>
|
||||
node<ESTree.CallExpression>({
|
||||
type: "CallExpression",
|
||||
callee,
|
||||
arguments: args,
|
||||
optional: false,
|
||||
});
|
||||
const unary = (operator: ESTree.UnaryOperator, argument: ESTree.Expression) =>
|
||||
node<ESTree.UnaryExpression>({ type: "UnaryExpression", operator, argument, prefix: true });
|
||||
const update = (
|
||||
operator: ESTree.UpdateOperator,
|
||||
argument: ESTree.SimpleAssignmentTarget,
|
||||
prefix = true,
|
||||
) => node<ESTree.UpdateExpression>({ type: "UpdateExpression", operator, argument, prefix });
|
||||
const bin = (operator: ESTree.BinaryOperator, left: ESTree.Expression, right: ESTree.Expression) =>
|
||||
node<ESTree.BinaryExpression>({ type: "BinaryExpression", operator, left, right });
|
||||
const seq = (...expressions: ESTree.Expression[]) =>
|
||||
node<ESTree.SequenceExpression>({ type: "SequenceExpression", expressions });
|
||||
const arr = (...elements: ESTree.ArrayExpressionElement[]) =>
|
||||
node<ESTree.ArrayExpression>({ type: "ArrayExpression", elements });
|
||||
const ret = (argument: ESTree.Expression) =>
|
||||
node<ESTree.ReturnStatement>({ type: "ReturnStatement", argument });
|
||||
|
||||
// One expression statement, which is how nearly every case below is shaped
|
||||
const e = (expression: ESTree.Expression) => program(stmt(expression));
|
||||
|
||||
// --- Runner ---------------------------------------------------------------------------------
|
||||
|
||||
/** A case: its name, the AST to print, and the output the printer must produce for it. */
|
||||
type Case = [name: string, ast: ESTree.Node, output: string];
|
||||
|
||||
/**
|
||||
* Define one test per case, checking the printer's output is exactly the expected string.
|
||||
*
|
||||
* @param cases - The cases
|
||||
*/
|
||||
function checkCases(cases: Case[]): void {
|
||||
test.each(cases)("%s", (_name, ast, output) => {
|
||||
expect(printSync(ast)).toBe(output);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Tests ----------------------------------------------------------------------------------
|
||||
|
||||
// A numeric literal followed by `.` needs a space when the number is plain digits, because
|
||||
// `0.toExponential()` would lex the `.` as part of the number. This is the `needSpaceBeforeDot`
|
||||
// path, and every case below is unreachable from a parsed fixture in at least one respect.
|
||||
describe("member access on numbers", () => {
|
||||
checkCases([
|
||||
["int-zero", e(call(member(num(0), id("toExponential")))), "0 .toExponential();\n"],
|
||||
["int-one", e(member(num(1), id("x"))), "1 .x;\n"],
|
||||
["int-large", e(member(num(255), id("x"))), "255 .x;\n"],
|
||||
["int-999", e(member(num(999), id("x"))), "999 .x;\n"],
|
||||
["int-1000", e(member(num(1000), id("x"))), "1e3.x;\n"],
|
||||
["decimal", e(member(num(0.5), id("x"))), ".5.x;\n"],
|
||||
["decimal-leading", e(member(num(1.5), id("x"))), "1.5.x;\n"],
|
||||
["exponent-large", e(member(num(1e21), id("x"))), "1e21.x;\n"],
|
||||
["exponent-small", e(member(num(1e-7), id("x"))), "1e-7.x;\n"],
|
||||
["negative-int", e(member(num(-1), id("x"))), "(-1).x;\n"],
|
||||
["negative-decimal", e(member(num(-0.5), id("x"))), "(-.5).x;\n"],
|
||||
["negative-infinity", e(member(num(-Infinity), id("x"))), "(-Infinity).x;\n"],
|
||||
["infinity", e(member(num(Infinity), id("x"))), "Infinity.x;\n"],
|
||||
["nan", e(member(num(NaN), id("x"))), "NaN.x;\n"],
|
||||
["chained-int", e(member(member(num(7), id("x")), id("y"))), "7 .x.y;\n"],
|
||||
["call-arg-int", e(call(id("f"), [member(num(3), id("x"))])), "f(3 .x);\n"],
|
||||
// The shortened forms, which all carry a `.`, an `e` or an `x` of their own and so must NOT
|
||||
// gain the space, against the long integers which stay plain digits and must
|
||||
["int-1500", e(member(num(1500), id("x"))), "1500 .x;\n"],
|
||||
["int-max-safe", e(member(num(9007199254740991), id("x"))), "9007199254740991 .x;\n"],
|
||||
["hex", e(member(num(281474976710655), id("x"))), "0xffffffffffff.x;\n"],
|
||||
["exponent-fold", e(member(num(1.2e101), id("x"))), "12e100.x;\n"],
|
||||
]);
|
||||
});
|
||||
|
||||
describe("numbers", () => {
|
||||
// Bare numeric literals, including the negative and non-finite paths
|
||||
checkCases([
|
||||
["int", e(num(42)), "42;\n"],
|
||||
["zero", e(num(0)), "0;\n"],
|
||||
["negative-zero", e(num(-0)), "-0;\n"],
|
||||
["negative-int", e(num(-1)), "-1;\n"],
|
||||
["negative-decimal", e(num(-0.5)), "-.5;\n"],
|
||||
["negative-large", e(num(-1e21)), "-1e21;\n"],
|
||||
["negative-small", e(num(-0.000001)), "-1e-6;\n"],
|
||||
["infinity", e(num(Infinity)), "Infinity;\n"],
|
||||
["negative-infinity", e(num(-Infinity)), "-Infinity;\n"],
|
||||
["nan", e(num(NaN)), "NaN;\n"],
|
||||
["exponent", e(num(1e21)), "1e21;\n"],
|
||||
["return-negative", program(ret(num(-1))), "return -1;\n"],
|
||||
["return-int", program(ret(num(1))), "return 1;\n"],
|
||||
["binary-negative", e(bin("+", id("a"), num(-1))), "a + -1;\n"],
|
||||
[
|
||||
"array-negatives",
|
||||
e(arr(num(-1), num(-0), num(-Infinity))),
|
||||
"[\n\t-1,\n\t-0,\n\t-Infinity\n];\n",
|
||||
],
|
||||
]);
|
||||
|
||||
// The shortest-form rules. A number prints as whichever of plain digits, hexadecimal and
|
||||
// exponent notation is shortest, and the suites reach almost none of it: real code writes small
|
||||
// integers. Every form and every "not worth it" boundary is here.
|
||||
checkCases([
|
||||
["int-1500", e(num(1500)), "1500;\n"], // trailing zeros too few to pay for the `e`
|
||||
// 16 digits, and hexadecimal still does not pay
|
||||
["int-max-safe", e(num(9007199254740991)), "9007199254740991;\n"],
|
||||
["hex", e(num(281474976710655)), "0xffffffffffff;\n"],
|
||||
["hex-negative", e(num(-281474976710655)), "-0xffffffffffff;\n"],
|
||||
// hexadecimal is tried first and wins, though `1e12` is shorter
|
||||
["hex-over-exponent", e(num(1e12)), "0xe8d4a51000;\n"],
|
||||
// The two cases above reach the hexadecimal test from plain digits. These reach it from
|
||||
// exponent notation, where the length it is judged against is one less than the text `String`
|
||||
// gave, because the `+` of the exponent always goes. The second is the boundary: hexadecimal is
|
||||
// exactly as long as the exponent form, so it must lose - and would wrongly win, as
|
||||
// `0x21e2073de72ea800000`, if that one character were not taken off.
|
||||
["hex-from-exponent", e(num(1.0000990573316814e21)), "0x36372999e429e40000;\n"],
|
||||
["hex-from-exponent-boundary", e(num(1.0000473745167254e22)), "10000473745167254e6;\n"],
|
||||
["exponent-fold", e(num(1.2e101)), "12e100;\n"], // the point folds into the exponent
|
||||
["max-value", e(num(1.7976931348623157e308)), "17976931348623157e292;\n"],
|
||||
["min-value", e(num(5e-324)), "5e-324;\n"],
|
||||
["small-fold", e(num(1.5e-7)), "15e-8;\n"],
|
||||
["small-no-fold", e(num(1.5e-9)), "1.5e-9;\n"], // folding would lengthen the exponent
|
||||
["decimal-thousandth", e(num(0.001)), ".001;\n"], // leading zeros too few to pay for the `e-`
|
||||
["decimal-ten-thousandth", e(num(0.0001)), "1e-4;\n"],
|
||||
]);
|
||||
});
|
||||
|
||||
// A negative bigint is parenthesized where the position binds tighter than a prefix operator, the
|
||||
// same rule `printNumericLiteral` follows. `-1n` parses as a unary minus around a positive literal,
|
||||
// so a `BigIntLiteral` whose text starts with `-` never comes from a parser and this whole branch
|
||||
// is unreachable from any fixture.
|
||||
describe("bigints", () => {
|
||||
checkCases([
|
||||
["positive", e(big("1")), "1n;\n"],
|
||||
["negative", e(big("-1")), "-1n;\n"],
|
||||
["negative-hex", e(big("-0x10")), "-0x10n;\n"],
|
||||
["positive-member", e(member(big("1"), id("x"))), "1n.x;\n"],
|
||||
["negative-member", e(member(big("-1"), id("x"))), "(-1n).x;\n"],
|
||||
["negative-typeof", e(unary("typeof", big("-1"))), "typeof -1n;\n"],
|
||||
]);
|
||||
});
|
||||
|
||||
// A regex with no flags leaves the closing `/` as the last thing written, which is what stops
|
||||
// `/a//b/` lexing as a line comment and what forces a space before a following identifier.
|
||||
describe("regexes", () => {
|
||||
checkCases([
|
||||
["no-flags", e(regex("a", "")), "/a/;\n"],
|
||||
["flags", e(regex("a", "g")), "/a/g;\n"],
|
||||
["member-no-flags", e(call(member(regex("a", ""), id("test")), [id("x")])), "/a/.test(x);\n"],
|
||||
["member-flags", e(call(member(regex("a", "gi"), id("test")), [id("x")])), "/a/gi.test(x);\n"],
|
||||
["in-array", e(arr(regex("a", ""), regex("b", "g"))), "[/a/, /b/g];\n"],
|
||||
["sequence", e(seq(regex("a", ""), regex("b", ""))), "/a/, /b/;\n"],
|
||||
["typeof", e(unary("typeof", regex("a", ""))), "typeof /a/;\n"],
|
||||
["divide", e(bin("/", regex("a", ""), id("x"))), "/a/ / x;\n"],
|
||||
["instanceof", e(bin("instanceof", regex("a", ""), id("RegExp"))), "/a/ instanceof RegExp;\n"],
|
||||
["script-pattern", e(regex("script", "")), "/script/;\n"],
|
||||
["return-no-flags", program(ret(regex("a", ""))), "return /a/;\n"],
|
||||
]);
|
||||
});
|
||||
|
||||
// `+ +x` and `- -x` need a separating space; `+ -x` does not. This is the whole
|
||||
// `printSpaceBeforeOperator` family.
|
||||
describe("unary and update expressions", () => {
|
||||
checkCases([
|
||||
["plus-plus", e(unary("+", unary("+", id("x")))), "+ +x;\n"],
|
||||
["minus-minus", e(unary("-", unary("-", id("x")))), "- -x;\n"],
|
||||
["plus-minus", e(unary("+", unary("-", id("x")))), "+-x;\n"],
|
||||
["minus-plus", e(unary("-", unary("+", id("x")))), "-+x;\n"],
|
||||
["plus-preinc", e(unary("+", update("++", id("x")))), "+ ++x;\n"],
|
||||
["minus-predec", e(unary("-", update("--", id("x")))), "- --x;\n"],
|
||||
["plus-predec", e(unary("+", update("--", id("x")))), "+--x;\n"],
|
||||
["minus-preinc", e(unary("-", update("++", id("x")))), "-++x;\n"],
|
||||
["not-not", e(unary("!", unary("!", id("x")))), "!!x;\n"],
|
||||
["tilde-tilde", e(unary("~", unary("~", id("x")))), "~~x;\n"],
|
||||
["not-predec", e(unary("!", update("--", id("x")))), "!--x;\n"],
|
||||
["typeof", e(unary("typeof", id("x"))), "typeof x;\n"],
|
||||
["void", e(unary("void", num(0))), "void 0;\n"],
|
||||
["delete", e(unary("delete", member(id("x"), id("y")))), "delete x.y;\n"],
|
||||
["delete-infinity", e(unary("delete", num(Infinity))), "delete (0, Infinity);\n"],
|
||||
["minus-negative-literal", e(unary("-", num(-1))), "- -1;\n"],
|
||||
["plus-negative-literal", e(unary("+", num(-1))), "+-1;\n"],
|
||||
["preinc", e(update("++", id("x"))), "++x;\n"],
|
||||
["postinc", e(update("++", id("x"), false)), "x++;\n"],
|
||||
["predec", e(update("--", id("x"))), "--x;\n"],
|
||||
["postdec", e(update("--", id("x"), false)), "x--;\n"],
|
||||
["postinc-plus", e(bin("+", update("++", id("x"), false), id("y"))), "x++ + y;\n"],
|
||||
["preinc-of-member-int", e(update("++", member(id("x"), id("y")))), "++x.y;\n"],
|
||||
]);
|
||||
});
|
||||
|
||||
// The directive prologue. A parenthesised string is NOT a directive, so the parentheses have to
|
||||
// survive printing or it is read back as one - which can silently make a function strict. The
|
||||
// parser never produces these shapes ambiguously, but it does produce ASTs where the only thing
|
||||
// distinguishing the two is `.directive`, so these lock the round-trip in.
|
||||
//
|
||||
// These cases give source rather than an AST, because `.directive` - the field under test - is one
|
||||
// the parser sets, and hand-building it correctly is exactly what is in question.
|
||||
describe("directives", () => {
|
||||
test.each<[name: string, source: string, output: string]>([
|
||||
["paren-string-alone", `("moon");`, `("moon");\n`],
|
||||
["after-one", `"use strict";\n("moon");`, `"use strict";\n("moon");\n`],
|
||||
["after-two", `"use strict";\n"use asm";\n("moon");`, `"use strict";\n"use asm";\n("moon");\n`],
|
||||
["prologue-closed", `"use strict";\nfoo;\n("moon");`, `"use strict";\nfoo;\n"moon";\n`],
|
||||
["mode-change", `"use asm";\n("use strict");`, `"use asm";\n("use strict");\n`],
|
||||
[
|
||||
"mode-change-fn",
|
||||
`function f(){ "use asm"; ("use strict"); return 1; }`,
|
||||
`function f() {\n\t"use asm";\n\t("use strict");\n\treturn 1;\n}\n`,
|
||||
],
|
||||
["plain", `"use strict";\nfoo;`, `"use strict";\nfoo;\n`],
|
||||
["only", `"use strict";`, `"use strict";\n`],
|
||||
["none", `foo;\nbar;`, `foo;\nbar;\n`],
|
||||
["string-mid-body", `foo;\n"bar";`, `foo;\n"bar";\n`],
|
||||
])("%s", (_name, source, output) => {
|
||||
const { program: parsed, errors } = parseSync("fixture.js", source, PARSE_OPTIONS);
|
||||
if (errors.length > 0) throw new Error(`fixture parse failed: ${errors[0].message}`);
|
||||
expect(printSync(parsed)).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
// A keyword or identifier straight after something which ends in an identifier character needs
|
||||
// a space - the `printSpaceBeforeIdentifier` path
|
||||
describe("space before identifiers", () => {
|
||||
checkCases([
|
||||
["return-ident", program(ret(id("x"))), "return x;\n"],
|
||||
["typeof-typeof", e(unary("typeof", unary("typeof", id("x")))), "typeof typeof x;\n"],
|
||||
["void-typeof", e(unary("void", unary("typeof", id("x")))), "void typeof x;\n"],
|
||||
["string-concat", e(bin("+", str("a"), id("b"))), '"a" + b;\n'],
|
||||
]);
|
||||
});
|
||||
|
||||
// The three `export default` declaration forms - interface, class and function - each end with a
|
||||
// newline, so whatever follows starts on its own line. `export default interface` printed without
|
||||
// one until `oxc_codegen` was fixed, gluing the next statement onto the closing brace.
|
||||
//
|
||||
// No conformance fixture contains the construct, so these cases are the only thing holding it in
|
||||
// place. They give source rather than an AST, because what is under test is the separation between
|
||||
// two statements.
|
||||
describe("export default declarations", () => {
|
||||
test.each<[name: string, source: string, output: string]>([
|
||||
[
|
||||
"interface",
|
||||
`export default interface X {}\nconst y = 1;`,
|
||||
`export default interface X {}\nconst y = 1;\n`,
|
||||
],
|
||||
["interface-alone", `export default interface X {}`, `export default interface X {}\n`],
|
||||
[
|
||||
"class",
|
||||
`export default class X {}\nconst y = 1;`,
|
||||
`export default class X {}\nconst y = 1;\n`,
|
||||
],
|
||||
[
|
||||
"function",
|
||||
`export default function X() {}\nconst y = 1;`,
|
||||
`export default function X() {}\nconst y = 1;\n`,
|
||||
],
|
||||
])("%s", (_name, source, output) => {
|
||||
const { program: parsed, errors } = parseSync("fixture.ts", source, PARSE_OPTIONS);
|
||||
if (errors.length > 0) throw new Error(`fixture parse failed: ${errors[0].message}`);
|
||||
expect(printSync(parsed, { ts: true })).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
// A JSX string prints from its raw source text, and the only decision is which quote to wrap it in -
|
||||
// the text itself is never escaped, because JSX strings have no escape sequences. The parser does
|
||||
// not decode HTML entities at present, so `"` is six characters and not a `"`, and `raw` and
|
||||
// `value` therefore hold the same text.
|
||||
//
|
||||
// Each case is printed twice: once as parsed, which takes the `raw` path, and once with `raw`
|
||||
// blanked, which takes the `value` path. Both must give the same output, and that is what breaks
|
||||
// when entities start being decoded - `value` becomes the decoded text while `raw` does not, so the
|
||||
// second assertion fails. Checking only parsed output would prove nothing, since the printer would
|
||||
// go on reading the unchanged `raw`.
|
||||
//
|
||||
// The conformance suites cannot catch this either: both printers read the same parser output, so
|
||||
// they would move together and stay byte-identical. Hence pinned expected strings here.
|
||||
describe("JSX strings", () => {
|
||||
test.each<[name: string, source: string, output: string]>([
|
||||
["plain", `<Foo bar="x" />;`, `<Foo bar="x" />;\n`],
|
||||
["single-quote-in-value", `<Foo bar="'" />;`, `<Foo bar="'" />;\n`],
|
||||
["double-quote-in-value", `<Foo bar='"' />;`, `<Foo bar='"' />;\n`],
|
||||
["quote-normalized", `<Foo bar=''' />;`, `<Foo bar="'" />;\n`],
|
||||
["entity-amp", `<Foo bar="&" />;`, `<Foo bar="&" />;\n`],
|
||||
// The three which change the moment entities are decoded - each holds an apostrophe alongside
|
||||
// a `"` which becomes the double quote the chosen wrapper then cannot contain
|
||||
["entity-quot", `<Foo bar=""" />;`, `<Foo bar=""" />;\n`],
|
||||
["entity-quot-with-apostrophe", `<Foo bar="'"" />;`, `<Foo bar="'"" />;\n`],
|
||||
["entity-quot-mixed", `<Foo bar="a'b"c" />;`, `<Foo bar="a'b"c" />;\n`],
|
||||
// `JSXText` reads `raw` the same way, so it carries the same hazard
|
||||
["text-entity", `<Foo>a&b</Foo>;`, `<Foo>a&b</Foo>;\n`],
|
||||
["text-entity-quot", `<Foo>"</Foo>;`, `<Foo>"</Foo>;\n`],
|
||||
])("%s", (_name, source, output) => {
|
||||
const parse = () => {
|
||||
const { program: parsed, errors } = parseSync("fixture.jsx", source, PARSE_OPTIONS);
|
||||
if (errors.length > 0) throw new Error(`fixture parse failed: ${errors[0].message}`);
|
||||
return parsed;
|
||||
};
|
||||
|
||||
// From `raw`, as a parser supplies it
|
||||
expect(printSync(parse(), { jsx: true })).toBe(output);
|
||||
|
||||
// From `value`, which is where a change in the parser would show
|
||||
const fromValue = parse();
|
||||
dropJsxRaw(fromValue);
|
||||
expect(printSync(fromValue, { jsx: true })).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Blank `raw` on every JSX string node, so the printer prints from `value` instead.
|
||||
*
|
||||
* `printJSXAttributeValue` and the `JSXText` arm both prefer `raw` wherever there is one,
|
||||
* and a parser always supplies one, so parsed input never reaches the `value` path at all.
|
||||
* Only a hand-built AST does - and that path is the one which moves when the parser changes.
|
||||
*
|
||||
* @param node - Node, or any value which might contain one
|
||||
*/
|
||||
function dropJsxRaw(node: unknown): void {
|
||||
if (typeof node !== "object" || node === null) return;
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const child of node) {
|
||||
dropJsxRaw(child);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = node as Record<string, unknown>;
|
||||
if (fields.type === "JSXText") {
|
||||
fields.raw = undefined;
|
||||
} else if (fields.type === "JSXAttribute") {
|
||||
const value = fields.value as Record<string, unknown> | null | undefined;
|
||||
if (value != null && value.type === "Literal") value.raw = undefined;
|
||||
}
|
||||
|
||||
for (const key in fields) {
|
||||
dropJsxRaw(fields[key]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
// Source map tests.
|
||||
//
|
||||
// Source maps are the one part of the printer whose output is not the printed code, so the
|
||||
// conformance suites say nothing about them. These check the mappings a print emits are
|
||||
// self-consistent and point where they claim to, and that turning source maps on does not change
|
||||
// the code that comes out.
|
||||
//
|
||||
// `oxc-parser` emits no `loc`, so it is synthesized here from `start` / `end` offsets.
|
||||
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join as pathJoin } from "node:path";
|
||||
import { parseSync } from "oxc-parser";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
|
||||
import { printSync } from "../dist/index.js";
|
||||
|
||||
import type { Program } from "oxc-parser";
|
||||
import type { Mapping, Position, SourceMapGenerator } from "../dist/index.js";
|
||||
|
||||
// Same directory the benchmarks download their fixtures to. Whichever are already cached are used;
|
||||
// the inline fixtures below always run.
|
||||
const CACHE_DIR_PATH = pathJoin(import.meta.dirname, "../../../target");
|
||||
|
||||
// `preserveParens: false` because this printer deliberately does not support the redundant
|
||||
// `ParenthesizedExpression` / `TSParenthesizedType` nodes. `experimentalRawTransfer` is how the
|
||||
// printer is meant to be used, so the ASTs here are the shape it really sees.
|
||||
const PARSE_OPTIONS = {
|
||||
preserveParens: false,
|
||||
experimentalRawTransfer: true,
|
||||
};
|
||||
|
||||
const INLINE_JS = `
|
||||
class Foo extends Bar {
|
||||
static #x = 1;
|
||||
get y() {
|
||||
if (this.z) {
|
||||
for (const k of [1, 2, 3]) {
|
||||
console.log(\`\${k}-\${this.z}\`);
|
||||
}
|
||||
}
|
||||
return { a: 1, b: 2, c: this.#x };
|
||||
}
|
||||
}
|
||||
function outer(a, b = 2, ...rest) {
|
||||
label: while (a) {
|
||||
try {
|
||||
a = b;
|
||||
} catch (e) {
|
||||
break label;
|
||||
} finally {
|
||||
b++;
|
||||
}
|
||||
}
|
||||
return outer;
|
||||
}
|
||||
export default outer;
|
||||
`;
|
||||
|
||||
const INLINE_TS = `
|
||||
namespace NS {
|
||||
export interface Shape<T extends object = {}> {
|
||||
readonly kind: "a" | "b";
|
||||
fn?(x: T): asserts x is T;
|
||||
}
|
||||
export abstract class Impl<T> implements Shape<object> {
|
||||
declare readonly kind: "a";
|
||||
constructor(private readonly x: number, protected y?: string) { super(); }
|
||||
abstract go<U>(v: U): U;
|
||||
}
|
||||
export enum E { A = 1, B }
|
||||
export type Mapped<T> = { [K in keyof T]-?: T[K] };
|
||||
}
|
||||
export default NS;
|
||||
`;
|
||||
|
||||
interface Fixture {
|
||||
name: string;
|
||||
/** Source text, or `null` if the fixture is a cached file which has not been downloaded */
|
||||
code: string | null;
|
||||
ts: boolean;
|
||||
jsx: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a fixture the benchmarks download, or `null` if it is not in the cache.
|
||||
*
|
||||
* @param name - Fixture filename
|
||||
* @returns Source text, or `null` if not cached
|
||||
*/
|
||||
function cached(name: string): string | null {
|
||||
const path = pathJoin(CACHE_DIR_PATH, name);
|
||||
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
||||
}
|
||||
|
||||
const FIXTURES: Fixture[] = [
|
||||
{ name: "inline.js", code: INLINE_JS, ts: false, jsx: false },
|
||||
{ name: "inline.ts", code: INLINE_TS, ts: true, jsx: false },
|
||||
{ name: "react.development.js", code: cached("react.development.js"), ts: false, jsx: false },
|
||||
{
|
||||
name: "RadixUIAdoptionSection.jsx",
|
||||
code: cached("RadixUIAdoptionSection.jsx"),
|
||||
ts: false,
|
||||
jsx: true,
|
||||
},
|
||||
{ name: "binder.ts", code: cached("binder.ts"), ts: true, jsx: false },
|
||||
{ name: "kitchen-sink.tsx", code: cached("kitchen-sink.tsx"), ts: true, jsx: true },
|
||||
];
|
||||
|
||||
// --- Helpers --------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a function converting a source offset to a line and column.
|
||||
*
|
||||
* @param source - Source text
|
||||
* @returns Function taking an offset and returning a 1-based line and 0-based column
|
||||
*/
|
||||
function lineColTable(source: string): (offset: number) => Position {
|
||||
const lineStarts = [0];
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (source.charCodeAt(i) === 10) lineStarts.push(i + 1);
|
||||
}
|
||||
return (offset) => {
|
||||
let lo = 0,
|
||||
hi = lineStarts.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi + 1) >> 1;
|
||||
if (lineStarts[mid] <= offset) lo = mid;
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return { line: lo + 1, column: offset - lineStarts[lo] };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a `loc` to every node in the AST, computed from its `start` / `end` offsets.
|
||||
*
|
||||
* @param root - AST to walk
|
||||
* @param posOf - Function converting an offset to a line and column
|
||||
*/
|
||||
function addLocs(root: object, posOf: (offset: number) => Position): void {
|
||||
const seen = new Set<object>();
|
||||
const stack: unknown[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
if (node === null || typeof node !== "object") continue;
|
||||
if (seen.has(node)) continue;
|
||||
seen.add(node);
|
||||
if (Array.isArray(node)) {
|
||||
for (const child of node) stack.push(child);
|
||||
continue;
|
||||
}
|
||||
const record = node as Record<string, unknown>;
|
||||
if (typeof record.type === "string" && typeof record.start === "number") {
|
||||
record.loc = { start: posOf(record.start), end: posOf(record.end as number) };
|
||||
}
|
||||
for (const key in record) {
|
||||
if (key === "loc" || key === "parent") continue;
|
||||
const child = record[key];
|
||||
if (child !== null && typeof child === "object") stack.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A mapping, copied out of the `Mapping` object the printer reuses across calls. */
|
||||
interface Recorded {
|
||||
generatedLine: number;
|
||||
generatedColumn: number;
|
||||
originalLine: number;
|
||||
originalColumn: number;
|
||||
name: string | undefined;
|
||||
source: string;
|
||||
}
|
||||
|
||||
/** Collects the mappings a print emits. This is the `sourceMap` option's whole interface. */
|
||||
class Collector implements SourceMapGenerator {
|
||||
file: string;
|
||||
mappings: Recorded[] = [];
|
||||
|
||||
constructor(file: string) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
addMapping(mapping: Mapping): void {
|
||||
// `Mapping` objects are reused across calls - must copy
|
||||
this.mappings.push({
|
||||
generatedLine: mapping.generated.line,
|
||||
generatedColumn: mapping.generated.column,
|
||||
originalLine: mapping.original.line,
|
||||
originalColumn: mapping.original.column,
|
||||
name: mapping.name,
|
||||
source: mapping.source,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a fixture and add a `loc` to every node.
|
||||
*
|
||||
* @param name - Fixture filename
|
||||
* @param code - Source text
|
||||
* @returns The AST
|
||||
*/
|
||||
function parseWithLocs(name: string, code: string): Program {
|
||||
const { program, errors } = parseSync(name, code, PARSE_OPTIONS);
|
||||
if (errors.length > 0) throw new Error(`parse ${name}: ${errors[0].message}`);
|
||||
addLocs(program, lineColTable(code));
|
||||
return program;
|
||||
}
|
||||
|
||||
const IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/;
|
||||
|
||||
// --- Per-fixture tests ----------------------------------------------------------------------
|
||||
|
||||
interface Printed {
|
||||
withMaps: string;
|
||||
withoutMaps: string;
|
||||
mappings: Recorded[];
|
||||
outLines: string[];
|
||||
srcLines: string[];
|
||||
}
|
||||
|
||||
for (const fixture of FIXTURES) {
|
||||
// A cached fixture which has not been downloaded is reported as skipped rather than passing
|
||||
// quietly. `pnpm run bench` downloads them.
|
||||
const describeFixture = fixture.code === null ? describe.skip : describe;
|
||||
|
||||
describeFixture(fixture.name, () => {
|
||||
let printed!: Printed;
|
||||
|
||||
beforeAll(() => {
|
||||
const code = fixture.code as string;
|
||||
const program = parseWithLocs(fixture.name, code);
|
||||
const collector = new Collector(fixture.name);
|
||||
const { jsx, ts } = fixture;
|
||||
|
||||
// Both builds through the public API - `printSync` picks the maps build when given a
|
||||
// `sourceMap`, and the no-maps build when not
|
||||
const withMaps = printSync(program, { jsx, ts, sourceMap: collector });
|
||||
const withoutMaps = printSync(program, { jsx, ts });
|
||||
|
||||
printed = {
|
||||
withMaps,
|
||||
withoutMaps,
|
||||
mappings: collector.mappings,
|
||||
outLines: withMaps.split("\n"),
|
||||
srcLines: code.split("\n"),
|
||||
};
|
||||
});
|
||||
|
||||
test("source maps do not change the printed code", () => {
|
||||
expect(printed.withMaps).toBe(printed.withoutMaps);
|
||||
});
|
||||
|
||||
test("emits mappings", () => {
|
||||
expect(printed.mappings.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("generated positions never go backwards", () => {
|
||||
let prevLine = 0,
|
||||
prevCol = 0;
|
||||
let outOfOrder = null;
|
||||
for (const mapping of printed.mappings) {
|
||||
const { generatedLine: line, generatedColumn: col } = mapping;
|
||||
if (line < prevLine || (line === prevLine && col < prevCol)) {
|
||||
outOfOrder = `${line}:${col} follows ${prevLine}:${prevCol}`;
|
||||
break;
|
||||
}
|
||||
prevLine = line;
|
||||
prevCol = col;
|
||||
}
|
||||
expect(outOfOrder).toBeNull();
|
||||
});
|
||||
|
||||
test("generated positions are within the printed output", () => {
|
||||
let outOfRange = null;
|
||||
for (const { generatedLine, generatedColumn } of printed.mappings) {
|
||||
const line = printed.outLines[generatedLine - 1];
|
||||
if (line === undefined || generatedColumn > line.length) {
|
||||
outOfRange = `${generatedLine}:${generatedColumn}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(outOfRange).toBeNull();
|
||||
});
|
||||
|
||||
test("original positions are within the source", () => {
|
||||
let outOfRange = null;
|
||||
for (const { originalLine, originalColumn } of printed.mappings) {
|
||||
const line = printed.srcLines[originalLine - 1];
|
||||
if (line === undefined || originalColumn > line.length) {
|
||||
outOfRange = `${originalLine}:${originalColumn}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(outOfRange).toBeNull();
|
||||
});
|
||||
|
||||
test("every mapping names the source file", () => {
|
||||
const wrong = printed.mappings.find(
|
||||
(mapping) => mapping.source !== fixture.name && mapping.source !== undefined,
|
||||
);
|
||||
expect(wrong).toBeUndefined();
|
||||
});
|
||||
|
||||
// Where a mapping lands on an identifier in the output, the source position it points at
|
||||
// should hold the same identifier. A handful disagree legitimately - a printed name can come
|
||||
// from a node whose span starts at a keyword - so this is a ratio rather than an absolute.
|
||||
test("identifier text agrees at mapped positions", () => {
|
||||
let checked = 0,
|
||||
mismatched = 0;
|
||||
for (const mapping of printed.mappings) {
|
||||
const outLine = printed.outLines[mapping.generatedLine - 1];
|
||||
const srcLine = printed.srcLines[mapping.originalLine - 1];
|
||||
if (outLine === undefined || srcLine === undefined) continue;
|
||||
const generatedIdent = IDENT_RE.exec(outLine.slice(mapping.generatedColumn));
|
||||
const originalIdent = IDENT_RE.exec(srcLine.slice(mapping.originalColumn));
|
||||
if (generatedIdent !== null && originalIdent !== null) {
|
||||
checked++;
|
||||
if (generatedIdent[0] !== originalIdent[0]) mismatched++;
|
||||
}
|
||||
}
|
||||
// Guard against the ratio passing because nothing was compared
|
||||
expect(checked).toBeGreaterThan(0);
|
||||
expect(1 - mismatched / checked).toBeGreaterThan(0.97);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Indentation ----------------------------------------------------------------------------
|
||||
|
||||
// `printIndent` goes through the same write path as everything else, so the `indent` option and
|
||||
// the mappings have to agree: indentation is honoured (or falls back to a tab), and no mapping
|
||||
// ever points into the middle of an indent run.
|
||||
//
|
||||
// The invalid values are typed `unknown` because they are what an untyped JS caller can pass -
|
||||
// that they fall back to a tab rather than corrupting the output is the point of testing them.
|
||||
const INDENT_CASES: unknown[] = [
|
||||
// Valid - used as given
|
||||
undefined,
|
||||
"\t",
|
||||
" ",
|
||||
" ",
|
||||
"\t\t",
|
||||
" \t",
|
||||
"\t ",
|
||||
// Invalid - must fall back to a tab
|
||||
"",
|
||||
"x",
|
||||
"\n",
|
||||
"\r\n",
|
||||
" x ",
|
||||
" ",
|
||||
4,
|
||||
null,
|
||||
{},
|
||||
];
|
||||
|
||||
describe("indent option", () => {
|
||||
test.each(
|
||||
INDENT_CASES.map((indent) => ({ label: JSON.stringify(indent) ?? "undefined", indent })),
|
||||
)("indent=$label", ({ indent }) => {
|
||||
const program = parseWithLocs("indent.js", INLINE_JS);
|
||||
const collector = new Collector("indent.js");
|
||||
const out = printSync(program, {
|
||||
indent: indent as string | undefined,
|
||||
sourceMap: collector,
|
||||
});
|
||||
|
||||
const expectedIndent = typeof indent === "string" && /^[ \t]+$/.test(indent) ? indent : "\t";
|
||||
const lines = out.split("\n");
|
||||
const indented = lines.filter((line) => line.startsWith("\t") || line.startsWith(" "));
|
||||
expect(indented.length).toBeGreaterThan(0);
|
||||
|
||||
// Every indented line's leading whitespace is a whole number of indent levels
|
||||
for (const line of indented) {
|
||||
const lead = /^[\t ]+/.exec(line)![0];
|
||||
expect(lead.length % expectedIndent.length).toBe(0);
|
||||
expect(lead).toBe(expectedIndent.repeat(lead.length / expectedIndent.length));
|
||||
}
|
||||
|
||||
// No mapping points into an indent run
|
||||
let insideIndent = null;
|
||||
for (const mapping of collector.mappings) {
|
||||
const lead = /^[\t ]*/.exec(lines[mapping.generatedLine - 1])![0].length;
|
||||
if (mapping.generatedColumn !== 0 && mapping.generatedColumn < lead) {
|
||||
insideIndent = `${mapping.generatedLine}:${mapping.generatedColumn}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(insideIndent).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
// Test262 conformance.
|
||||
//
|
||||
// Tests this package prints every Test262 fixture exactly as Rust `oxc_codegen` prints it.
|
||||
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { join as pathJoin } from "node:path";
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import { checkFixture, TEST262_DIR_PATH, test262SourceType } from "./utils/common.ts";
|
||||
|
||||
// Test262 keeps its fixtures' shared helpers in `harness`, which are not test cases
|
||||
const fixturePaths = (await readdir(TEST262_DIR_PATH, { recursive: true }))
|
||||
.filter((path) => path.endsWith(".js") && !path.startsWith("harness/"))
|
||||
.sort();
|
||||
|
||||
describe.concurrent("test262", () => {
|
||||
for (const path of fixturePaths) {
|
||||
// oxlint-disable-next-line vitest/valid-title, vitest/expect-expect
|
||||
it(path, async (ctx) => {
|
||||
const sourceText = await readFile(pathJoin(TEST262_DIR_PATH, path), "utf8");
|
||||
if (!checkFixture(path, sourceText, "js", test262SourceType(sourceText))) ctx.skip();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
// TypeScript conformance.
|
||||
//
|
||||
// Tests this package prints every TypeScript fixture exactly as Rust `oxc_codegen` prints it.
|
||||
//
|
||||
// A TypeScript test file can hold several units, separated by `// @filename:` directives,
|
||||
// each with its own language and module setting. `makeUnitsFromTest` splits them out,
|
||||
// and every unit is checked.
|
||||
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { join as pathJoin } from "node:path";
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import { checkFixture, langOf, TS_CASES_DIR_PATH } from "./utils/common.ts";
|
||||
import { makeUnitsFromTest } from "./utils/typescript-make-units-from-test.ts";
|
||||
|
||||
// The two directories which hold the test cases.
|
||||
// The rest of `tests/cases` is editor scenarios and multi-file projects, which are not
|
||||
// standalone sources. This is the same pair `tasks/coverage` takes its TypeScript suite from.
|
||||
const CASE_DIR_NAMES = ["compiler", "conformance"];
|
||||
|
||||
// Every extension `makeUnitsFromTest` knows how to give a source type to
|
||||
const EXTENSION_REGEX = /\.([cm]?[jt]sx?)$/;
|
||||
|
||||
const fixturePaths = (
|
||||
await Promise.all(
|
||||
CASE_DIR_NAMES.map(async (dirName) => {
|
||||
const paths = await readdir(pathJoin(TS_CASES_DIR_PATH, dirName), { recursive: true });
|
||||
return paths.filter((path) => EXTENSION_REGEX.test(path)).map((path) => `${dirName}/${path}`);
|
||||
}),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.sort();
|
||||
|
||||
// `oxc-parser` deserializes the AST with recursive JS functions, and these fixtures nest binary
|
||||
// expressions deeply enough to overflow the stack.
|
||||
// They sit right on the limit, so they fail intermittently rather than every time.
|
||||
// That is a limit of the parser rather than of the printer.
|
||||
//
|
||||
// `napi/parser`'s tests never meet them. Their TypeScript fixture list comes from the `estree-conformance` submodule,
|
||||
// which holds no recording for either file.
|
||||
const SKIPPED_PATHS = new Set([
|
||||
"compiler/binderBinaryExpressionStress.ts",
|
||||
"compiler/binderBinaryExpressionStressJs.ts",
|
||||
]);
|
||||
|
||||
describe.concurrent("TypeScript", () => {
|
||||
for (const path of fixturePaths) {
|
||||
const itFixture = SKIPPED_PATHS.has(path) ? it.skip : it;
|
||||
|
||||
itFixture(path, async (ctx) => {
|
||||
let sourceText = await readFile(pathJoin(TS_CASES_DIR_PATH, path), "utf8");
|
||||
// Trim off UTF-8 BOM
|
||||
if (sourceText.charCodeAt(0) === 0xfeff) sourceText = sourceText.slice(1);
|
||||
|
||||
const { tests } = makeUnitsFromTest(path, sourceText);
|
||||
|
||||
let checked = 0;
|
||||
for (const { name, content, sourceType } of tests) {
|
||||
const lang = langOf(sourceType.typescript, sourceType.jsx);
|
||||
|
||||
// Always a TS-shaped AST, even for the `.js` units. The TypeScript suite has fixtures
|
||||
// which put TS syntax in a `.js` file on purpose, and Oxc's Rust parser keeps and prints it,
|
||||
// so the JS side has to be able to see it too.
|
||||
const checkedUnit = checkFixture(
|
||||
name,
|
||||
content,
|
||||
lang,
|
||||
sourceType.module ? "module" : "unambiguous",
|
||||
"ts",
|
||||
);
|
||||
if (checkedUnit) checked++;
|
||||
}
|
||||
|
||||
// Every unit failed to parse, so the fixture contributed nothing to check
|
||||
if (checked === 0) ctx.skip();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
// Shared machinery for the conformance tests.
|
||||
//
|
||||
// Each test takes one fixture's source text and prints it twice:
|
||||
//
|
||||
// 1. In Rust, with `oxc_parser` + `oxc_codegen`, via the `oxc_codegen_conformance` Node addon.
|
||||
// 2. In JS, by parsing with `oxc-parser` (the copy in this repo) and printing with this package.
|
||||
//
|
||||
// The two must agree byte for byte. Both sides are given the same source text and the same `lang`
|
||||
// and `sourceType`, and the addon derives its `SourceType` with `oxc_napi::get_source_type` -
|
||||
// the same function `oxc-parser` uses. So the two printers are handed the same AST and the only
|
||||
// thing under test is the printing.
|
||||
//
|
||||
// Fixtures which do not parse cleanly have no AST to print, so the addon returns `null` for them
|
||||
// and the test is reported as skipped rather than passing quietly.
|
||||
|
||||
import { join as pathJoin } from "node:path";
|
||||
import { codegen as rustPrint } from "oxc-codegen-conformance";
|
||||
import { parseSync } from "oxc-parser";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { printSync } from "../../dist/index.js";
|
||||
|
||||
export const ROOT_DIR_PATH = pathJoin(import.meta.dirname, "../../../..");
|
||||
|
||||
// The fixture suites, all of them git submodules of this repo. `just submodules` clones them.
|
||||
export const TEST262_DIR_PATH = pathJoin(ROOT_DIR_PATH, "tasks/coverage/test262/test");
|
||||
export const TS_CASES_DIR_PATH = pathJoin(ROOT_DIR_PATH, "tasks/coverage/typescript/tests/cases");
|
||||
export const JSX_DIR_PATH = pathJoin(
|
||||
ROOT_DIR_PATH,
|
||||
"tasks/coverage/estree-conformance/tests/acorn-jsx/pass",
|
||||
);
|
||||
|
||||
// How the source text is to be parsed. The same values go to both printers.
|
||||
export type Lang = "js" | "jsx" | "ts" | "tsx";
|
||||
export type SourceTypeOption = "script" | "module" | "unambiguous";
|
||||
|
||||
// --- The check --------------------------------------------------------------------------------
|
||||
|
||||
// Both parse modes are checked for every fixture.
|
||||
//
|
||||
// `true` is `oxc-parser`'s default, and puts `ParenthesizedExpression` / `TSParenthesizedType`
|
||||
// wrapper nodes in the AST. `false` drops them, leaving the printer to re-derive every
|
||||
// parenthesis from precedence. The two are different code paths through the printer,
|
||||
// so both are worth checking.
|
||||
const PRESERVE_PARENS_MODES = [false, true];
|
||||
|
||||
/**
|
||||
* Check this package prints a fixture exactly as Rust `oxc_codegen` does.
|
||||
*
|
||||
* Checked in both parse modes - see `PRESERVE_PARENS_MODES`.
|
||||
*
|
||||
* Throws if the two disagree.
|
||||
*
|
||||
* Returns `false` without checking anything if the fixture does not parse cleanly,
|
||||
* so the caller can report the test as skipped.
|
||||
*
|
||||
* @param filename - Fixture filename, which is what decides the language when `lang` is absent
|
||||
* @param sourceText - Fixture source text
|
||||
* @param lang - Language to parse as
|
||||
* @param sourceType - Script/module/unambiguous
|
||||
* @param astType - Which properties the AST carries, defaulting to whatever `lang` implies.
|
||||
* A JS-shaped AST leaves out the TypeScript-only properties - `optional`, `accessibility`,
|
||||
* `override`, `declare` and the rest - which Oxc's Rust AST holds for a `.js` file all the same,
|
||||
* and prints. Fixtures which can contain that syntax must ask for a TS-shaped AST, or the JS
|
||||
* side is being asked to reproduce something it was never told.
|
||||
* @returns `true` if the fixture was checked, `false` if it does not parse
|
||||
*/
|
||||
export function checkFixture(
|
||||
filename: string,
|
||||
sourceText: string,
|
||||
lang: Lang,
|
||||
sourceType: SourceTypeOption,
|
||||
astType?: "js" | "ts",
|
||||
): boolean {
|
||||
const ts = astType === undefined ? lang[0] === "t" : astType === "ts";
|
||||
const jsx = lang.endsWith("x");
|
||||
|
||||
let checked = false;
|
||||
for (const preserveParens of PRESERVE_PARENS_MODES) {
|
||||
// Rust first.
|
||||
// `null` means the fixture does not parse cleanly, so nothing to compare.
|
||||
const expected = rustPrint(filename, sourceText, { lang, sourceType, preserveParens });
|
||||
if (expected === null) continue;
|
||||
|
||||
const { program, errors } = parseSync(filename, sourceText, {
|
||||
preserveParens,
|
||||
lang,
|
||||
sourceType,
|
||||
astType,
|
||||
// @ts-expect-error `experimentalRawTransfer` is experimental, so is not in `ParserOptions`
|
||||
experimentalRawTransfer: true,
|
||||
});
|
||||
|
||||
// Rust parsed it cleanly, so this should not happen - if it does, it is a parser difference
|
||||
// rather than a printer one, and saying so is more use than a diff of printed output.
|
||||
expect(errors, "Rust parsed this fixture cleanly but `oxc-parser` did not").toEqual([]);
|
||||
|
||||
const actual = printSync(program, { ts, jsx });
|
||||
expect(actual, `preserveParens: ${preserveParens}`).toBe(expected);
|
||||
checked = true;
|
||||
}
|
||||
|
||||
return checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `lang` to parse a unit as, from what it is.
|
||||
*
|
||||
* @param ts - `true` if the unit is TypeScript
|
||||
* @param jsx - `true` if JSX syntax is enabled
|
||||
* @returns The `lang` to give both printers
|
||||
*/
|
||||
export function langOf(ts: boolean, jsx: boolean): Lang {
|
||||
if (ts) return jsx ? "tsx" : "ts";
|
||||
return jsx ? "jsx" : "js";
|
||||
}
|
||||
|
||||
// The `flags` value in a Test262 frontmatter block, as either a flow sequence (`[a, b]`)
|
||||
// or the indented lines of a block sequence.
|
||||
const FLAGS_REGEX = /^flags:[ \t]*(\[[^\]]*\]|(?:\r?\n[ \t]+-[^\r\n]*)+)/m;
|
||||
|
||||
/**
|
||||
* Read the `sourceType` a Test262 fixture asks for.
|
||||
*
|
||||
* Test262 files carry a YAML frontmatter block, in which a `module` flag marks the ones
|
||||
* which must be parsed as modules. Everything else is a script.
|
||||
*
|
||||
* The flags are written either as a YAML flow sequence, `flags: [module]`,
|
||||
* or as a block sequence on the lines below `flags:`. Both forms are in use.
|
||||
*
|
||||
* The `_FIXTURE.js` files are the exception. They are module dependencies of other tests
|
||||
* rather than tests themselves, so they carry no frontmatter at all,
|
||||
* and plenty of them use `import` or top-level `await`.
|
||||
*
|
||||
* The comparison stays valid whatever this returns, since both printers are given the same value -
|
||||
* getting it right only decides how many fixtures parse, not whether the check is sound.
|
||||
*
|
||||
* @param sourceText - Fixture source text
|
||||
* @returns The source type to parse it as
|
||||
*/
|
||||
export function test262SourceType(sourceText: string): SourceTypeOption {
|
||||
const frontmatterEnd = sourceText.indexOf("---*/");
|
||||
if (frontmatterEnd === -1) return "unambiguous";
|
||||
|
||||
const flags = FLAGS_REGEX.exec(sourceText.slice(0, frontmatterEnd));
|
||||
if (flags === null) return "script";
|
||||
|
||||
return /\bmodule\b/.test(flags[1]) ? "module" : "script";
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// TypeScript test file parser.
|
||||
//
|
||||
// Port of Rust implementation from `oxc/tasks/coverage/src/typescript/meta.rs`.
|
||||
// Generated by GitHub Copilot initially and amended since.
|
||||
//
|
||||
// Only the part which splits a test file into units is ported. The Rust version also reads
|
||||
// TypeScript's `*.errors.txt` baselines, to find the fixtures TypeScript itself expects not to compile,
|
||||
// and skips them. Nothing here needs that - these tests compare two implementations on the same source,
|
||||
// and whether TypeScript would reject that source has no bearing on whether the two agree about it.
|
||||
//
|
||||
// This file is duplicated in `oxc-project/estree-conformance` repo in `src/utils` directory,
|
||||
// and in `napi/parser/test` in this repo. Any changes made here should also be replicated there.
|
||||
|
||||
import path from "path";
|
||||
|
||||
// Regex patterns equivalent to the Rust version
|
||||
const META_OPTIONS_REGEX = /^\/\/\s*@(\w+)\s*:\s*([^\r\n]*)/gm;
|
||||
|
||||
interface SourceType {
|
||||
typescript: boolean;
|
||||
jsx: boolean;
|
||||
module: boolean;
|
||||
}
|
||||
|
||||
interface TestUnitData {
|
||||
name: string;
|
||||
content: string;
|
||||
sourceType: SourceType;
|
||||
}
|
||||
|
||||
interface TestCaseContent {
|
||||
tests: TestUnitData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the test file sets a `@jsx` compiler option, which forces JSX on for every unit in it.
|
||||
*
|
||||
* The only compiler option any of this needs. Values are already trimmed when they go into the map,
|
||||
* so an empty string means the directive was there with nothing after it.
|
||||
*
|
||||
* @param options - Compiler options
|
||||
*/
|
||||
function hasJsxOption(options: Map<string, string>): boolean {
|
||||
const jsx = options.get("jsx");
|
||||
return jsx !== undefined && jsx !== "";
|
||||
}
|
||||
|
||||
// Mapping from file extension to source type
|
||||
const EXTENSIONS = {
|
||||
".js": { typescript: false, jsx: false, module: true },
|
||||
".mjs": { typescript: false, jsx: false, module: true },
|
||||
".cjs": { typescript: false, jsx: false, module: false },
|
||||
".jsx": { typescript: false, jsx: true, module: true },
|
||||
".ts": { typescript: true, jsx: false, module: true },
|
||||
".mts": { typescript: true, jsx: false, module: true },
|
||||
".cts": { typescript: true, jsx: false, module: false },
|
||||
".tsx": { typescript: true, jsx: true, module: true },
|
||||
};
|
||||
|
||||
function isSupportedExtension(ext: string): ext is keyof typeof EXTENSIONS {
|
||||
return Object.hasOwn(EXTENSIONS, ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if extension explicitly indicates module type
|
||||
* @param ext - File extension (lowercase, with dot)
|
||||
*/
|
||||
function isExplicitModuleExtension(ext: string): boolean {
|
||||
return [".mts", ".mjs", ".cjs", ".cts"].includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source type from file path
|
||||
* @param filePath - Path to the file
|
||||
* @param jsxOption - `true` if the test file sets a `@jsx` compiler option
|
||||
* @returns Source type, or `null` for an unsupported extension
|
||||
*/
|
||||
function getSourceType(filePath: string, jsxOption: boolean): SourceType | null {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (!isSupportedExtension(ext)) return null;
|
||||
|
||||
// For explicit module extensions (.mts, .mjs, .cjs, .cts), preserve the extension-based module value.
|
||||
// For ambiguous extensions (.js, .jsx, .ts, .tsx), use content-based detection (start with false).
|
||||
const sourceType: SourceType = {
|
||||
...EXTENSIONS[ext],
|
||||
module: isExplicitModuleExtension(ext) ? EXTENSIONS[ext].module : false,
|
||||
};
|
||||
if (jsxOption) sourceType.jsx = true;
|
||||
|
||||
return sourceType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract individual test units from a TypeScript test file
|
||||
* @param filePath - Path to the test file
|
||||
* @param code - Content of the test file
|
||||
* @returns Parsed test case content
|
||||
*/
|
||||
export function makeUnitsFromTest(filePath: string, code: string): TestCaseContent {
|
||||
const currentFileOptions = new Map<string, string>();
|
||||
let currentFileName: string | null = null;
|
||||
const testUnitData: Omit<TestUnitData, "sourceType">[] = [];
|
||||
let currentFileContent = "";
|
||||
|
||||
// Process the file line by line
|
||||
const lines = code.split(/\r?\n/);
|
||||
if (lines[lines.length - 1] === "") lines.length--;
|
||||
|
||||
for (const line of lines) {
|
||||
// Reset regex state
|
||||
META_OPTIONS_REGEX.lastIndex = 0;
|
||||
const match = META_OPTIONS_REGEX.exec(line);
|
||||
|
||||
if (match) {
|
||||
const metaName = match[1].toLowerCase();
|
||||
const metaValue = match[2].trim();
|
||||
|
||||
if (metaName === "filename") {
|
||||
if (currentFileName !== null) {
|
||||
testUnitData.push({
|
||||
name: currentFileName,
|
||||
content: currentFileContent,
|
||||
});
|
||||
currentFileContent = "";
|
||||
}
|
||||
currentFileName = metaValue;
|
||||
} else {
|
||||
currentFileOptions.set(metaName, metaValue);
|
||||
}
|
||||
} else {
|
||||
if (currentFileContent.length > 0) {
|
||||
currentFileContent += "\n";
|
||||
}
|
||||
currentFileContent += line;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the last file (or the only file if no @filename is used)
|
||||
const fileName = currentFileName || path.basename(filePath);
|
||||
testUnitData.push({
|
||||
name: fileName,
|
||||
content: currentFileContent,
|
||||
});
|
||||
|
||||
const jsxOption = hasJsxOption(currentFileOptions);
|
||||
|
||||
// Update source types and filter out unsupported files
|
||||
const isModule = testUnitData.length > 1;
|
||||
const validTestUnits: TestUnitData[] = [];
|
||||
for (const unit of testUnitData) {
|
||||
const sourceType = getSourceType(unit.name, jsxOption);
|
||||
if (!sourceType) continue;
|
||||
// Only upgrade to module for ambiguous extensions (.ts, .js, .tsx, .jsx).
|
||||
// Explicit extensions (.mts, .mjs, .cjs, .cts) should keep their extension-based module kind.
|
||||
const ext = path.extname(unit.name).toLowerCase();
|
||||
if (isModule && !isExplicitModuleExtension(ext)) sourceType.module = true;
|
||||
validTestUnits.push({ ...unit, sourceType });
|
||||
}
|
||||
|
||||
return { tests: validTestUnits };
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"target": "ESNext",
|
||||
"allowImportingTsExtensions": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Build configuration.
|
||||
//
|
||||
// 5 builds out of one source tree - the entry point, and the printer over the 2 build-time feature flags.
|
||||
// The plugin order matters: `strip_ts` is a text transform, so it has to run before the plugins which parse.
|
||||
|
||||
import { defineConfig } from "tsdown";
|
||||
import removeAssertsPlugin from "./tsdown_plugins/remove_asserts.ts";
|
||||
import stripTsPlugin from "./tsdown_plugins/strip_ts.ts";
|
||||
import unmapWritesPlugin from "./tsdown_plugins/unmap_writes.ts";
|
||||
import constFunctionsPlugin from "./tsdown_plugins/const_functions.ts";
|
||||
|
||||
const isEnabled = (env: string | undefined) => env === "true" || env === "1";
|
||||
|
||||
// When run with `pnpm run build-dev`, generate a debug build with extra assertions.
|
||||
// This is the build to run the conformance suites against - see `conformance/run.mjs`.
|
||||
// It replaces the release build in `dist`, so rebuild with `pnpm run build` before benchmarking.
|
||||
const DEBUG = isEnabled(process.env.DEBUG);
|
||||
|
||||
// Only remove assertions in release build. Debug builds keep `debugAssert` calls live.
|
||||
const assertPlugins = DEBUG ? [] : [removeAssertsPlugin];
|
||||
|
||||
// Global constants defined at build time. See `src-js/globals.d.ts`.
|
||||
// `DEBUG: false` lets the minifier remove the body of `debugAssert` and any other debug-only code.
|
||||
const definedGlobals = { DEBUG: DEBUG ? "true" : "false" };
|
||||
|
||||
// Base config.
|
||||
// `platform: "node"` because the entry point loads the printer with `createRequire`.
|
||||
const commonConfig = defineConfig({
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
outDir: "dist",
|
||||
format: "esm",
|
||||
unbundle: false,
|
||||
hash: false,
|
||||
fixedExtension: false,
|
||||
// `scripts/build.ts` deletes `dist` before TSDown runs.
|
||||
// This allows generating all 5 builds into the same directory.
|
||||
clean: false,
|
||||
plugins: [...assertPlugins],
|
||||
inputOptions: {
|
||||
// For plugins
|
||||
experimental: { nativeMagicString: true },
|
||||
},
|
||||
});
|
||||
|
||||
// Minification options.
|
||||
// Release builds are minified in full - that is what is published, and it is half the size.
|
||||
// Debug builds keep names and whitespace, so `dist` stays readable while working on the printer.
|
||||
const minifyConfig = DEBUG
|
||||
? {
|
||||
compress: { keepNames: { function: true, class: true } },
|
||||
mangle: false,
|
||||
codegen: { removeWhitespace: false },
|
||||
}
|
||||
: true;
|
||||
|
||||
// One printer build. The printer is built 4 times from `src-js/print/index.ts`,
|
||||
// over 2 build-time feature flags (see `src-js/globals.d.ts`):
|
||||
//
|
||||
// - `SOURCEMAPS`: Source map support costs a little speed even when unused.
|
||||
// - `TS`: TypeScript syntax support. JS-only builds lose all the TS field checks (via minifier dead-code removal)
|
||||
// and the TS switch arms + printer functions (via `strip_ts.ts`).
|
||||
//
|
||||
// `src-js/index.ts` loads whichever build the caller's options call for.
|
||||
//
|
||||
// In builds without source maps, nothing reads the `node` argument the mapping writes take,
|
||||
// so `unmap_writes` rewrites every `writeWithMap` / `writeWithMapNoLast` call, and the imports
|
||||
// which bring them in, into the plain `write` / `writeNoLast` they become without it.
|
||||
const printerConfig = (name: string, { sourcemaps, ts }: { sourcemaps: boolean; ts: boolean }) => ({
|
||||
...commonConfig,
|
||||
minify: minifyConfig,
|
||||
// Only the entry point's types are published
|
||||
dts: false,
|
||||
entry: { [name]: "src-js/print/index.ts" },
|
||||
define: {
|
||||
...definedGlobals,
|
||||
SOURCEMAPS: sourcemaps ? "true" : "false",
|
||||
TS: ts ? "true" : "false",
|
||||
},
|
||||
plugins: [
|
||||
// `strip_ts` is a text transform, so must run before the AST-based plugins
|
||||
...(ts ? [] : [stripTsPlugin()]),
|
||||
...(sourcemaps ? [] : [unmapWritesPlugin]),
|
||||
constFunctionsPlugin,
|
||||
...assertPlugins,
|
||||
],
|
||||
});
|
||||
|
||||
export default defineConfig([
|
||||
// Entry point
|
||||
{
|
||||
...commonConfig,
|
||||
entry: { index: "src-js/index.ts" },
|
||||
minify: minifyConfig,
|
||||
dts: true,
|
||||
define: definedGlobals,
|
||||
deps: {
|
||||
// The printer builds are loaded at runtime, so must not be bundled in
|
||||
neverBundle: ["./print_js.js", "./print_js_maps.js", "./print_ts.js", "./print_ts_maps.js"],
|
||||
},
|
||||
},
|
||||
|
||||
// Printers
|
||||
printerConfig("print_js", { sourcemaps: false, ts: false }),
|
||||
printerConfig("print_js_maps", { sourcemaps: true, ts: false }),
|
||||
printerConfig("print_ts", { sourcemaps: false, ts: true }),
|
||||
printerConfig("print_ts_maps", { sourcemaps: true, ts: true }),
|
||||
]);
|
||||
@@ -0,0 +1,68 @@
|
||||
import { parseSync } from "oxc-parser";
|
||||
|
||||
import type { Plugin } from "rolldown";
|
||||
|
||||
/**
|
||||
* Plugin to rewrite the printer's top-level function declarations as `const` bindings.
|
||||
*
|
||||
* ```ts
|
||||
* // Original code
|
||||
* function printIf(node: ESTree.IfStatement, state: State): void {
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* // After transform
|
||||
* const printIf = (node: ESTree.IfStatement, state: State): void => {
|
||||
* // ...
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* A function declaration creates a binding which could be assigned to later, so a call has to read
|
||||
* the current value out of the module's scope. A `const` binding cannot, so V8 can treat it
|
||||
* as a constant and call it directly. Measured on all 7 benchmark fixtures, it is worth ~3%.
|
||||
*
|
||||
* It really is the `const`, and not the arrow function or the smaller output it allows - the same rewrite
|
||||
* with `let` measures ~4% slower than this one, and slower than leaving the declarations alone.
|
||||
* Do not relax it to `let`.
|
||||
*
|
||||
* The source keeps its declarations - they hoist, they read better, and they are what the rest of
|
||||
* the codebase looks like. This is why the rewrite is a build step rather than how printer is written.
|
||||
*/
|
||||
const plugin: Plugin = {
|
||||
name: "const-functions",
|
||||
transform: {
|
||||
// Only process TS files in `src-js/print` directory
|
||||
filter: { id: /\/src-js\/print\/.+\.ts$/ },
|
||||
|
||||
handler(code, path, meta) {
|
||||
const magicString = meta.magicString!;
|
||||
const { program, errors } = parseSync(path, code);
|
||||
if (errors.length !== 0) throw new Error(`Failed to parse ${path}: ${errors[0].message}`);
|
||||
|
||||
for (const statement of program.body) {
|
||||
let fn;
|
||||
if (statement.type === "FunctionDeclaration") {
|
||||
fn = statement;
|
||||
} else if (
|
||||
statement.type === "ExportNamedDeclaration" &&
|
||||
statement.declaration?.type === "FunctionDeclaration"
|
||||
) {
|
||||
fn = statement.declaration;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fn.async || fn.generator) continue;
|
||||
|
||||
// `function name` -> `const name =`, `{` of the body -> `=> {`, and `}` -> `};`
|
||||
magicString.overwrite(fn.start, fn.id!.end, `const ${fn.id!.name} =`);
|
||||
magicString.appendLeft(fn.body!.start, "=> ");
|
||||
magicString.appendRight(fn.end, ";");
|
||||
}
|
||||
|
||||
return { code: magicString };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { join as pathJoin } from "node:path";
|
||||
import { parseSync, Visitor } from "oxc-parser";
|
||||
|
||||
import type { Plugin } from "rolldown";
|
||||
|
||||
// Path to file which defines assertion functions
|
||||
const ASSERTS_PATH = pathJoin(import.meta.dirname, "../src-js/asserts.ts");
|
||||
|
||||
/**
|
||||
* Plugin to remove imports of `typeAssertIs` from `src-js/asserts.ts`, and all its call sites.
|
||||
*
|
||||
* ```ts
|
||||
* // Original code
|
||||
* import { typeAssertIs } from "./asserts.ts";
|
||||
* typeAssertIs<Foo>(node.value);
|
||||
* print(node.value);
|
||||
*
|
||||
* // After transform
|
||||
* print(node.value);
|
||||
* ```
|
||||
*
|
||||
* The minifier can already remove the calls on its own, as the function body is empty and lands
|
||||
* in the same chunk. But it can't prove that the expressions *inside* the calls have no side effects -
|
||||
* `node.value` could be a getter - so it leaves those behind as bare expression statements.
|
||||
*
|
||||
* This plugin removes the calls entirely, expressions included, which makes `typeAssertIs`
|
||||
* cost nothing at all. Adapted from `apps/oxlint/tsdown_plugins/replace_asserts.ts`.
|
||||
*/
|
||||
const plugin: Plugin = {
|
||||
name: "remove-asserts",
|
||||
transform: {
|
||||
// Only process TS files in `src-js` directory
|
||||
filter: { id: /\/src-js\/.+(?<!\.d)\.ts$/ },
|
||||
|
||||
async handler(code, path, meta) {
|
||||
const magicString = meta.magicString!;
|
||||
const { program, errors } = parseSync(path, code);
|
||||
if (errors.length !== 0) throw new Error(`Failed to parse ${path}: ${errors[0].message}`);
|
||||
|
||||
// Gather names of assertion functions imported from `asserts.ts`.
|
||||
// Also gather all identifiers used in the `import` statements, so can avoid erroring on them
|
||||
// in visitor below.
|
||||
const assertFnNames = new Set<string>(),
|
||||
idents = new Set();
|
||||
for (const stmt of program.body) {
|
||||
if (stmt.type !== "ImportDeclaration") continue;
|
||||
|
||||
// Check if import is from `asserts.ts`.
|
||||
// `endsWith` check is just a shortcut to avoid resolving the specifier to a full path for most imports.
|
||||
const source = stmt.source.value;
|
||||
if (!source.endsWith("/asserts.ts")) continue;
|
||||
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
const importedId = await this.resolve(source, path);
|
||||
if (importedId === null || importedId.id !== ASSERTS_PATH) continue;
|
||||
|
||||
// Remove `import` statement
|
||||
for (const specifier of stmt.specifiers) {
|
||||
if (specifier.type !== "ImportSpecifier") {
|
||||
throw new Error(`Only use named imports when importing from \`asserts.ts\`: ${path}`);
|
||||
}
|
||||
idents.add(specifier.local);
|
||||
if (specifier.imported.type === "Identifier") idents.add(specifier.imported);
|
||||
assertFnNames.add(specifier.local.name);
|
||||
}
|
||||
magicString.remove(stmt.start, stmt.end);
|
||||
}
|
||||
|
||||
if (assertFnNames.size === 0) return;
|
||||
|
||||
// Visit AST and remove all calls to assertion functions
|
||||
const visitor = new Visitor({
|
||||
// Replace `typeAssertIs(...)` calls with `null`. Minifier will remove the `null`.
|
||||
CallExpression(node) {
|
||||
const { callee } = node;
|
||||
if (callee.type !== "Identifier") return;
|
||||
if (assertFnNames.has(callee.name)) {
|
||||
idents.add(callee);
|
||||
magicString.overwrite(node.start, node.end, "null");
|
||||
}
|
||||
},
|
||||
// Error if assertion functions are used in any other way. We lack logic to deal with that.
|
||||
Identifier(node) {
|
||||
const { name } = node;
|
||||
if (assertFnNames.has(name) && !idents.has(node)) {
|
||||
throw new Error(
|
||||
`Do not use \`${name}\` imported from \`asserts.ts\` except in function calls: ${path}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
visitor.visit(program);
|
||||
|
||||
return { code: magicString };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { Plugin } from "rolldown";
|
||||
|
||||
/**
|
||||
* Plugin to remove the TypeScript-only regions of `print.ts` from the JS-only builds.
|
||||
*
|
||||
* The `TS` build-time constant (see `src-js/globals.d.ts`) lets the minifier drop TS-only code
|
||||
* in expression positions - `if (TS && node.declare)` folds to nothing when `TS` is `false`.
|
||||
* But `case "TSEnumDeclaration":` switch arms are not expressions, and no minifier can remove an arm
|
||||
* on the grounds that its node type never occurs. Those arms are fenced with `IF TS` / `END_IF`
|
||||
* block-comment markers (the same convention as the raw transfer deserializer builds):
|
||||
*
|
||||
* ```ts
|
||||
* /* IF TS *\/
|
||||
* case "TSEnumDeclaration":
|
||||
* ...
|
||||
* break;
|
||||
* /* END_IF *\/
|
||||
* ```
|
||||
*
|
||||
* (`*\/` escapes above only because this is itself a block comment.)
|
||||
*
|
||||
* This plugin deletes everything from each `IF TS` marker line through its `END_IF` marker line.
|
||||
* Once the arms are gone, the TS-only printer functions they called lose their last references,
|
||||
* and tree-shaking removes them wholesale.
|
||||
*
|
||||
* It's a plain text transform (the fences are only visible in source text), so it must run before
|
||||
* the AST-based plugins in the plugin list, and only in builds where `TS` is `false`.
|
||||
*/
|
||||
const REGION = /[ \t]*\/\* IF TS \*\/\n[\s\S]*?\/\* END_IF \*\/\n/g;
|
||||
|
||||
// A factory, so each build counts its own regions rather than sharing a total with the others
|
||||
const stripTsPlugin = (): Plugin => {
|
||||
let strippedRegions = 0;
|
||||
|
||||
return {
|
||||
name: "strip-ts",
|
||||
|
||||
buildStart() {
|
||||
strippedRegions = 0;
|
||||
},
|
||||
|
||||
transform: {
|
||||
// Only process TS files in `src-js/print` directory
|
||||
filter: { id: /\/src-js\/print\/.+\.ts$/ },
|
||||
|
||||
handler(code) {
|
||||
code = code.replace(REGION, () => {
|
||||
strippedRegions++;
|
||||
return "";
|
||||
});
|
||||
|
||||
// Unbalanced or nested fences leave a marker behind.
|
||||
// Most files have no fences at all, so the "did this plugin do anything" check is per build, in `buildEnd`.
|
||||
if (code.includes("IF TS") || code.includes("END_IF")) {
|
||||
throw new Error("strip-ts: unbalanced or nested `/* IF TS */` / `/* END_IF */` fences");
|
||||
}
|
||||
|
||||
return code;
|
||||
},
|
||||
},
|
||||
|
||||
buildEnd() {
|
||||
// No fences anywhere means they have been lost and this plugin is silently doing nothing
|
||||
if (strippedRegions === 0) {
|
||||
throw new Error("strip-ts: no `/* IF TS */` / `/* END_IF */` fences found");
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default stripTsPlugin;
|
||||
@@ -0,0 +1,169 @@
|
||||
import { parseSync, Visitor } from "oxc-parser";
|
||||
|
||||
import type { Plugin } from "rolldown";
|
||||
|
||||
/**
|
||||
* Plugin to turn every mapped write back into a plain one, for builds without source maps.
|
||||
*
|
||||
* ```ts
|
||||
* // Original code
|
||||
* writeWithMap(state, "declare ", CAT_OTHER, node);
|
||||
*
|
||||
* // After transform
|
||||
* write(state, "declare ", CAT_OTHER);
|
||||
* ```
|
||||
*
|
||||
* `writeWithMap` and `writeWithMapNoLast` exist to record a source mapping for the node they are given.
|
||||
* A build without source map support has nothing to record, so the call becomes the plain write
|
||||
* it would otherwise be, and the node argument goes with it - it would still be evaluated,
|
||||
* and held live across the call, for a function which ignores it.
|
||||
*
|
||||
* The import is rewritten to match, which both keeps the plain name in scope and leaves the two
|
||||
* mapped functions unreferenced for the minifier to remove.
|
||||
*
|
||||
* Only valid where `SOURCEMAPS` is `false`, which is where the config includes it.
|
||||
*
|
||||
* The checks afterwards fail the build rather than let a mismatch through. A rewrite which dropped
|
||||
* the wrong argument, or left a call to a name nothing imports, produces a printer that is broken
|
||||
* everywhere or subtly wrong everywhere, and nothing downstream names the plugin as the cause.
|
||||
*/
|
||||
const REWRITES = {
|
||||
// `write` takes the `last` category between the code and the node, `writeNoLast` does not
|
||||
writeWithMap: { arity: 4, plain: "write" },
|
||||
writeWithMapNoLast: { arity: 3, plain: "writeNoLast" },
|
||||
} as const;
|
||||
|
||||
const WRITE_MODULE = "./write.ts";
|
||||
|
||||
type MappedName = keyof typeof REWRITES;
|
||||
|
||||
const isMapped = (name: string): name is MappedName => name in REWRITES;
|
||||
|
||||
const plugin: Plugin = {
|
||||
name: "unmap-writes",
|
||||
transform: {
|
||||
// Only process TS files in `src-js/print` directory
|
||||
filter: { id: /\/src-js\/print\/.+\.ts$/ },
|
||||
|
||||
handler(code, path, meta) {
|
||||
const magicString = meta.magicString!;
|
||||
const { program, errors } = parseSync(path, code);
|
||||
if (errors.length !== 0) throw new Error(`Failed to parse ${path}: ${errors[0].message}`);
|
||||
|
||||
// Plain names this file will need in scope once its calls are rewritten
|
||||
const needed = new Set<string>();
|
||||
|
||||
new Visitor({
|
||||
CallExpression(node) {
|
||||
const { callee } = node;
|
||||
if (callee.type !== "Identifier" || !isMapped(callee.name)) return;
|
||||
|
||||
const { arity, plain } = REWRITES[callee.name];
|
||||
const args = node.arguments;
|
||||
if (args.length !== arity) {
|
||||
throw new Error(
|
||||
`\`${callee.name}\` takes ${arity} arguments, found ${args.length}: ` +
|
||||
`${path}:${node.start}`,
|
||||
);
|
||||
}
|
||||
|
||||
magicString.overwrite(callee.start, callee.end, plain);
|
||||
// Remove `, node`, from the end of the argument before it to the end of it
|
||||
magicString.remove(args[arity - 2].end, args[arity - 1].end);
|
||||
needed.add(plain);
|
||||
},
|
||||
}).visit(program);
|
||||
|
||||
if (needed.size !== 0) {
|
||||
// Rewrite the import to drop the mapped names and carry whatever the rewrites now need.
|
||||
// `write.ts` declares them itself and imports nothing, so it never reaches this.
|
||||
// A file may import from `write.ts` more than once - the types separately from the values.
|
||||
// The one to rewrite is whichever brought the mapped names in.
|
||||
const imports = program.body.filter(
|
||||
(statement) =>
|
||||
statement.type === "ImportDeclaration" &&
|
||||
statement.source.value === WRITE_MODULE &&
|
||||
statement.specifiers.some(
|
||||
(specifier) =>
|
||||
specifier.type === "ImportSpecifier" &&
|
||||
specifier.imported.type === "Identifier" &&
|
||||
isMapped(specifier.imported.name),
|
||||
),
|
||||
);
|
||||
|
||||
if (imports.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected 1 \`${WRITE_MODULE}\` import bringing in mapped writes, found ` +
|
||||
`${imports.length}: ${path}`,
|
||||
);
|
||||
}
|
||||
|
||||
const importNode = imports[0];
|
||||
if (importNode.type !== "ImportDeclaration") throw new Error(`Unreachable: ${path}`);
|
||||
|
||||
const names = new Set(needed);
|
||||
for (const specifier of importNode.specifiers) {
|
||||
if (specifier.type !== "ImportSpecifier" || specifier.imported.type !== "Identifier") {
|
||||
throw new Error(`Unexpected \`${WRITE_MODULE}\` import specifier: ${path}`);
|
||||
}
|
||||
if (!isMapped(specifier.imported.name)) names.add(specifier.local.name);
|
||||
}
|
||||
|
||||
const sorted = [...names].sort();
|
||||
magicString.overwrite(
|
||||
importNode.start,
|
||||
importNode.end,
|
||||
`import { ${sorted.join(", ")} } from "${WRITE_MODULE}";`,
|
||||
);
|
||||
}
|
||||
|
||||
const transformed = magicString.toString();
|
||||
const { program: after, errors: afterErrors } = parseSync(path, transformed);
|
||||
if (afterErrors.length !== 0) {
|
||||
throw new Error(`Transform produced invalid code: ${path}: ${afterErrors[0].message}`);
|
||||
}
|
||||
|
||||
// Nothing may still reach a mapped write.
|
||||
// Their declarations stay, for the minifier to remove once nothing imports them.
|
||||
const bound = new Set<string>();
|
||||
const problems: string[] = [];
|
||||
|
||||
new Visitor({
|
||||
ImportSpecifier(node) {
|
||||
if (node.imported.type !== "Identifier") return;
|
||||
if (isMapped(node.imported.name)) {
|
||||
problems.push(`import of ${node.imported.name} at ${node.start}`);
|
||||
}
|
||||
bound.add(node.local.name);
|
||||
},
|
||||
FunctionDeclaration(node) {
|
||||
if (node.id !== null) bound.add(node.id.name);
|
||||
},
|
||||
}).visit(after);
|
||||
|
||||
const called = new Set<string>();
|
||||
new Visitor({
|
||||
CallExpression(node) {
|
||||
const { callee } = node;
|
||||
if (callee.type !== "Identifier") return;
|
||||
if (isMapped(callee.name)) problems.push(`call to ${callee.name} at ${node.start}`);
|
||||
if (callee.name === "write" || callee.name === "writeNoLast") called.add(callee.name);
|
||||
},
|
||||
}).visit(after);
|
||||
|
||||
// A rewritten call to a name this file does not have in scope would be a `ReferenceError`
|
||||
// on the first print, so check rather than assume the import rewrite above got it right
|
||||
for (const name of called) {
|
||||
if (!bound.has(name)) problems.push(`\`${name}\` is called but not in scope`);
|
||||
}
|
||||
|
||||
if (problems.length !== 0) {
|
||||
throw new Error(`unmap-writes left ${path} broken: ${problems.join(", ")}`);
|
||||
}
|
||||
|
||||
return { code: magicString };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
Generated
+30
@@ -402,6 +402,36 @@ importers:
|
||||
|
||||
npm/runtime: {}
|
||||
|
||||
packages/codegen:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 24.1.0
|
||||
cross-env:
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.0
|
||||
oxc-codegen-conformance:
|
||||
specifier: workspace:*
|
||||
version: link:../../tasks/codegen_conformance
|
||||
oxc-parser:
|
||||
specifier: workspace:*
|
||||
version: link:../../napi/parser
|
||||
rolldown:
|
||||
specifier: 1.2.4
|
||||
version: 1.2.4
|
||||
tsdown:
|
||||
specifier: 'catalog:'
|
||||
version: 0.22.14(@arethetypeswrong/core@0.18.5)(publint@0.3.23)(tsx@4.23.9)(typescript@6.0.3)
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.10(@types/node@24.1.0)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(happy-dom@20.0.11)(vite@7.3.0(@types/node@24.1.0)(lightningcss@1.33.0)(terser@5.44.1)(tsx@4.23.9))
|
||||
|
||||
tasks/codegen_conformance:
|
||||
devDependencies:
|
||||
'@napi-rs/cli':
|
||||
specifier: 'catalog:'
|
||||
version: 3.8.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)
|
||||
|
||||
tasks/compat_data:
|
||||
devDependencies:
|
||||
degit:
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
packages:
|
||||
- apps/*
|
||||
- napi/*
|
||||
- packages/*
|
||||
- wasm/*
|
||||
- npm/*
|
||||
- tasks/codegen_conformance
|
||||
- tasks/e2e
|
||||
- tasks/transform_conformance
|
||||
- tasks/compat_data
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.node
|
||||
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "oxc_codegen_conformance"
|
||||
version = "0.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "Node API binding around `oxc_parser` + `oxc_codegen`, used as the reference output for the `oxc-codegen` package's conformance tests."
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
oxc_allocator = { workspace = true }
|
||||
oxc_ast = { workspace = true }
|
||||
oxc_ast_visit = { workspace = true }
|
||||
oxc_codegen = { workspace = true }
|
||||
oxc_napi = { workspace = true }
|
||||
oxc_parser = { workspace = true }
|
||||
oxc_syntax = { workspace = true }
|
||||
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = { workspace = true }
|
||||
|
||||
# `napi` is not named in the source - the `#[napi]` macro expands to code which uses it, which
|
||||
# `cargo-shear` cannot see through.
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["napi"]
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Parse `source_text` with `oxc_parser` and print the AST with `oxc_codegen`.
|
||||
*
|
||||
* Returns `null` if the source text could not be parsed without errors, which tells the caller
|
||||
* there is nothing to compare for this fixture.
|
||||
*
|
||||
* Comments are not printed. `oxc-codegen` does not print comments, and the ESTree AST the JS side
|
||||
* works from carries no comment data anyway.
|
||||
*/
|
||||
export declare function codegen(filename: string, sourceText: string, options?: Options | undefined | null): string | null
|
||||
|
||||
/**
|
||||
* Options describing how a fixture should be parsed.
|
||||
*
|
||||
* These are the same options the JS side passes to `oxc-parser`, so that both sides parse
|
||||
* the fixture identically.
|
||||
*/
|
||||
export interface Options {
|
||||
/** `"js"`, `"jsx"`, `"ts"` or `"tsx"`. When absent, the language comes from the filename. */
|
||||
lang?: string
|
||||
/** `"script"`, `"module"` or `"unambiguous"`. When absent, it comes from the language. */
|
||||
sourceType?: string
|
||||
/**
|
||||
* Whether to keep the `ParenthesizedExpression` / `TSParenthesizedType` wrapper nodes.
|
||||
* Default `false`.
|
||||
*/
|
||||
preserveParens?: boolean
|
||||
}
|
||||
export interface Comment {
|
||||
type: 'Line' | 'Block'
|
||||
value: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface ErrorLabel {
|
||||
message: string | null
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface OxcError {
|
||||
severity: Severity
|
||||
message: string
|
||||
labels: Array<ErrorLabel>
|
||||
helpMessage: string | null
|
||||
codeframe: string | null
|
||||
}
|
||||
|
||||
export declare const enum Severity {
|
||||
Error = 'Error',
|
||||
Warning = 'Warning',
|
||||
Advice = 'Advice'
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
// prettier-ignore
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
import { createRequire } from 'module'
|
||||
const require = createRequire(import.meta.url)
|
||||
const __dirname = new URL('.', import.meta.url).pathname
|
||||
|
||||
const { readFileSync } = require('fs')
|
||||
let nativeBinding = null
|
||||
const loadErrors = []
|
||||
|
||||
const isMusl = () => {
|
||||
let musl = false
|
||||
if (process.platform === 'linux') {
|
||||
musl = isMuslFromFilesystem()
|
||||
if (musl === null) {
|
||||
musl = isMuslFromReport()
|
||||
}
|
||||
if (musl === null) {
|
||||
musl = isMuslFromChildProcess()
|
||||
}
|
||||
}
|
||||
return musl
|
||||
}
|
||||
|
||||
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
|
||||
|
||||
const isMuslFromFilesystem = () => {
|
||||
try {
|
||||
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const isMuslFromReport = () => {
|
||||
let report = null
|
||||
if (process.report && typeof process.report.getReport === 'function') {
|
||||
process.report.excludeNetwork = true
|
||||
report = process.report.getReport()
|
||||
}
|
||||
if (!report) {
|
||||
return null
|
||||
}
|
||||
if (report.header && report.header.glibcVersionRuntime) {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(report.sharedObjects)) {
|
||||
if (report.sharedObjects.some(isFileMusl)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isMuslFromChildProcess = () => {
|
||||
try {
|
||||
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
|
||||
} catch (e) {
|
||||
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function requireNative() {
|
||||
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
||||
try {
|
||||
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
|
||||
} catch (err) {
|
||||
loadErrors.push(err)
|
||||
}
|
||||
} else if (process.platform === 'android') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./codegen_conformance.android-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-android-arm64')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-android-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./codegen_conformance.android-arm-eabi.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-android-arm-eabi')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-android-arm-eabi/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
if (process.arch === 'x64') {
|
||||
if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) {
|
||||
try {
|
||||
return require('./codegen_conformance.win32-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-win32-x64-gnu')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-win32-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./codegen_conformance.win32-x64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-win32-x64-msvc')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-win32-x64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ia32') {
|
||||
try {
|
||||
return require('./codegen_conformance.win32-ia32-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-win32-ia32-msvc')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-win32-ia32-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./codegen_conformance.win32-arm64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-win32-arm64-msvc')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-win32-arm64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
try {
|
||||
return require('./codegen_conformance.darwin-universal.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-darwin-universal')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-darwin-universal/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./codegen_conformance.darwin-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-darwin-x64')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-darwin-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./codegen_conformance.darwin-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-darwin-arm64')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-darwin-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'freebsd') {
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./codegen_conformance.freebsd-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-freebsd-x64')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-freebsd-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./codegen_conformance.freebsd-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-freebsd-arm64')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-freebsd-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
if (process.arch === 'x64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-x64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-x64-musl')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-x64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-x64-gnu')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-arm64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-arm64-musl')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-arm64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-arm64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-arm64-gnu')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-arm64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-arm-musleabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-arm-musleabihf')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-arm-musleabihf/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-arm-gnueabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-arm-gnueabihf')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-arm-gnueabihf/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'loong64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-loong64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-loong64-musl')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-loong64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-loong64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-loong64-gnu')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-loong64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'riscv64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-riscv64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-riscv64-musl')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-riscv64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-riscv64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-riscv64-gnu')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-riscv64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ppc64') {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-ppc64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-ppc64-gnu')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-ppc64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 's390x') {
|
||||
try {
|
||||
return require('./codegen_conformance.linux-s390x-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-linux-s390x-gnu')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-linux-s390x-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'openharmony') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./codegen_conformance.openharmony-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-openharmony-arm64')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-openharmony-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./codegen_conformance.openharmony-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-openharmony-x64')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-openharmony-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./codegen_conformance.openharmony-arm.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('oxc-codegen-conformance-openharmony-arm')
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-openharmony-arm/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
|
||||
}
|
||||
}
|
||||
|
||||
function createLoadErrorChain(errors) {
|
||||
return errors.reduce((previous, current) => {
|
||||
let message
|
||||
try {
|
||||
message =
|
||||
current && typeof current.message === 'string'
|
||||
? current.message
|
||||
: String(current)
|
||||
} catch {
|
||||
message = 'Unknown error'
|
||||
}
|
||||
const error = new Error(message)
|
||||
error.cause = previous
|
||||
return error
|
||||
}, null)
|
||||
}
|
||||
|
||||
// NAPI_RS_FORCE_WASI is a tri-state flag:
|
||||
// unset / any other value → native binding preferred, WASI is only a fallback
|
||||
// 'true' → prefer WASI, but retain native as a lazy fallback
|
||||
// 'error' → require WASI without initializing a native fallback
|
||||
// Treating any non-empty string as truthy (the historical behavior) meant
|
||||
// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered
|
||||
// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file.
|
||||
//
|
||||
// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict
|
||||
// WASI loading. It never crosses into another flavor or falls back to native.
|
||||
const __napiWasiFlavors = ["wasm32-wasi"]
|
||||
const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR
|
||||
const __napiWasiFlavorRequested =
|
||||
typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0
|
||||
if (
|
||||
__napiWasiFlavorRequested &&
|
||||
__napiWasiFlavors.indexOf(__napiWasiFlavor) === -1
|
||||
) {
|
||||
throw new Error(
|
||||
'Unsupported WASI flavor "' +
|
||||
__napiWasiFlavor +
|
||||
'". Available flavors: ' +
|
||||
__napiWasiFlavors.join(', '),
|
||||
)
|
||||
}
|
||||
const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error'
|
||||
const forceWasi =
|
||||
process.env.NAPI_RS_FORCE_WASI === 'true' ||
|
||||
forceWasiError ||
|
||||
__napiWasiFlavorRequested
|
||||
|
||||
if (!forceWasi) {
|
||||
nativeBinding = requireNative()
|
||||
}
|
||||
|
||||
if (!nativeBinding || forceWasi) {
|
||||
let wasiBinding = null
|
||||
let wasiBindingLoaded = false
|
||||
const wasiBindingErrors = []
|
||||
const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => {
|
||||
try {
|
||||
require.resolve(specifier)
|
||||
} catch (resolveError) {
|
||||
if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') {
|
||||
throw resolveError
|
||||
}
|
||||
if (isPackage) {
|
||||
try {
|
||||
require.resolve(specifier + '/package.json')
|
||||
} catch (packageError) {
|
||||
if (packageError && packageError.code === 'MODULE_NOT_FOUND') {
|
||||
return resolveError
|
||||
}
|
||||
// An exports restriction proves the package exists even when its
|
||||
// package.json is not public. Preserve the root resolution failure.
|
||||
throw resolveError
|
||||
}
|
||||
// The package exists but its main/export target is broken.
|
||||
throw resolveError
|
||||
}
|
||||
return resolveError
|
||||
}
|
||||
if (localArtifacts) {
|
||||
let artifactError = null
|
||||
for (let i = 0; i < localArtifacts.length; i++) {
|
||||
try {
|
||||
require.resolve(localArtifacts[i])
|
||||
return null
|
||||
} catch (resolveError) {
|
||||
if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') {
|
||||
throw resolveError
|
||||
}
|
||||
artifactError = resolveError
|
||||
}
|
||||
}
|
||||
return artifactError
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) {
|
||||
let candidateError = null
|
||||
let candidateFailed = false
|
||||
try {
|
||||
candidateError = __napiWasiResolveCandidate('./codegen_conformance.wasi.cjs', false, ["./codegen_conformance.wasm32-wasi.debug.wasm","./codegen_conformance.wasm32-wasi.wasm"])
|
||||
candidateFailed = candidateError !== null
|
||||
if (!candidateFailed) {
|
||||
wasiBinding = require('./codegen_conformance.wasi.cjs')
|
||||
nativeBinding = wasiBinding
|
||||
wasiBindingLoaded = true
|
||||
}
|
||||
} catch (err) {
|
||||
candidateError = err
|
||||
candidateFailed = true
|
||||
}
|
||||
if (candidateFailed) {
|
||||
wasiBindingErrors.push(candidateError)
|
||||
loadErrors.push(candidateError)
|
||||
}
|
||||
}
|
||||
if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) {
|
||||
let candidateError = null
|
||||
let candidateFailed = false
|
||||
try {
|
||||
candidateError = __napiWasiResolveCandidate('oxc-codegen-conformance-wasm32-wasi', true, undefined)
|
||||
candidateFailed = candidateError !== null
|
||||
if (!candidateFailed) {
|
||||
if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
const bindingPackageVersion = require('oxc-codegen-conformance-wasm32-wasi/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0') {
|
||||
throw new Error(`WASI binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
}
|
||||
wasiBinding = require('oxc-codegen-conformance-wasm32-wasi')
|
||||
nativeBinding = wasiBinding
|
||||
wasiBindingLoaded = true
|
||||
}
|
||||
} catch (err) {
|
||||
candidateError = err
|
||||
candidateFailed = true
|
||||
}
|
||||
if (candidateFailed) {
|
||||
wasiBindingErrors.push(candidateError)
|
||||
loadErrors.push(candidateError)
|
||||
}
|
||||
}
|
||||
if (
|
||||
!wasiBindingLoaded &&
|
||||
forceWasi &&
|
||||
!forceWasiError &&
|
||||
!__napiWasiFlavorRequested
|
||||
) {
|
||||
nativeBinding = requireNative()
|
||||
}
|
||||
if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) {
|
||||
const error = new Error(
|
||||
__napiWasiFlavorRequested
|
||||
? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found'
|
||||
: 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error',
|
||||
)
|
||||
error.cause = createLoadErrorChain(wasiBindingErrors)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadErrors.length > 0) {
|
||||
const error = new Error(
|
||||
`Cannot find native binding. ` +
|
||||
`npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +
|
||||
'Please try `npm i` again after removing both package-lock.json and node_modules directory.',
|
||||
)
|
||||
// assign instead of the `new Error(message, { cause })` options form,
|
||||
// which Node < 16.9 silently ignores
|
||||
error.cause = createLoadErrorChain(loadErrors)
|
||||
throw error
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { codegen, Severity } = nativeBinding
|
||||
export { codegen }
|
||||
export { Severity }
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "oxc-codegen-conformance",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Node API binding around `oxc_parser` + `oxc_codegen`, used as the reference output for the `oxc-codegen` package's conformance tests",
|
||||
"license": "MIT",
|
||||
"author": "Boshen and oxc contributors",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/oxc-project/oxc.git",
|
||||
"directory": "tasks/codegen_conformance"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build-dev": "napi build --esm --platform",
|
||||
"build-test": "pnpm run build-dev --profile coverage",
|
||||
"build": "pnpm run build-dev --release"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "catalog:"
|
||||
},
|
||||
"napi": {
|
||||
"binaryName": "codegen_conformance"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//! Node API binding which parses source text with [`oxc_parser`] and prints it with [`oxc_codegen`].
|
||||
//!
|
||||
//! This is the reference the `oxc-codegen` package's conformance tests are checked against.
|
||||
//! For each fixture, the tests parse the source twice - once here in Rust, and once with `oxc-parser`
|
||||
//! on the JS side - and require both printers to produce the same output.
|
||||
//!
|
||||
//! Both sides are given the same source text and the same `SourceType`, via [`oxc_napi::get_source_type`],
|
||||
//! which is the same function `oxc-parser` itself uses to turn a filename and `lang` / `sourceType` options
|
||||
//! into a `SourceType`. So the ASTs the two printers are handed are the same AST, and the only thing
|
||||
//! under test is the printing.
|
||||
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
|
||||
use napi_derive::napi;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::ast::{
|
||||
ArrowFunctionExpression, ExportAllDeclaration, ExportFromDeclaration, FormalParameterRest,
|
||||
Function, ImportDeclaration, WithClauseKeyword,
|
||||
};
|
||||
use oxc_ast_visit::{VisitMut, walk_mut};
|
||||
use oxc_codegen::{Codegen, CodegenOptions, CommentOptions};
|
||||
use oxc_napi::get_source_type;
|
||||
use oxc_parser::{ParseOptions, Parser};
|
||||
use oxc_syntax::scope::ScopeFlags;
|
||||
|
||||
/// Options describing how a fixture should be parsed.
|
||||
///
|
||||
/// These are the same options the JS side passes to `oxc-parser`, so that both sides parse
|
||||
/// the fixture identically.
|
||||
#[napi(object)]
|
||||
pub struct Options {
|
||||
/// `"js"`, `"jsx"`, `"ts"` or `"tsx"`. When absent, the language comes from the filename.
|
||||
pub lang: Option<String>,
|
||||
/// `"script"`, `"module"` or `"unambiguous"`. When absent, it comes from the language.
|
||||
pub source_type: Option<String>,
|
||||
/// Whether to keep the `ParenthesizedExpression` / `TSParenthesizedType` wrapper nodes.
|
||||
/// Default `false`.
|
||||
pub preserve_parens: Option<bool>,
|
||||
}
|
||||
|
||||
/// Parse `source_text` with `oxc_parser` and print the AST with `oxc_codegen`.
|
||||
///
|
||||
/// Returns `null` if the source text could not be parsed without errors, which tells the caller
|
||||
/// there is nothing to compare for this fixture.
|
||||
///
|
||||
/// Comments are not printed. `oxc-codegen` does not print comments, and the ESTree AST the JS side
|
||||
/// works from carries no comment data anyway.
|
||||
#[napi]
|
||||
pub fn codegen(filename: String, source_text: String, options: Option<Options>) -> Option<String> {
|
||||
let options =
|
||||
options.unwrap_or(Options { lang: None, source_type: None, preserve_parens: None });
|
||||
|
||||
// Oxc should never panic, but a panic in a Node addon aborts the process, which would throw
|
||||
// away a whole conformance run over ~58,000 fixtures. Contain it instead - the fixture is
|
||||
// reported as unprintable, and the run continues.
|
||||
catch_unwind(AssertUnwindSafe(move || {
|
||||
codegen_impl(
|
||||
&filename,
|
||||
&source_text,
|
||||
options.lang.as_deref(),
|
||||
options.source_type.as_deref(),
|
||||
options.preserve_parens.unwrap_or(false),
|
||||
)
|
||||
}))
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
fn codegen_impl(
|
||||
filename: &str,
|
||||
source_text: &str,
|
||||
lang: Option<&str>,
|
||||
source_type_option: Option<&str>,
|
||||
preserve_parens: bool,
|
||||
) -> Option<String> {
|
||||
let source_type = get_source_type(filename, lang, source_type_option);
|
||||
|
||||
let allocator = Allocator::default();
|
||||
let ret = Parser::new(&allocator, source_text, source_type)
|
||||
.with_options(ParseOptions {
|
||||
// Whatever the JS side was given.
|
||||
// Parse the same way, or the two printers would be given different ASTs,
|
||||
// and the comparison would be measuring the parse, not the print.
|
||||
preserve_parens,
|
||||
..ParseOptions::default()
|
||||
})
|
||||
.parse();
|
||||
|
||||
// Only print fixtures which parse cleanly. Oxc's parser recovers from errors, but a recovered
|
||||
// AST is not something the JS side can be expected to reproduce.
|
||||
if ret.panicked || !ret.diagnostics.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut program = ret.program;
|
||||
Normalize { preserve_parens }.visit_program(&mut program);
|
||||
|
||||
let codegen_options =
|
||||
CodegenOptions { comments: CommentOptions::disabled(), ..CodegenOptions::default() };
|
||||
Some(Codegen::new().with_options(codegen_options).build(&program).code)
|
||||
}
|
||||
|
||||
/// Erases the parts of Oxc's AST which its own ESTree representation cannot carry.
|
||||
///
|
||||
/// The JS side reaches the printer through `oxc-parser`'s ESTree AST. Where that AST holds less
|
||||
/// than the Rust one, the JS printer cannot possibly reproduce what the Rust printer does with the
|
||||
/// extra information, so the Rust AST is brought down to what ESTree can say before printing.
|
||||
///
|
||||
/// Each case below is a real difference in what the two representations can express,
|
||||
/// not a workaround for a printer bug.
|
||||
struct Normalize {
|
||||
/// `true` when the AST keeps its `ParenthesizedExpression` / `TSParenthesizedType` wrappers.
|
||||
preserve_parens: bool,
|
||||
}
|
||||
|
||||
impl<'a> VisitMut<'a> for Normalize {
|
||||
/// ESTree has no way to record that a function expression was parenthesized,
|
||||
/// which is what Oxc's `pife` flag tracks (and prints parentheses for).
|
||||
///
|
||||
/// With `preserve_parens`, it does not need one - the parentheses are in the AST
|
||||
/// as a `ParenthesizedExpression` of their own, which the JS side reads and prints.
|
||||
/// Oxc's own printer ignores that node and goes by `pife`, so the flag has to survive
|
||||
/// for the two to agree.
|
||||
fn visit_function(&mut self, func: &mut Function<'a>, flags: ScopeFlags) {
|
||||
if !self.preserve_parens {
|
||||
func.pife = false;
|
||||
}
|
||||
walk_mut::walk_function(self, func, flags);
|
||||
}
|
||||
|
||||
/// As `visit_function`, for arrow functions.
|
||||
fn visit_arrow_function_expression(&mut self, arrow: &mut ArrowFunctionExpression<'a>) {
|
||||
if !self.preserve_parens {
|
||||
arrow.pife = false;
|
||||
}
|
||||
walk_mut::walk_arrow_function_expression(self, arrow);
|
||||
}
|
||||
|
||||
/// A rest parameter's decorators are `#[estree(skip)]`ped - ESTree types them as
|
||||
/// an always-empty array on `RestElement` - so the JS side never sees them.
|
||||
/// Decorators on the other parameters do come through, and are left alone.
|
||||
fn visit_formal_parameter_rest(&mut self, rest: &mut FormalParameterRest<'a>) {
|
||||
rest.decorators.clear();
|
||||
walk_mut::walk_formal_parameter_rest(self, rest);
|
||||
}
|
||||
|
||||
/// ESTree cannot distinguish `import "m"` from `import {} from "m"` - `specifiers` is `[]` for both.
|
||||
/// It also cannot distinguish an attribute-less import from one with an empty `with {}` clause -
|
||||
/// `attributes` is `[]` for both.
|
||||
///
|
||||
/// It also does not record which keyword introduced the attributes, so `assert {...}` and `with {...}`
|
||||
/// are indistinguishable.
|
||||
fn visit_import_declaration(&mut self, decl: &mut ImportDeclaration<'a>) {
|
||||
if decl.specifiers.as_ref().is_some_and(|specifiers| specifiers.is_empty()) {
|
||||
decl.specifiers = None;
|
||||
}
|
||||
normalize_with_clause(&mut decl.with_clause);
|
||||
walk_mut::walk_import_declaration(self, decl);
|
||||
}
|
||||
|
||||
/// As `visit_import_declaration`, for `export ... from "m"`.
|
||||
fn visit_export_from_declaration(&mut self, decl: &mut ExportFromDeclaration<'a>) {
|
||||
normalize_with_clause(&mut decl.with_clause);
|
||||
walk_mut::walk_export_from_declaration(self, decl);
|
||||
}
|
||||
|
||||
/// As `visit_import_declaration`, for `export * from "m"`.
|
||||
fn visit_export_all_declaration(&mut self, decl: &mut ExportAllDeclaration<'a>) {
|
||||
normalize_with_clause(&mut decl.with_clause);
|
||||
walk_mut::walk_export_all_declaration(self, decl);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop an empty `with` clause, and forget which keyword introduced a non-empty one.
|
||||
fn normalize_with_clause(
|
||||
with_clause: &mut Option<oxc_allocator::Box<'_, oxc_ast::ast::WithClause<'_>>>,
|
||||
) {
|
||||
if let Some(inner) = with_clause {
|
||||
inner.keyword = WithClauseKeyword::With;
|
||||
|
||||
if inner.with_entries.is_empty() {
|
||||
*with_clause = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user