mirror of
https://github.com/oven-sh/bun.git
synced 2026-10-25 21:40:30 +08:00
bun fmt: every comment is attached to one node before printing, as in Prettier (#45205)
### What does this PR do?
Rebuilds how `bun fmt` handles comments in JavaScript and TypeScript. Before printing starts, every comment is attached to exactly one node, as a leading, trailing or dangling comment, the way Prettier does it. A printer prints the comments that a node owns. It no longer decides who owns a comment.
It started as a fix for three bugs with `// prettier-ignore`. Each fix led to more findings of the same kind, because the old model found comments by their position while printing, and every printer had its own idea of which comments were its own. The bugs below are what a user sees. The rest is about the new model.
## Bugs that are fixed
### A trailing `// prettier-ignore` was deleted, and parentheses went with it
```js
const x = (a + b) // prettier-ignore
.foo();
```
`bun fmt` printed this, with exit code 0:
```js
const x = a + b
.foo();
```
`(a + b).foo()` had become `a + b.foo()`. Prettier leaves the input unchanged, and so does `bun fmt` now.
| Input, with `// prettier-ignore` and a line break at `⏎` | `bun fmt` printed | Effect |
|---|---|---|
| `(a + b) ⏎ .foo()` | `a + b.foo()` | another program |
| `(a ? b : c) ⏎ .foo()` | `a ? b : c.foo()` | another program |
| `(await y) ⏎ .foo()` | `await y.foo()` | another program |
| `(a + b) ⏎ (1)` | `a + b(1)` | another program |
| `new (a + b) ⏎ (1)` | `new a + b(1)` | another program |
| `(() => {}) ⏎ ()` | `() => {}()` | doesn't parse |
| `(y as T) ⏎ .foo()` | `y as T.foo()` | doesn't parse |
| `foo({ a: 1 }) ⏎ .bar()` | the comment was deleted | a second run joined the lines |
### The left side of `a && // prettier-ignore` was reformatted
```js
const x =
foo({ a: 1, b: 2 }) && // prettier-ignore
bar;
```
For Prettier the comment trails the left operand, so that operand is printed as written. `bun fmt` reformatted it.
### A comment that contains the letters `else` was taken for the keyword
```js
if (a)
b(); // else
// note
else if (c) d();
```
`bun fmt` moved `// note` into the branch. The code looked for `else` in the source text, which could include an earlier comment.
### A comment in a Vue binding was deleted
`v-slot="/* c */"` became `v-slot=""`, and `v-for="(/* c */) in xs"` became `v-for="() in xs"`. Code in a template that cannot be written with all of its comments now stays as it is, and the rest of the file is formatted, which is what Prettier does.
### `<!--` and `-->` comments in a script were deleted with an `.oxfmtrc.json`
```js
<!-- a
b();
```
was printed as `b();`. By default the comments were kept, but `var⏎--> c⏎a = b` was printed as `var --> c`, which does not parse: `-->` starts a comment at the start of a line only. With these comments in every gap of 20 statements (1,140 files), what is printed does not parse in 40 files on main and in none now.
### Comments in rare places are where Prettier and oxfmt put them
One comment, in 14 forms, in every gap between two tokens of 861 snippets of rare syntax: 176,498 files. The share that is byte for byte what the real tool prints:
| | main | this PR |
|---|---|---|
| default, against Prettier 3.9.9 | 94.62% | 99.56% |
| with an `.oxfmtrc.json`, against oxfmt 0.72 | 95.46% | 97.49% |
### `--debug-check` reports an output that has a syntax error
The parser goes on after some errors, such as a line break before `=>`, and builds the same tree. The check compared the trees and was silent.
## The new model
- **One record per comment.** The lexer writes it: the span, where the previous token ends and the next one starts, line or block, and whether there is a line break before and after it. Its index is the comment's id. Nothing else describes a comment.
- **The parser finds the nodes around each comment while it parses**: the enclosing, the preceding and the following node. Each time a node is finished, its range is compared with the closest comments that are still pending. Nothing walks the tree afterwards.
- **`src/format/js/attach/`** turns those three nodes into an owner. `rules.rs` is Prettier's `handle-comments.js`, with the same function names. `rules/oxfmt.rs` has the rules for where oxfmt prints a comment somewhere else.
- **Leading and trailing comments are printed by the wrappers** that every node goes through, dangling comments by the function that writes the node. `// prettier-ignore` is handled in one place.
- **Every comment is printed exactly once, and none costs the file.** If a comment is left over, the file is formatted once more, and the statement or class member around that comment writes it. Prettier throws in that case.
- **`bun check` and `bun lint` run a parser that is compiled without the hooks.**
`src/format/CLAUDE.md` describes it in the section "Comments".
## Where `bun fmt` differs from the tool on purpose
All follow the formatter's rule that what is written has to mean what the file means. Each has a test in `test/cli/format/format.test.ts` that says what the tool prints.
| Tool | What it prints | Example of its output |
|---|---|---|
| Prettier | a `;` after an ignored declaration in the head of a `for` | `for (/* prettier-ignore */ const a; of b) {}` |
| Prettier | what it copies without the parentheses that only its text needs | `x =⏎ a <T> + // prettier-ignore⏎ y;` for `(a <T>)` |
| oxfmt | what it copies without its parentheses | `const x = a + b // prettier-ignore⏎(1);` |
| both | a comment of several lines before the `[]` of an array type, where a line break ends the type | `type A = D /* c⏎ x */[];` |
| Prettier | a line break between two comments before `=>` | `(a) /* b */⏎/* c */ => d` |
| Prettier | the type after a line comment, in a class without a name | `class implements // cA {}` |
| Prettier | the `}` of a substitution in a template type after a line comment | `` type A = `${B // c}-${C}`; `` |
| oxfmt | the `]` of an array after a line comment that follows a hole | `[a, ,// c];` |
| Prettier | a comment that waits for the end of the line in the middle of the next comment | `a; /**⏎// b⏎ * c⏎ */` |
| Prettier | nothing: it throws for an ignore comment before an empty body | `class A // prettier-ignore⏎{ }` |
| oxfmt | a Vue binding without its comment | `v-slot=""` |
| oxfmt | a `-->` comment that is not at the start of its line | `var --> c`, and `x = a--> c⏎.b.c()`, which is `x = a-- > c.b.c()` |
## Known differences that are left
- Two line comments can end up on one line (`a.b; // c // d`) where the code between them is joined. Prettier prints that in its own fixtures too, so it is not changed in general. In a few shapes the tool keeps them apart and `bun fmt` does not, for example `import a from "b" with // c⏎{ // d⏎};`.
- `import a // b⏎/**⏎ * c⏎ */ from "d"` has two blanks in `import a from // b`. With oxfmt's option `jsdoc`, if the option rewrites that JSDoc comment, `// b` still ends up inside it, as on main: `write_lines` in `js/jsdoc/mod.rs` needs the boundary too.
- `--cursor-offset`: in 211 of 25,440 cases with a comment next to the cursor the text is Prettier's and the offset is not. 202 of them are wrong on main too. The other 9 are `interface A // c⏎{}`.
- With an `.oxfmtrc.json`, 2.5% of the gaps above differ from oxfmt, spread evenly over the 14 forms of comment. On main it is 4.5%. 13 of the inputs in `own/cases` have their oxfmt snapshot as `.snap.todo`.
- A file in which every statement, property, element, argument or member has a comment takes 1.0 to 1.7 times as long as on main, and 130 to 680 bytes more for each comment while it is formatted. Seven such files with 40,000 comments each take 0.35 s together, 0.28 s on main. Ordinary code is faster than on main, see below.
### How did you verify your code works?
**Against the real tools, Prettier 3.9.9 and oxfmt 0.72**
| | main | this PR |
|---|---|---|
| Prettier's own tests, failing cases | 9 | 5 |
| oxfmt's own tests, failing cases | 157 | 159 |
| the owner of each of 179,799 comments in 27,475 files is the node that Prettier attaches it to | | 99.942% |
| gap corpus, Prettier (163,914 compared) | 155,090 | 163,194 |
| gap corpus, oxfmt (162,754 compared) | 155,357 | 158,665 |
| 3,648 inputs with an ignore comment, same as Prettier | 2,614 | 3,539 |
| 3,648 inputs with an ignore comment, same as oxfmt | 3,257 | 3,447 |
| 9,996 inputs with a comment in an expression, same as Prettier | 9,861 | 9,980 |
| 9,996 inputs with a comment in an expression, same as oxfmt | 8,736 | 8,782 |
| 25,440 cursor positions next to a comment, same as Prettier | 23,023 | 25,031 |
| 50,880 ranges next to a comment, same as Prettier | 47,017 | 50,426 |
| 1,130 inputs with a `<!--` or `-->` comment, same as oxfmt | 20 | 999 |
The two more failures in oxfmt's tests are one fixture at two widths, where `bun fmt` differs on purpose: the first row of the table above. So are all the cursor positions and ranges but 9 that are right on main and not here.
**Real code.** The 11,546 JavaScript and TypeScript files of this repository are printed byte for byte as on main, in both flavors (9,512 of them are changed by formatting).
**Fuzzing against the real tools.** 4.6 million generated inputs: a comment in every gap, two and three comments, ignore comments on the members of 38 kinds of list, parentheses and casts, ASI, odd files, CRLF, templates, Vue, Svelte, Angular, HTML, Markdown, MDX, sorted imports, JSDoc, pragmas, ranges, cursors, damaged inputs. No crash, no hang, and no comment lost or doubled in embedded code. What it found is fixed in this PR or listed above.
**Tests in the tree**
- `test/cli/format/own/cases`: 426 inputs with what both tools print.
- `own/comment-owners.txt`: the node that Prettier attaches each of 1,292 comments in 228 of these inputs to. `conformance.test.ts` compares it with `bun fmt --debug-comments`. 44 differences are listed with their reasons: nodes that ESTree has and the HIR does not.
- "JavaScript with many comments takes time in proportion to its size": 83 shapes that repeat something with a comment 40,000 times, in both flavors, by processor time. All of them together take 4.7 s by default and 5.1 s with an `.oxfmtrc.json`. On main they take 93 s and 41 s, because some are quadratic there.
- `format.test.ts`, `conformance.test.ts`, `svelte.test.ts`: 1,283 pass, with the release binary of CI too. The 78 test files of `bun fmt`, `bun lint` and `bun check`: 16,044 pass and 1 fails, which fails on main too. The 61 tests that compare `bun check` with `tsc` pass.
**Speed, on the release binaries of CI (LTO), formatting and checking the sources of VS Code**
| billions of instructions | main | this PR | change |
|---|---|---|---|
| `bun fmt`, default | 18.059 | 17.752 | -1.70% |
| `bun fmt`, with an `.oxfmtrc.json` | 18.468 | 18.350 | -0.64% |
| `bun check` | 130.474 | 130.523 | +0.04% |
The runs of `bun fmt` differ by at most 1 million instructions, those of `bun check` by up to 57 million. It runs a parser without the hooks, but the lexer writes a larger record for each comment, which the reader of JSDoc and the linter use too. In cycles, 21 pairs of runs on 8 pinned cores: `bun fmt` is faster than main in 21 by default (median -1.5%) and in 19 with an `.oxfmtrc.json` (median -0.9%). The binary for Linux x64 is 152 KB larger.
**How deep code can be nested** before "The code is nested too deeply", same binaries:
| | main | this PR | change |
|---|---|---|---|
| `a.b.b.b` | 160,215 | more than 400,000 | |
| `a!.b!.b` | 40,714 | 77,228 | +90% |
| `switch` in `switch` | 19,234 | 28,998 | +51% |
| `{{{}}}` | 26,655 | 37,394 | +40% |
| `a as a as a` | 38,566 | 49,500 | +28% |
| `if (a) if (a)` | 18,648 | 23,725 | +27% |
| `else if` chain | 18,648 | 23,725 | +27% |
| `while (a) while (a)` | 18,648 | 23,725 | +27% |
| `a()()()` | 29,193 | 32,512 | +11% |
| `try` in `try` | 15,524 | 17,282 | +11% |
| `<a><a></a></a>` | 9,081 | 9,667 | +6% |
| `a ? b : a ? b : c` | 25,093 | 26,264 | +5% |
| function in function | 6,738 | 6,933 | +3% |
| `!!!a` | 52,234 | 52,234 | +0% |
| class in method | 3,419 | 3,419 | +0% |
| `() => () => a` | 5,957 | 5,762 | -3% |
| `(B \| (B \| C))` | 5,957 | 5,762 | -3% |
| `f(f(f(a)))` | 4,786 | 4,590 | -4% |
| `[[[]]]` | 4,590 | 4,395 | -4% |
| `{a:{a:1}}` | 4,395 | 4,200 | -4% |
| `a + (a + (a))` | 4,200 | 4,005 | -5% |
| `B<B<C>>` | 4,590 | 4,005 | -13% |
| `for (;;) for (;;)` | 18,648 | 16,305 | -13% |
Where it is less, the parser is what gives up: parsing alone has the same limit. A checkpoint for looking ahead holds 16 more bytes, and `for` is not inlined into the function that parses a statement, so that `if` and `else if` need less.
This commit is contained in:
1 parent
47ba1c8d85
commit
f841e36709
541 files changed
+27983
-10058
No files matched your search
+90
-39
@@ -15,8 +15,8 @@ Not there: Astro, Babel-only proposals, plugins. CSS, GraphQL, Markdown and HTML
|
||||
## The pipeline
|
||||
|
||||
```
|
||||
File (HIR + binder tables) bun_lint::ast
|
||||
└─ js::comments::collect → [Comment] all comments, in order, with what is around them
|
||||
File (HIR + binder tables) bun_lint::ast. The parser has recorded the nodes around each comment
|
||||
└─ js::comments::of_file → FileComments which node owns each comment: js/attach/
|
||||
└─ js::format_file → [FormatElement] the document (IR): ir/element.rs
|
||||
└─ ir::document::Tracker told about each element as it is written: a group with a forced line
|
||||
break in it is broken, every other group is measured
|
||||
@@ -29,28 +29,28 @@ What a caller does with a file, in this order, is `format_for_tests` in `src/lin
|
||||
|
||||
**Import sorting** (`js/sort_imports/`, option `FormatOptions::sort_imports`, compiled once per run from `sort_imports::Settings`) has five flavours. `@trivago`/`@ianvs` `importOrder*` and prettier-plugin-organize-imports are text → text preprocessors in Prettier, so they are here too: `sort_imports::sorted_text(file, how)` gives the text that has to be parsed and formatted instead of the file, byte for byte the plugin's (`babel.rs`: @babel/parser's comment attachment over the HIR, `generator.rs`: @babel/generator's printer for imports, `trivago.rs`/`ianvs.rs`: the plugins, `organize.rs`: TypeScript's organizeImports and textChanges), or `None` if formatting the file as it is gives the same (`layout.rs`): only a file whose imports move is parsed twice. oxfmt's `sortImports` (`oxfmt/`) happens inside `format`: `FormatStatements` tells an `ImportRun` what it is about to write, a run of imports is captured and written as `Interned` sub-ranges, one per line, in sorted order. `sortImports` from bunfig.toml or `--sort-imports` (`How::Native`) runs oxfmt's sorter with two additions. Side-effect imports act as barriers, and `native.rs` sorts the names inside braces and merges imports from the same module. `native.rs` is a text → text pass too, so both `sorted_text` and `format` do part of the work. The driver checks the result with `SortImports::keeps_imports`. Tests: `test/cli/format/sort-imports`, made by `test/cli/format/oracle/sort-imports/`, and `test/cli/format/sort-imports.test.ts`, which has no oracle.
|
||||
|
||||
| directory | what | oxc |
|
||||
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| `ir/` | the IR, `Formatter`, builders (`group`, `indent`, `soft_line_break`, ..), macros, the printer. Knows nothing about JavaScript except `Formatter::context` | `oxc_formatter_core` |
|
||||
| `options.rs` | `FormatOptions`, with oxc's field and type names, and `set("semi", "false")` with Prettier's | `oxc_formatter/src/options.rs` |
|
||||
| `js/format.rs` | `impl Format for Expr, Stmt, TypeNode, ..`: comments, `prettier-ignore`, parentheses, then dispatch to `js/print/` | the generated `ast_nodes/generated/format.rs` |
|
||||
| `js/ast_nodes.rs`, `fields.rs`, `siblings.rs` | the tree as oxc sees it: `AstNodes`, `parent()`, `span()`, field accessors, the next sibling | `ast_nodes/` |
|
||||
| `js/comments.rs`, `trivia.rs` | which comments are printed where | `formatter/comments.rs`, `formatter/trivia.rs` |
|
||||
| `js/parentheses/` | `needs_parentheses` | `parentheses/` |
|
||||
| `js/print/` | one function per kind of node | `print/` |
|
||||
| `js/utils/` | what several kinds of nodes share: assignments, member chains, conditionals, strings, numbers | `utils/` |
|
||||
| `js/sort_imports/` | import sorting: `@trivago`/`@ianvs` `importOrder*`, oxfmt's `sortImports` | |
|
||||
| `json/`, `css/`, `graphql/` | JSON (`json`, `json5`, `jsonc`, `json-stringify`), style sheets (`css`, `less`, `scss`) and GraphQL, each with a parser of its own. `embed.rs` in the last two: the same in the templates of JavaScript | |
|
||||
| `yaml/` | YAML: Bun's own parser says what is in what and where the tokens are (`bun_parsers::yaml::YAML::parse_as_written`), `ast.rs` makes the tree of `yaml-unist-parser` of that, `printer.rs` is `language-yaml`. Prints with `css/doc.rs`. Also the front matter of style sheets and of Markdown. What Bun does not read as YAML is a syntax error | |
|
||||
| `markdown/` | Markdown and MDX: `parse.rs` makes the tree of micromark and remark (for MDX: of remark-parse 8) from the events of Bun's own parser, `bun_md`. `printer.rs` is `language-markdown`. Prints with `css/doc.rs`. Code blocks go to the other formatters. For JavaScript the caller passes a closure to `markdown::format_with`, or sets `FormatOptions::format_javascript`, since this crate does not parse it | |
|
||||
| `handlebars/` | Handlebars as Glimmer reads it (`.hbs`, `.handlebars`): ports of the lexer and the grammar of `@handlebars/parser`, of `simple-html-tokenizer` and of the handlers of `@glimmer/syntax` in the mode `codemod`, and `language-handlebars`, which writes to the `Elements` of `css/doc.rs`. What the parsers drop or change (a doctype, `{{this/a}}`), Prettier does not print. Here the node that it is dropped from is marked, `Node::is_as_written`, and printed as the template has it. If it is in no node, so is all of the template: `Tree::has_lost_text`. `verify.rs` is `--debug-check`: both texts are parsed and the trees compared | |
|
||||
| `html/` | HTML, Vue, Angular templates, LWC and MJML: ports of `angular-html-parser`, of `language-html` and of the lexer and parser of Angular's expressions. It writes to the document of `ir/`. See "HTML" below | |
|
||||
| `svelte/` | Svelte: see "Svelte" below | |
|
||||
| `toml/` | TOML, in the flavor of oxfmt (Prettier has none) and where no tool is stood in for: the tokens of Bun's own scanner (`bun_parsers::toml::TOML::tokens`), the tree that taplo's parser makes of them, and a port of the formatter of `oxc-toml` as far as oxfmt's options reach. Keys and values are copied. What Bun does not read as TOML is a syntax error: oxfmt writes such a file again around its errors, and out of order | |
|
||||
| `js/jsdoc/` | oxfmt's option `jsdoc`: JSDoc comments are formatted as prettier-plugin-jsdoc does. Off unless `.oxfmtrc.json` or `[fmt]` sets it. A comment whose formatted text would have `*/` in it stays as it is. What is a tag, a type, a name and a text in a comment is read by `file.jsdoc()` (`src/lint/ast/jsdoc.rs`), nothing here splits a comment. The parameters that `@param` tags are sorted by are those of the function in the tree | `formatter/jsdoc/` |
|
||||
| `conformance/` | the crate `bun_format_conformance`: runs the tests of Prettier and of oxfmt | |
|
||||
| `pragma.rs`, `range.rs`, `cursor.rs` | `insertPragma`/`requirePragma`/`checkIgnorePragma`, `rangeStart`/`rangeEnd`, `cursorOffset`: Prettier's `src/main/core.js` | |
|
||||
| `verify.rs`, `verify/` | `--debug-check`: formatting did not change the program. The trees and the comments before and after | `detect_code_removal` (different) |
|
||||
| directory | what | oxc |
|
||||
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `ir/` | the IR, `Formatter`, builders (`group`, `indent`, `soft_line_break`, ..), macros, the printer. Knows nothing about JavaScript except `Formatter::context` | `oxc_formatter_core` |
|
||||
| `options.rs` | `FormatOptions`, with oxc's field and type names, and `set("semi", "false")` with Prettier's | `oxc_formatter/src/options.rs` |
|
||||
| `js/format.rs` | `impl Format for Expr, Stmt, TypeNode, ..`: comments, `prettier-ignore`, parentheses, then dispatch to `js/print/` | the generated `ast_nodes/generated/format.rs` |
|
||||
| `js/ast_nodes.rs`, `fields.rs` | the tree as oxc sees it: `AstNodes`, `parent()`, `span()`, field accessors | `ast_nodes/` |
|
||||
| `js/attach/`, `comments.rs`, `owners.rs` | which node owns each comment, and which comments have been printed. `trivia.rs` writes them | (different) `formatter/trivia.rs` |
|
||||
| `js/parentheses/` | `needs_parentheses` | `parentheses/` |
|
||||
| `js/print/` | one function per kind of node | `print/` |
|
||||
| `js/utils/` | what several kinds of nodes share: assignments, member chains, conditionals, strings, numbers | `utils/` |
|
||||
| `js/sort_imports/` | import sorting: `@trivago`/`@ianvs` `importOrder*`, oxfmt's `sortImports` | |
|
||||
| `json/`, `css/`, `graphql/` | JSON (`json`, `json5`, `jsonc`, `json-stringify`), style sheets (`css`, `less`, `scss`) and GraphQL, each with a parser of its own. `embed.rs` in the last two: the same in the templates of JavaScript | |
|
||||
| `yaml/` | YAML: Bun's own parser says what is in what and where the tokens are (`bun_parsers::yaml::YAML::parse_as_written`), `ast.rs` makes the tree of `yaml-unist-parser` of that, `printer.rs` is `language-yaml`. Prints with `css/doc.rs`. Also the front matter of style sheets and of Markdown. What Bun does not read as YAML is a syntax error | |
|
||||
| `markdown/` | Markdown and MDX: `parse.rs` makes the tree of micromark and remark (for MDX: of remark-parse 8) from the events of Bun's own parser, `bun_md`. `printer.rs` is `language-markdown`. Prints with `css/doc.rs`. Code blocks go to the other formatters. For JavaScript the caller passes a closure to `markdown::format_with`, or sets `FormatOptions::format_javascript`, since this crate does not parse it | |
|
||||
| `handlebars/` | Handlebars as Glimmer reads it (`.hbs`, `.handlebars`): ports of the lexer and the grammar of `@handlebars/parser`, of `simple-html-tokenizer` and of the handlers of `@glimmer/syntax` in the mode `codemod`, and `language-handlebars`, which writes to the `Elements` of `css/doc.rs`. What the parsers drop or change (a doctype, `{{this/a}}`), Prettier does not print. Here the node that it is dropped from is marked, `Node::is_as_written`, and printed as the template has it. If it is in no node, so is all of the template: `Tree::has_lost_text`. `verify.rs` is `--debug-check`: both texts are parsed and the trees compared | |
|
||||
| `html/` | HTML, Vue, Angular templates, LWC and MJML: ports of `angular-html-parser`, of `language-html` and of the lexer and parser of Angular's expressions. It writes to the document of `ir/`. See "HTML" below | |
|
||||
| `svelte/` | Svelte: see "Svelte" below | |
|
||||
| `toml/` | TOML, in the flavor of oxfmt (Prettier has none) and where no tool is stood in for: the tokens of Bun's own scanner (`bun_parsers::toml::TOML::tokens`), the tree that taplo's parser makes of them, and a port of the formatter of `oxc-toml` as far as oxfmt's options reach. Keys and values are copied. What Bun does not read as TOML is a syntax error: oxfmt writes such a file again around its errors, and out of order | |
|
||||
| `js/jsdoc/` | oxfmt's option `jsdoc`: JSDoc comments are formatted as prettier-plugin-jsdoc does. Off unless `.oxfmtrc.json` or `[fmt]` sets it. A comment whose formatted text would have `*/` in it stays as it is. What is a tag, a type, a name and a text in a comment is read by `file.jsdoc()` (`src/lint/ast/jsdoc.rs`), nothing here splits a comment. The parameters that `@param` tags are sorted by are those of the function in the tree | `formatter/jsdoc/` |
|
||||
| `conformance/` | the crate `bun_format_conformance`: runs the tests of Prettier and of oxfmt | |
|
||||
| `pragma.rs`, `range.rs`, `cursor.rs` | `insertPragma`/`requirePragma`/`checkIgnorePragma`, `rangeStart`/`rangeEnd`, `cursorOffset`: Prettier's `src/main/core.js` | |
|
||||
| `verify.rs`, `verify/` | `--debug-check`: formatting did not change the program. The trees and the comments before and after | `detect_code_removal` (different) |
|
||||
|
||||
## Style sheets, YAML, Markdown: the other printer
|
||||
|
||||
@@ -82,6 +82,8 @@ The printer for style sheets returns no documents. It writes operations (text, l
|
||||
|
||||
- **Whatever calls itself for what is nested asks the stack first**, in every stage: parser, what walks the tree, printer, document, `--debug-check`. `stack.rs`: `Stack::has_room()`. One that is told no goes on with anything, and whoever started the work asks `Stack::ran_out()` at the end: `FormatError::NestedTooDeeply`, "RangeError: The code is nested too deeply.", and the file stays as it is. Never an answer like "it does not match" or "as it is written". For JavaScript: `f.context_mut().has_stack_left()`.
|
||||
- **In `js/` the doors ask**: `write!(f, child)` for an `Expr`, a `Stmt`, a `TypeNode`, and `write_expression`. What writes something that is nested in itself in any other way (a `Format` of its own that makes another of itself, a function that is handed a child) asks for itself, with a comment that says "not by way of `Expr`". One that does not is not seen by a test: another one beside it asks, so the file is refused with the right words, until it is a little deeper and the stack ends. To see it, tell the checks of this crate that the stack ends 200 KB below them, let the parser keep the real one, and watch the memory behind that end.
|
||||
- **How deep is a matter of bytes per level, and only an LTO build shows them.** LTO inlines whole printers into `write_declaration`, `write_type` and `write_less_common_expression`, and a function takes the stack for everything that is inlined into it, used or not. So a printer that needs much of it is `#[inline(never)]`, one that only runs for a node with comments is `#[cold]` too, and one that writes its child first (`a.b`, `a as b`) does the rest in a function of its own. `objdump` shows what a function takes: the `push`es and the `sub $.., %rsp` at its start.
|
||||
- **Find the stage that gives up before changing a frame.** `--debug-comments` parses and attaches and does not print, so it has the same limit if the parser is the one. A file that the fast variant of the parser refuses is parsed again by the general one (`Parser<true, ..>`), so the limit is the larger of the two. A breakpoint at `Parser::refuse` and the stack pointer of each frame above it show the functions that call each other and what each takes.
|
||||
- **What builds a tree** that is walked afterwards, and the first that calls itself for a tree that a loop has built, takes `Stack::for_tree()` or `check_for_tree()`, which leaves room for what comes afterwards.
|
||||
- **A document has nothing in it twice.** A document of Prettier can have the same object in both branches of an `ifBreak`. A copy of it here doubles with each level: `Alignment::IfBreak` is there for the one place that needs it.
|
||||
- **A question with a yes or a no** (is there a line break in it, is it empty) is a loop over a list of what has not been looked at.
|
||||
@@ -183,9 +185,9 @@ Every variant has one field, the handle. It emulates the nodes that oxc has and
|
||||
| `parent.is_call_like_callee_span(span)` | `parent.is_call_like_callee(e)` |
|
||||
| `node.needs_parentheses(f)` | `parentheses::expression::needs_parentheses(e, f)`, `parentheses::ts_type::needs_parentheses(ty, f)` |
|
||||
| `node.write(f)` (without comments and parentheses) | `write_expression(e, ExprOptions::None, f)`, `write_declaration(stmt, f)`, `write_type(ty, f)` |
|
||||
| `node.format_leading_comments(f)` | `format_leading_comments(span).fmt(f)` |
|
||||
| `node.format_leading_comments(f)` | `leading_comments(node).fmt(f)` |
|
||||
| `node.format_trailing_comments(f)` | `write_trailing_comments_of(node, f)` |
|
||||
| `self.id()`, `member.property()`: an identifier node | `identifier(ident, parent_node)`. Names are `Ident`s, not nodes |
|
||||
| `self.id()`, `member.property()`: an identifier node | `identifier(ident)`. Names are `Ident`s, not nodes |
|
||||
| `property.key()` | `FormatKey::new(key, parent_node)`, `format_computed_or_property_key(..)` in `utils/object.rs` |
|
||||
| `self.type_annotation()` (`: T`) | `x.ty().map(FormatTypeAnnotation)` |
|
||||
| `self.type_parameters()`, `self.type_arguments()` | `type_parameters(list, owner)`, `type_arguments(list, owner)` in `print/type_parameters.rs`. They write nothing for an empty list |
|
||||
@@ -209,32 +211,81 @@ So oxc's `matches!(callee, Expression::StaticMemberExpression(_))`, which is fal
|
||||
|
||||
## Comments
|
||||
|
||||
The algorithm is oxc's, not Prettier's: comments are not attached to nodes up front. `Comments` is a **cursor** over the sorted comments of the file. Whoever formats a position asks for "the unprinted comments before X" and prints them, which advances the cursor. So:
|
||||
The algorithm is Prettier's, not oxc's. Before printing starts, every comment is attached to exactly one node, as a leading, trailing or dangling comment. A printer prints the comments that a node owns. It never decides who owns a comment.
|
||||
|
||||
- Nodes have to be formatted **in source order**. If something is formatted ahead of its turn to look at it (`memoized().inspect(f)`, `f.intern`), everything before it has to be formatted first, and the result has to be used, not thrown away. To throw it away: `f.speculate_will_break(&content)`, which restores the cursor.
|
||||
- A comment that nobody asks for is printed by the next node as a leading comment.
|
||||
- `impl Format for <handle>` prints leading comments (all unprinted ones before `span.start`), the node, then trailing comments: `Comments::get_trailing_comments(enclosing_span, preceding_span, following_span_start)` decides how many of the next comments belong to this node and not to the next sibling. The parent's span and the next sibling come from `AstNodes::parent()` and `siblings.rs`, and are only computed if there is a comment nearby.
|
||||
- Dangling comments (`{ /* here */ }`) are printed by the function that writes the node: `format_dangling_comments(span).with_block_indent()`.
|
||||
- `FormatNodeWithoutTrailingComments(&x)` leaves the comments after `x` to the caller.
|
||||
- `// prettier-ignore`, and in the flavor of oxfmt `// oxfmt-ignore` (`Flavor::is_ignore_comment`, for every language): `f.comments().is_suppressed(span.start)` → `FormatSuppressedNode(span)` prints the source text.
|
||||
- Where Prettier attaches a comment to a node that it is not next to (`handleMemberExpressionComments`, a comment before the `(` of a signature), `comments::collect` ends with `move_comments`: the comment gets a position that it counts as being at, `Comment::start()`/`end()` (zero width, `is_moved()`), and the list is sorted by that. `comment.span` stays where the text is. All queries of `Comments` go by `start()`/`end()`. A moved comment at the very end of a node is behind it: `comments_before_end_of(span)`.
|
||||
- The comments in a member access, a call, a `new` or an `a!`, before the property or the first argument, are attached as Prettier's `attachComments` does it, to which parentheses are nothing: `comments::attach_in_link`. One that trails what the link follows (the object, what is called) is moved to the end of that: `Comments::comments_trailing_link`. One that leads a link of a member chain is moved to where the `.b` of the link is written, wherever it is in the source (`(/* here */ a.b)()`): `comments_leading_link`. One that leads a member access outside of a chain is moved to the start of that, and is written by it, not by what starts with it, so that it breaks the same groups as in Prettier: `format_leading_comments(span)` leaves it alone for a longer `span` (`comments_leading_node`). `utils/member_chain` asks nothing else about comments, neither to write them nor to group the links. In the flavor of oxfmt nothing is attached: there it goes by what is around a comment.
|
||||
### One record per comment
|
||||
|
||||
`hir::Comment` (`src/sema/hir.rs`) is the only representation of a comment. Its index in `File::comments` is the comment's id.
|
||||
|
||||
| who writes it | what |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| the lexer | the span, where the previous token ends and the next token starts, line or block, whether it spans lines, whether there is a line break before and after it |
|
||||
| the parser | the enclosing, preceding and following node |
|
||||
|
||||
- The parser finds the three nodes while it parses (`src/sema/parser/parser/comments.rs`). Each time a node is finished, its range is compared with the closest comments that are still pending. Nothing walks the tree afterwards.
|
||||
- This only happens with `Options::attach_comments`, which only the formatter sets. The hooks are behind the parameter `COMMENTS` of the `Parser`, so `bun lint` and `bun check` run a parser that is compiled without them. One comparison per node was 0.1% of the instructions of `bun check` on vscode.
|
||||
- `bun_lint::ast::comments::Comment<'a>` is the handle to the record, `Copy` like `Expr<'a>`.
|
||||
- The formatter stores only what it decides itself, by comment id (`js/comments.rs`): placement and marker, the table from a node to its comment ids (`js/owners.rs`), and one "printed" bit.
|
||||
- `hir::File::detached_semicolons` lists each `;` that ends a statement but does not directly follow the previous token. `File::without_semicolon(span)` uses it to give Prettier's `locEnd` without looking at the text. It is recorded with `attach_comments` too.
|
||||
|
||||
**Do not scan the source text** to find a token, or to skip whitespace and comments. If a position is missing, have the lexer or the parser record it.
|
||||
|
||||
### Attaching: `js/attach/`
|
||||
|
||||
1. `surrounding_nodes.rs` converts the three recorded nodes to `AstNodes`, and adjusts them where ESTree's tree is not the HIR's. For example, ESTree has a node for the closing tag of a JSX element.
|
||||
2. `rules.rs` is Prettier's `handle-comments.js`, with the same function names. `rules/oxfmt.rs` has the rules of the oxfmt flavor: where oxfmt prints a comment somewhere else than Prettier, the comment has another owner.
|
||||
3. A comment that no rule handles is attached by its position in the line (`attach_by_text_position`, `break_ties` in `mod.rs`): on its own line it leads the following node, at the end of a line it trails the preceding node.
|
||||
|
||||
An owner is an `AstNodes`, a name (an identifier or a literal key is a token here and a node in ESTree, so it is identified by its start offset), or the parentheses after a `/** @type {T} */` cast.
|
||||
|
||||
**Nothing here may walk a list for each comment**: not the comments of the gap, not the elements of the array, not the parents up to the root. A file can have 40,000 comments between two tokens, or one in each of 40,000 elements. `AttachContext::comments_between_same_tokens` has what the rules ask about all the comments between two tokens, found once. Elements are in source order, so search them by halves. The test "JavaScript with many comments takes time in proportion to its size" has the shapes that once took quadratic time. Add yours.
|
||||
|
||||
### Printing
|
||||
|
||||
```rust
|
||||
let owned = f.comments().of_node(node); // leading, trailing, dangling: CommentList<'a>, and is_ignored
|
||||
f.comments().of_name(start_offset)
|
||||
leading_comments(node) trailing_comments(node) dangling_comments(node).with_block_indent() // impl Format
|
||||
```
|
||||
|
||||
- **Leading and trailing comments are printed by the wrappers**: `write!(f, expr)`, `write!(f, statement)`, `write!(f, ty)`, `format_node(node, f, |f| ..)` and `format_name(span, f, |f| ..)` in `js/format.rs`. This is Prettier's `printComments`.
|
||||
- **Dangling comments are printed by the function that writes the node**, like Prettier's `printDanglingComments`. If a node prints its dangling comments in several places, `f.comments().attachment(comment)` has Prettier's `comment.marker` to tell them apart.
|
||||
- **`// prettier-ignore`**, and `// oxfmt-ignore` in that flavor: the same wrappers copy the node's source text (`write_ignored_node`). `f.comments().is_ignored(node)` is Prettier's `hasNodeIgnoreComment`.
|
||||
- **`FormatNodeWithoutTrailingComments(&child)`** leaves the child's trailing comments to the caller, which prints them with `trailing_comments(child_node)`.
|
||||
- **Prettier's `willPrintOwnComments`**: a few nodes print their own leading and trailing comments (a JSX element inside parentheses, the members of a union). The wrappers skip those.
|
||||
- **Every comment is printed exactly once.** `Comments::mark_printed` returns false for a comment that has been printed.
|
||||
- **A comment that is left over does not cost the file.** Prettier throws (`ensureAllCommentsPrinted`). Here the file is formatted once more, and the statement or the class member around the comment writes it after itself (`ir/run.rs`, `write_comments_without_printer`). The comment moves, so it is still a bug: the owner has no printer. `--debug-unprinted-comments` lists what the first pass left over, and the tests want nothing there. Code in a template has no second pass and stays as written.
|
||||
- **A comment that waits for the end of the line** (`line_suffix`) is written at the next line break, wherever that is. `Comment::fmt` puts a `line_suffix_boundary` before a block comment that it writes with line breaks of its own (`/**⏎ * a⏎ */`), so that it is not inside that comment.
|
||||
- **Formatting something only to look at it**: `f.speculate_will_break(&content)` forgets which comments were printed meanwhile. The result of `memoized().inspect(f)` or `f.intern` has to be used, not thrown away.
|
||||
- **Questions by offset are for layout only**: `has_comment_in_span`, `comments_in_span`, `has_comment_before(token_start)`, `comments_directly_before(token_start)`. Their answers do not depend on what has been printed. Never use them to pick the comments to print.
|
||||
- `/** @type {T} */ (e)`: `utils/typecast.rs` keeps the parentheses.
|
||||
|
||||
### `f.is_quiet()`
|
||||
|
||||
Most nodes have no comment anywhere near. While a node is written whose span has no unprinted comment and none behind it on the same line, `f.is_quiet()` is true, and the wrappers in `format.rs` skip all of the above. Use it to skip work that is only about comments:
|
||||
Most nodes have no comment anywhere near them. `f.is_quiet()` is true while a node is written that has no comment inside its span, and no node inside it that owns a comment from elsewhere. The wrappers in `format.rs` then skip everything above. Use it to skip work that is only about comments:
|
||||
|
||||
```rust
|
||||
if !f.is_quiet() { let comments = f.comments().comments_before_character(start, b'='); .. }
|
||||
if !f.is_quiet() && !f.comments().of_node(node).dangling.is_empty() { .. }
|
||||
```
|
||||
|
||||
Calling the `Comments` methods without the check is always correct: they are cheap when the next comment is far away.
|
||||
A node's own leading and trailing comments are outside its span, so ask for those before entering the node. Calling the `Comments` methods without the check is always correct: `of_node` tests one bit for a node that owns nothing.
|
||||
|
||||
### Debugging
|
||||
|
||||
In builds that have the test runner (debug and canary):
|
||||
|
||||
```sh
|
||||
bun bd fmt --debug-comments out a.ts # out/a.ts.txt: the owner of each comment, and what the parser recorded
|
||||
bun bd fmt --debug-comments-oxfmt out a.ts # the same in the oxfmt flavor
|
||||
bun bd fmt --debug-unprinted-comments out a.ts # the comments that no printer printed
|
||||
```
|
||||
|
||||
## Performance rules
|
||||
|
||||
The goal is to be faster per core than oxfmt and Biome.
|
||||
|
||||
- **Measure on an LTO build, and measure time too.** Without LTO, what is inlined changes with unrelated code, which is worth 0.2% either way. A count of instructions does not see a large `memmove`: it is one instruction.
|
||||
|
||||
- No heap allocation per node. `SmallVec` for lists that are small in practice, `f.take_vec()` for element buffers.
|
||||
- No `String`, no `format!`, no `to_vec()` on the path of ordinary code. Text is `&[u8]`.
|
||||
- `format_with` closures and `format_args!` are static dispatch and cost nothing. `&dyn Format` only where oxc needs it (`best_fitting!`).
|
||||
@@ -337,4 +388,4 @@ oxfmt's option, which is made of `prettier-plugin-tailwindcss`: the classes of T
|
||||
- `export` is a modifier of the declaration. `stmt.as_ast_nodes()` is the `ExportNamedDeclaration`, `FormatDeclaration(stmt)` the declaration in it, `stmt.span_without_export()` its span.
|
||||
- Methods: `member.func()` / `prop.func()` is the function, whose span starts at the parameters in ESTree.
|
||||
- Elements of `implements` and of the `extends` of an interface are `TypeKind::Ref`, or `TypeKind::Heritage` if they are not names.
|
||||
- `x!!` is one `ExprKind::NonNull(x)`. `FormatNonNullMarks(e)` writes every `!` and the comments between them.
|
||||
- `x!!` is one `ExprKind::NonNull(x)`. `FormatNonNullMarks(e)` writes every `!`.
|
||||
@@ -88,6 +88,9 @@ const EXCLUDED: &[(&str, &str)] = &[
|
||||
/// Formatting their output again changes it, in Prettier too: `unstableTests` of
|
||||
/// `tests/config/format-test/failed-format-tests.js`.
|
||||
const UNSTABLE: &[&str] = &[
|
||||
"flow/hook/declare-hook.js",
|
||||
"flow/hook/hook-type-annotation.js",
|
||||
"flow/comments/type_annotations.js",
|
||||
"js/ignore/semi/class-expression-decorator.js",
|
||||
"js/ignore/semi/head-ignored.js",
|
||||
"js/comments/return-statement.js",
|
||||
|
||||
+1
-11
@@ -1584,16 +1584,6 @@ pub(crate) fn around_jsx_children<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// What is written from here on is the node at `span`, up to [`extend_node`]. For a node that writes
|
||||
/// the comments around it itself.
|
||||
#[inline]
|
||||
pub(crate) fn enter_node(span: Span, f: &mut Formatter<'_>) {
|
||||
if f.context().cursor.is_active() {
|
||||
let cursor = f.context().cursor;
|
||||
cursor.enter(span, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// What has been written since the node at `span` is part of what Prettier prints for that node:
|
||||
/// the `;` after a member.
|
||||
#[inline]
|
||||
@@ -1844,7 +1834,7 @@ pub(crate) fn format_with<'a>(
|
||||
scratch: &mut Scratch,
|
||||
out: &mut Vec<u8>,
|
||||
is_aligned: bool,
|
||||
write: impl FnOnce(&'a File<'a>, &mut Formatter<'a>),
|
||||
write: impl Fn(&'a File<'a>, &mut Formatter<'a>),
|
||||
) -> Result<Option<u32>, FormatError> {
|
||||
let source = file.text();
|
||||
let offset =
|
||||
|
||||
+19
-100
@@ -12,117 +12,36 @@ fn is_identifier(e: Expr<'_>, names: &[&[u8]]) -> bool {
|
||||
matches!(e.kind(), ExprKind::Ident(_)) && names.contains(&e.text())
|
||||
}
|
||||
|
||||
/// Whether a block comment with exactly `language` in it, like ` GraphQL `, is right before `start`, with nothing
|
||||
/// but other comments between. `is_statement`: a statement starts at `start`, and empty statements before it are
|
||||
/// nothing either.
|
||||
fn follows_language_comment(
|
||||
start: u32,
|
||||
is_statement: bool,
|
||||
language: &[u8],
|
||||
f: &Formatter<'_>,
|
||||
) -> bool {
|
||||
let source = f.source_text();
|
||||
let unprinted = f.comments().comments_before(start);
|
||||
let (mut position, mut has_empty_statement) = (start, false);
|
||||
let mut comments = unprinted
|
||||
.iter()
|
||||
.rev()
|
||||
.chain(f.comments().printed_comments().iter().rev());
|
||||
while let Some(comment) = comments.next() {
|
||||
if comment.span.end > position {
|
||||
return false;
|
||||
}
|
||||
let between = source.text_for(&Span::after(comment.span, position));
|
||||
if !bun_core::strings::is_all_js_whitespace(between) {
|
||||
if !is_statement
|
||||
|| !bun_core::strings::split(between, b";")
|
||||
.all(bun_core::strings::is_all_js_whitespace)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
has_empty_statement = true;
|
||||
}
|
||||
position = comment.span.start;
|
||||
if !comment.is_block() || source.text_for(&comment.content_span()) != language {
|
||||
continue;
|
||||
}
|
||||
if !has_empty_statement {
|
||||
return true;
|
||||
}
|
||||
// Behind a statement on the same line, it trails that. So do the comments before it.
|
||||
let mut first = comment;
|
||||
for previous in comments {
|
||||
if first.preceded_by_newline()
|
||||
|| !bun_core::strings::is_all_js_whitespace(
|
||||
source.text_for(&previous.span.between(first.span)),
|
||||
)
|
||||
{
|
||||
break;
|
||||
}
|
||||
first = previous;
|
||||
}
|
||||
return first.preceded_by_newline()
|
||||
|| matches!(
|
||||
bun_core::strings::trim_js_whitespace(
|
||||
source.text_for(&Span::before(0, first.span))
|
||||
),
|
||||
[] | [.., b'{']
|
||||
);
|
||||
}
|
||||
false
|
||||
/// The nodes whose leading comments can name the language of the template `e`: the template itself, and an expression
|
||||
/// statement or an `as const` directly around it.
|
||||
fn nodes_with_language_comment<'a>(e: Expr<'a>, parent: AstNodes<'a>) -> [Option<AstNodes<'a>>; 2] {
|
||||
let parent_counts = match parent {
|
||||
AstNodes::ExpressionStatement(_) => true,
|
||||
AstNodes::TSAsExpression(cast) => matches!(cast.kind(), ExprKind::AsConst(_)),
|
||||
_ => false,
|
||||
};
|
||||
[Some(e.as_ast_nodes()), parent_counts.then_some(parent)]
|
||||
}
|
||||
|
||||
/// Whether the comment leads `e`. A comment leads the outermost of the nodes that start behind it, and
|
||||
/// parentheses are not nodes.
|
||||
fn is_led_by_language_comment<'a>(
|
||||
e: Expr<'a>,
|
||||
parent: AstNodes<'a>,
|
||||
language: &[u8],
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
let outer_start = e.outer_span().start;
|
||||
(e.is_parenthesized() && follows_language_comment(e.span().start, false, language, f))
|
||||
|| (parent.span().start != outer_start
|
||||
&& follows_language_comment(outer_start, false, language, f))
|
||||
}
|
||||
|
||||
/// Whether a comment is before `e`, with nothing but white space, `(`, `;` and comments between. No other comment says what
|
||||
/// language the template `e` is in.
|
||||
/// Whether a comment could name the language of the template `e`. A quick check before the embedded languages are tried.
|
||||
pub(crate) fn is_behind_comment<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let start = e.span().start;
|
||||
let comments = f.comments();
|
||||
let nearest = comments
|
||||
.comments_before(start)
|
||||
.last()
|
||||
.or_else(|| comments.printed_comments().last());
|
||||
nearest.is_some_and(|comment| {
|
||||
comment.span.end > start
|
||||
|| f.source_text()
|
||||
.text_for(&Span::after(comment.span, start))
|
||||
.iter()
|
||||
.all(|byte| matches!(byte, b'(' | b';' | 0x80..) || byte.is_ascii_whitespace())
|
||||
})
|
||||
(nodes_with_language_comment(e, e.ast_parent())
|
||||
.into_iter()
|
||||
.flatten())
|
||||
.any(|node| !f.comments().of_node(node).leading.is_empty())
|
||||
}
|
||||
|
||||
/// Prettier's `hasLanguageComment`. `language`: what is between the `/*` and the `*/`.
|
||||
/// Prettier's `hasLanguageComment`: `/* GraphQL */ `..``. `language` is the text between the `/*` and the `*/`.
|
||||
pub(crate) fn has_language_comment<'a>(
|
||||
e: Expr<'a>,
|
||||
parent: AstNodes<'a>,
|
||||
language: &[u8],
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
is_behind_comment(e, f)
|
||||
&& (is_led_by_language_comment(e, parent, language, f)
|
||||
|| match parent {
|
||||
AstNodes::ExpressionStatement(_) => {
|
||||
follows_language_comment(parent.span().start, true, language, f)
|
||||
}
|
||||
AstNodes::TSAsExpression(cast) => {
|
||||
matches!(cast.kind(), ExprKind::AsConst(_))
|
||||
&& is_led_by_language_comment(cast, parent.parent(), language, f)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
(nodes_with_language_comment(e, parent).into_iter().flatten()).any(|node| {
|
||||
(f.comments().of_node(node).leading.iter())
|
||||
.any(|comment| comment.is_block() && comment.content() == language)
|
||||
})
|
||||
}
|
||||
|
||||
/// Prettier's `isEmbedGraphQL`. `e`: a template.
|
||||
|
||||
@@ -150,7 +150,7 @@ fn format_normalized(
|
||||
// Prettier's `addAlignmentToDoc`. The line break makes the indentation take effect.
|
||||
let tab_width = options.indent_width.value().max(1) as usize;
|
||||
let (levels, spaces) = (alignment / tab_width, (alignment % tab_width) as u32);
|
||||
let context = JsFormatContext::without_file(text, options.clone(), &[]);
|
||||
let context = JsFormatContext::without_file(options.clone(), &crate::js::comments::NO_COMMENTS);
|
||||
let root = crate::ir::run::write_with(context, text, document, |f| {
|
||||
if spaces > 0 {
|
||||
f.write_element(FormatElement::Tag(Tag::StartAlign(Align(spaces))));
|
||||
|
||||
@@ -361,7 +361,7 @@ pub(crate) fn write_template<'a>(
|
||||
let indent_level = indent_level_in_source(e, &options, f) + u32::from(line.is_some());
|
||||
let slot = f.start_capture();
|
||||
// The code in the text is written with the options of the formatter.
|
||||
let context = JsFormatContext::without_file(&text, options.clone(), &[]);
|
||||
let context = JsFormatContext::without_file(options.clone(), &crate::js::comments::NO_COMMENTS);
|
||||
// Of the name of the file, no more than this is asked.
|
||||
let path = options.is_in_html_file.then_some(&b".html"[..]);
|
||||
let written = f.write_embedded(context, &text, |f| {
|
||||
|
||||
+28
-43
@@ -5,7 +5,8 @@
|
||||
|
||||
use super::map_strings::{MapString, write_mapped};
|
||||
use crate::ir::element::{Interned, TextWidth};
|
||||
use crate::js::comments::{ParsedBy, collect};
|
||||
use crate::js::ast_nodes::node_as_ast_nodes;
|
||||
use crate::js::comments::{self, NO_COMMENTS};
|
||||
use crate::js::context::JsFormatContext;
|
||||
use crate::js::print::program::{FormatStatements, write_hashbang};
|
||||
use crate::js::sort_imports::{SortImports, sorted_text};
|
||||
@@ -225,24 +226,16 @@ pub(crate) fn with_file(
|
||||
{
|
||||
return;
|
||||
}
|
||||
let comments = file.extension(|| {
|
||||
let mut comments = Vec::new();
|
||||
let parsed_by = match options.in_html.has_tree_of_babel {
|
||||
true => ParsedBy::Babel,
|
||||
false => ParsedBy::WhatItsNameSays,
|
||||
};
|
||||
collect(file, options.flavor, parsed_by, &mut comments);
|
||||
comments
|
||||
});
|
||||
let comments = comments.map_or(&[][..], |comments: &Vec<Comment>| comments);
|
||||
let comments = comments::of_file(file, &options);
|
||||
// Only in what is known to be a script are `<!--` and `-->` comments to Babel.
|
||||
let is_html_like = |comment: &Comment| {
|
||||
let is_html_like = |comment: Comment<'_>| {
|
||||
matches!(
|
||||
file.text().get(comment.span.start as usize),
|
||||
file.text().get(comment.span().start as usize),
|
||||
Some(b'<' | b'-')
|
||||
)
|
||||
};
|
||||
if source_type != SourceType::Script && comments.iter().any(is_html_like) {
|
||||
if source_type != SourceType::Script && file.comments_by_id().any(is_html_like)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(sorted) = sorts_text.and_then(|how| sorted_text(file, how)) {
|
||||
@@ -250,7 +243,14 @@ pub(crate) fn with_file(
|
||||
return;
|
||||
}
|
||||
let context = JsFormatContext::new(file, options.clone(), comments);
|
||||
let is_written = f.write_embedded(context, file.text(), |f| write(file, f));
|
||||
let is_written = match file.comment_count() {
|
||||
0 => f.write_embedded(context, file.text(), |f| write(file, f)),
|
||||
// Prettier's `ensureAllCommentsPrinted`. It throws, and the code stays as it is written:
|
||||
// `v-slot="/* comment */"`
|
||||
_ => f.try_write_embedded(context, file.text(), |f| {
|
||||
write(file, f) && f.comments().unprinted().next().is_none()
|
||||
}),
|
||||
};
|
||||
result = Attempt::Written(is_written);
|
||||
});
|
||||
result
|
||||
@@ -468,33 +468,20 @@ pub(crate) fn paths_of(syntax: Syntax, code: &[u8]) -> &'static [&'static [u8]]
|
||||
|
||||
/// What Prettier makes of a `Program`, without the line break at the end.
|
||||
fn write_statements<'b>(file: &'b File<'b>, f: &mut Formatter<'b>) {
|
||||
// Nothing that a comment could belong to. Without a statement at all the comments keep the empty lines between
|
||||
// them, as in `write_program`, which counts on being at the start of a line.
|
||||
let is_all_empty = file
|
||||
.body()
|
||||
.iter()
|
||||
.all(|it| matches!(it.kind(), StmtKind::Empty));
|
||||
write_hashbang(
|
||||
is_all_empty && f.comments().unprinted_comments().is_empty(),
|
||||
let program = node_as_ast_nodes(Node::File(file));
|
||||
let owned = f.comments().of_node(program);
|
||||
let has_statements = (file.body().iter()).any(|it| !matches!(it.kind(), StmtKind::Empty));
|
||||
let is_hashbang_last = !has_statements && owned.leading.is_empty() && owned.dangling.is_empty();
|
||||
write_hashbang(is_hashbang_last, f);
|
||||
// The program only owns comments if there is no statement to attach them to.
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(program),
|
||||
FormatStatements(file.body()),
|
||||
dangling_comments(program)
|
||||
]
|
||||
);
|
||||
if is_all_empty && (!file.body().is_empty() || f.options().in_html.is_in_attribute) {
|
||||
let comments = f.comments().unprinted_comments();
|
||||
let indent = DanglingIndentMode::None;
|
||||
return write!(f, FormatDanglingComments::Comments { comments, indent });
|
||||
}
|
||||
write!(f, FormatStatements(file.body()));
|
||||
let mut rest = f.comments().unprinted_comments();
|
||||
// The empty lines before the first thing in a script go.
|
||||
if let Some((first, others)) = rest.split_first()
|
||||
&& (file.text().get(..first.span.start as usize))
|
||||
.is_some_and(|before| before.trim_ascii().is_empty())
|
||||
{
|
||||
let (comments, indent) = (std::slice::from_ref(first), DanglingIndentMode::None);
|
||||
write!(f, FormatDanglingComments::Comments { comments, indent });
|
||||
rest = others;
|
||||
}
|
||||
write!(f, FormatTrailingComments::Comments(rest));
|
||||
}
|
||||
|
||||
/// `textToDoc(code, { parser })` for a program. `flavor`: whose output it is. Returns whether it has been written.
|
||||
@@ -726,8 +713,6 @@ pub(crate) fn write_expression(
|
||||
let should_hug = hug.of_expression(e, in_html.root);
|
||||
let content = format_with(|f| {
|
||||
write!(f, e);
|
||||
let rest = f.comments().unprinted_comments();
|
||||
write!(f, FormatTrailingComments::Comments(rest));
|
||||
});
|
||||
write_hugged(should_hug, &content, f);
|
||||
true
|
||||
@@ -803,7 +788,7 @@ pub(crate) fn write_angular_expression(
|
||||
}
|
||||
write_rest(f);
|
||||
});
|
||||
let context = JsFormatContext::new(file, options.clone(), &[]);
|
||||
let context = JsFormatContext::new(file, options.clone(), &NO_COMMENTS);
|
||||
f.write_embedded(context, source, |f| write_hugged(should_hug, &content, f));
|
||||
is_written = true;
|
||||
});
|
||||
|
||||
@@ -431,7 +431,8 @@ fn format_angular_expression(
|
||||
if strings::trim_js_whitespace(&text).is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut context = JsFormatContext::without_file(&text, options.clone(), &[]);
|
||||
let mut context =
|
||||
JsFormatContext::without_file(options.clone(), &crate::js::comments::NO_COMMENTS);
|
||||
context.parse_javascript = parse_javascript;
|
||||
let mut is_written = false;
|
||||
let document = crate::ir::run::write_with(context, &text, &mut scratch.document, |f| {
|
||||
@@ -571,7 +572,8 @@ pub fn format_with(
|
||||
};
|
||||
let path = Some(path).filter(|path| !path.is_empty());
|
||||
let mut result = Ok(None);
|
||||
let mut context = JsFormatContext::without_file(&text, options.clone(), &[]);
|
||||
let mut context =
|
||||
JsFormatContext::without_file(options.clone(), &crate::js::comments::NO_COMMENTS);
|
||||
context.parse_javascript = parse_javascript;
|
||||
let root = crate::ir::run::write_with(context, &text, &mut scratch.document, |f| {
|
||||
result = write_document_with_cursor(
|
||||
|
||||
@@ -533,7 +533,8 @@ impl<Content: ?Sized> IfGroupBreaks<'_, Content> {
|
||||
}
|
||||
|
||||
impl<'a, Content: Format<'a> + ?Sized> Format<'a> for IfGroupBreaks<'_, Content> {
|
||||
#[inline]
|
||||
/// A trailing comma is written for every call, array and object. Inlined, this is one write of a constant element.
|
||||
#[inline(always)]
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if let (Some(text), None, PrintMode::Expanded) =
|
||||
(self.content.as_token(), self.group_id, self.mode)
|
||||
@@ -541,6 +542,16 @@ impl<'a, Content: Format<'a> + ?Sized> Format<'a> for IfGroupBreaks<'_, Content>
|
||||
{
|
||||
return f.write_element(FormatElement::TokenIfBreaks(token));
|
||||
}
|
||||
self.write_conditional_content(f);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Content: ?Sized> IfGroupBreaks<'_, Content> {
|
||||
#[inline(never)]
|
||||
fn write_conditional_content<'a>(&self, f: &mut Formatter<'a>)
|
||||
where
|
||||
Content: Format<'a>,
|
||||
{
|
||||
let condition = Condition::new(self.mode).with_group_id(self.group_id);
|
||||
f.write_element(FormatElement::Tag(Tag::StartConditionalContent(condition)));
|
||||
self.content.fmt(f);
|
||||
|
||||
@@ -512,6 +512,26 @@ impl<'a> Formatter<'a> {
|
||||
result
|
||||
}
|
||||
|
||||
/// The same, but what `write` has written only becomes part of the document if `write` returns true.
|
||||
pub(crate) fn try_write_embedded<'b>(
|
||||
&mut self,
|
||||
context: JsFormatContext<'b>,
|
||||
source: &'b [u8],
|
||||
write: impl FnOnce(&mut Formatter<'b>) -> bool,
|
||||
) -> bool {
|
||||
let first = self.storage.pool.len();
|
||||
let mut inner = self.start_embedded(context, source);
|
||||
let slot = inner.start_capture();
|
||||
let is_written = write(&mut inner);
|
||||
if let Some(content) = inner.end_intern(slot)
|
||||
&& is_written
|
||||
{
|
||||
inner.write_element(content);
|
||||
}
|
||||
self.end_embedded(inner, first);
|
||||
is_written
|
||||
}
|
||||
|
||||
/// A formatter for `source` that has what this one has written.
|
||||
fn start_embedded<'b>(
|
||||
&mut self,
|
||||
|
||||
+67
-19
@@ -3,8 +3,9 @@
|
||||
use super::element::Interned;
|
||||
use super::formatter::Formatter;
|
||||
use crate::cursor::CursorRegion;
|
||||
use crate::js::comments::{ParsedBy, collect};
|
||||
use crate::js::comments;
|
||||
use crate::js::context::JsFormatContext;
|
||||
use crate::options::DebugComments;
|
||||
use crate::{FormatError, FormatOptions};
|
||||
use bun_lint::ast::File;
|
||||
|
||||
@@ -13,20 +14,54 @@ use bun_lint::ast::File;
|
||||
pub struct Scratch {
|
||||
formatter: super::formatter::FormatterBuffers,
|
||||
printer: super::printer::PrinterBuffers,
|
||||
/// A line for each comment of the last file that no printer has printed.
|
||||
unprinted_comments: Vec<u8>,
|
||||
/// Their ids, for the second pass.
|
||||
unprinted_comment_ids: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Appends the formatted text of `file` to `out`: the document that `write` writes. Returns where the ends of `cursor` are
|
||||
/// in what is appended.
|
||||
///
|
||||
/// Every comment is owned by a node or a name, and whoever writes that has to write the comment. Prettier throws if a comment
|
||||
/// is left over. Here the file is formatted once more, and the statement or the class member that such a comment is in writes
|
||||
/// it after itself: the comment moves, and the file is formatted. `--debug-unprinted-comments` lists these comments.
|
||||
pub(crate) fn format_with_marks<'a>(
|
||||
file: &'a File<'a>,
|
||||
options: &FormatOptions,
|
||||
cursor: CursorRegion,
|
||||
scratch: &mut Scratch,
|
||||
out: &mut Vec<u8>,
|
||||
write: impl FnOnce(&'a File<'a>, &mut Formatter<'a>),
|
||||
write: impl Fn(&'a File<'a>, &mut Formatter<'a>),
|
||||
) -> Result<[Option<u32>; 2], FormatError> {
|
||||
let root = write_document(file, options, cursor, scratch, write)?;
|
||||
print(root, file.text(), options, scratch, out)?;
|
||||
if options.debug_comments == DebugComments::Owners {
|
||||
let has_tree_of_babel = crate::js::context::has_tree_of_babel(file, options);
|
||||
let attach_options = crate::js::attach::Options {
|
||||
has_tree_of_babel,
|
||||
experimental_ternaries: options.experimental_ternaries,
|
||||
flavor: options.flavor,
|
||||
};
|
||||
let cast_parens = comments::cast_parens(file, options.flavor, has_tree_of_babel);
|
||||
crate::js::attach::list(file, &cast_parens, attach_options, out);
|
||||
return Ok([None; 2]);
|
||||
}
|
||||
let mut root = write_document(file, options, cursor, scratch, Vec::new(), &write);
|
||||
if options.debug_comments == DebugComments::Unprinted {
|
||||
out.append(&mut scratch.unprinted_comments);
|
||||
return Ok([None; 2]);
|
||||
}
|
||||
if matches!(root, Err(FormatError::InvalidDocument)) {
|
||||
let comments_without_printer = std::mem::take(&mut scratch.unprinted_comment_ids);
|
||||
root = write_document(
|
||||
file,
|
||||
options,
|
||||
cursor,
|
||||
scratch,
|
||||
comments_without_printer,
|
||||
&write,
|
||||
);
|
||||
}
|
||||
print(root?, file.text(), options, scratch, out)?;
|
||||
Ok(scratch.printer.marks)
|
||||
}
|
||||
|
||||
@@ -62,27 +97,16 @@ fn write_document<'a>(
|
||||
options: &FormatOptions,
|
||||
cursor: CursorRegion,
|
||||
scratch: &mut Scratch,
|
||||
write: impl FnOnce(&'a File<'a>, &mut Formatter<'a>),
|
||||
comments_without_printer: Vec<u32>,
|
||||
write: &impl Fn(&'a File<'a>, &mut Formatter<'a>),
|
||||
) -> Result<Interned, FormatError> {
|
||||
if file.has_parse_errors() {
|
||||
return Err(FormatError::SyntaxError);
|
||||
}
|
||||
// The file keeps them, so that they live as long as the handles.
|
||||
let comments = file.extension(|| {
|
||||
let mut comments = Vec::new();
|
||||
collect(
|
||||
file,
|
||||
options.flavor,
|
||||
ParsedBy::WhatItsNameSays,
|
||||
&mut comments,
|
||||
);
|
||||
comments
|
||||
});
|
||||
let comments = comments.map_or(&[][..], |comments: &Vec<crate::js::comments::Comment>| {
|
||||
comments
|
||||
});
|
||||
let comments = comments::of_file(file, options);
|
||||
let mut context = JsFormatContext::new(file, options.clone(), comments);
|
||||
context.cursor = cursor;
|
||||
context.comments_without_printer = comments_without_printer;
|
||||
write_with(context, file.text(), scratch, |f| write(file, f))
|
||||
}
|
||||
|
||||
@@ -103,10 +127,34 @@ pub(crate) fn write_with<'a>(
|
||||
/// Gives the vectors of `formatter` back to `scratch`. Returns where the document is.
|
||||
fn finish(formatter: Formatter<'_>, scratch: &mut Scratch) -> Result<Interned, FormatError> {
|
||||
let ran_out_of_stack = formatter.context().ran_out_of_stack;
|
||||
scratch.unprinted_comments.clear();
|
||||
scratch.unprinted_comment_ids.clear();
|
||||
for comment in formatter.context().comments().unprinted() {
|
||||
use std::io::Write;
|
||||
scratch.unprinted_comment_ids.push(comment.id());
|
||||
let Some(attachment) = formatter.context().comments().attachment(comment) else {
|
||||
continue;
|
||||
};
|
||||
let owner = (crate::js::ast_nodes::AST_KIND_NAMES)
|
||||
.get(attachment.owner_kind as usize)
|
||||
.unwrap_or(&"Name");
|
||||
let _ = writeln!(
|
||||
scratch.unprinted_comments,
|
||||
"{} {} {:?} {owner} {:?}",
|
||||
comment.span().start,
|
||||
comment.span().end,
|
||||
attachment.placement,
|
||||
attachment.marker
|
||||
);
|
||||
}
|
||||
let (root, buffers) = formatter.finish();
|
||||
scratch.formatter = buffers;
|
||||
if ran_out_of_stack {
|
||||
return Err(FormatError::NestedTooDeeply);
|
||||
}
|
||||
// Prettier's `ensureAllCommentsPrinted`: a file is left alone rather than written without one of its comments.
|
||||
if !scratch.unprinted_comment_ids.is_empty() {
|
||||
return Err(FormatError::InvalidDocument);
|
||||
}
|
||||
Ok(root)
|
||||
}
|
||||
+127
-11
@@ -25,7 +25,6 @@ use bun_lint::ast::{
|
||||
PropKind, Stmt, StmtKind, StmtTag, TupleElem, TypeKind, TypeNode, TypeParam, UnOp, VarDecl,
|
||||
};
|
||||
use bun_lint::span::{Span, Spanned};
|
||||
use bun_lint::tokens::skip_trivia_back;
|
||||
use bun_sema::hir::ExprTag;
|
||||
|
||||
/// An expression statement, or the body of an arrow function that is an expression, which oxc has
|
||||
@@ -65,6 +64,15 @@ impl<'a> ExpressionStatement<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ExpressionStatement<'a>> for Node<'a> {
|
||||
fn from(statement: ExpressionStatement<'a>) -> Node<'a> {
|
||||
match statement {
|
||||
ExpressionStatement::Stmt(statement) => Node::Stmt(statement),
|
||||
ExpressionStatement::ArrowBody(func) => Node::Func(func),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! ast_nodes {
|
||||
($($payload:ty { $($variant:ident)* })*) => {
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
@@ -72,6 +80,36 @@ macro_rules! ast_nodes {
|
||||
Program(Program<'a>),
|
||||
$($($variant($payload),)*)*
|
||||
}
|
||||
|
||||
/// Which variant of [`AstNodes`].
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum AstKind {
|
||||
Program,
|
||||
$($($variant,)*)*
|
||||
}
|
||||
|
||||
/// The names of the variants of [`AstKind`], by their number.
|
||||
pub(crate) const AST_KIND_NAMES: &[&str] = &["Program", $($(stringify!($variant),)*)*];
|
||||
|
||||
impl<'a> AstNodes<'a> {
|
||||
#[inline]
|
||||
pub(crate) fn kind(self) -> AstKind {
|
||||
match self {
|
||||
AstNodes::Program(_) => AstKind::Program,
|
||||
$($(AstNodes::$variant(_) => AstKind::$variant,)*)*
|
||||
}
|
||||
}
|
||||
|
||||
/// The node of `bun_lint::ast` that it is made of.
|
||||
#[inline]
|
||||
pub(crate) fn raw(self) -> Node<'a> {
|
||||
match self {
|
||||
AstNodes::Program(file) => Node::File(file.0),
|
||||
$($(AstNodes::$variant(it) => Node::from(it),)*)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -199,7 +237,7 @@ fn is_comma(e: Expr<'_>) -> bool {
|
||||
}
|
||||
|
||||
/// Whether `e` is the `a, b` of `a, b, c`, which is not a node of its own in ESTree.
|
||||
fn is_inner_comma(e: Expr<'_>) -> bool {
|
||||
pub(crate) fn is_inner_comma(e: Expr<'_>) -> bool {
|
||||
is_comma(e)
|
||||
&& !e.is_parenthesized()
|
||||
&& matches!(e.parent(), Node::Expr(parent) if is_comma(parent) && parent.left() == Some(e))
|
||||
@@ -399,7 +437,7 @@ fn parent_of_expr<'a>(e: Expr<'a>, above: Level) -> AstNodes<'a> {
|
||||
_ => inner_of_stmt(statement),
|
||||
},
|
||||
Node::VarDecl(declaration) => declaration.as_ast_nodes(),
|
||||
Node::Param(param) if param.default() == Some(e) => param.as_ast_nodes(),
|
||||
Node::Param(param) if param.default() == Some(e) => parent_in_parameter(param),
|
||||
Node::Param(_) => N::Decorator(e),
|
||||
Node::PatProp(prop) if prop.default() == Some(e) => {
|
||||
N::AssignmentPattern(Node::PatProp(prop))
|
||||
@@ -407,7 +445,9 @@ fn parent_of_expr<'a>(e: Expr<'a>, above: Level) -> AstNodes<'a> {
|
||||
Node::PatProp(prop) => N::BindingProperty(prop),
|
||||
Node::PatElem(element) => N::AssignmentPattern(Node::PatElem(element)),
|
||||
Node::Prop(prop) => prop.as_ast_nodes(),
|
||||
Node::Member(member) if member.decorators().any(|it| it == e) => N::Decorator(e),
|
||||
Node::Member(member) if member.modifiers().around(e.span().start).is_some() => {
|
||||
N::Decorator(e)
|
||||
}
|
||||
Node::Member(member) => member.as_ast_nodes(),
|
||||
Node::Func(func) => N::ExpressionStatement(ExpressionStatement::ArrowBody(func)),
|
||||
Node::EnumMember(member) => N::TSEnumMember(member),
|
||||
@@ -512,7 +552,7 @@ impl<'a> AsAstNodes<'a> for VarDecl<'a> {
|
||||
}
|
||||
|
||||
/// The statement without the `export` around it.
|
||||
fn inner_of_stmt<'a>(statement: Stmt<'a>) -> AstNodes<'a> {
|
||||
pub(crate) fn inner_of_stmt<'a>(statement: Stmt<'a>) -> AstNodes<'a> {
|
||||
use AstNodes as N;
|
||||
match statement.tag() {
|
||||
StmtTag::Var => return N::VariableDeclaration(statement),
|
||||
@@ -730,11 +770,21 @@ impl<'a> AsAstNodes<'a> for Pat<'a> {
|
||||
AstNodes::AssignmentPattern(Node::PatElem(element))
|
||||
}
|
||||
Node::PatElem(element) => node_as_ast_nodes(element.parent()),
|
||||
Node::Param(param) => parent_in_parameter(param),
|
||||
other => node_as_ast_nodes(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What the pattern, the type annotation and the default value of `param` are in. `constructor(private a = 1) {}` is two nodes
|
||||
/// in ESTree: a `TSParameterProperty`, which is the `FormalParameter` here, and the `AssignmentPattern` `a = 1` in it.
|
||||
fn parent_in_parameter(param: Param<'_>) -> AstNodes<'_> {
|
||||
match param.is_parameter_property() && param.default().is_some() {
|
||||
true => AstNodes::AssignmentPattern(Node::Param(param)),
|
||||
false => param.as_ast_nodes(),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a node of `bun_lint::ast` is. A statement is with its `export`.
|
||||
pub(crate) fn node_as_ast_nodes<'a>(node: Node<'a>) -> AstNodes<'a> {
|
||||
use AstNodes as N;
|
||||
@@ -778,6 +828,26 @@ fn parent_of_owner<'a>(owner: Node<'a>) -> AstNodes<'a> {
|
||||
}
|
||||
|
||||
impl<'a> AstNodes<'a> {
|
||||
/// The expression that it is, if it is one: what parentheses can be written around.
|
||||
pub(crate) fn as_expression(self) -> Option<Expr<'a>> {
|
||||
use AstNodes as N;
|
||||
match self {
|
||||
N::Function(func) | N::ArrowFunctionExpression(func) => func.owner().as_expr(),
|
||||
N::Class(class) => class.owner().as_expr(),
|
||||
N::SpreadElement(_)
|
||||
| N::JSXExpressionContainer(_)
|
||||
| N::JSXSpreadChild(_)
|
||||
| N::JSXOpeningElement(_)
|
||||
| N::Decorator(_)
|
||||
| N::ChainExpression(_)
|
||||
| N::AssignmentTargetRest(_) => None,
|
||||
_ => match self.raw() {
|
||||
Node::Expr(e) => Some(e),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// What it is directly in. `Program` is in itself.
|
||||
pub(crate) fn parent(self) -> AstNodes<'a> {
|
||||
use AstNodes as N;
|
||||
@@ -954,11 +1024,15 @@ impl<'a> AstNodes<'a> {
|
||||
}
|
||||
N::BindingProperty(prop) => node_as_ast_nodes(prop.parent()),
|
||||
N::AssignmentPattern(Node::PatProp(prop)) => N::BindingProperty(prop),
|
||||
N::AssignmentPattern(Node::Param(param)) => param.as_ast_nodes(),
|
||||
N::AssignmentPattern(node) | N::BindingRestElement(node) => {
|
||||
node_as_ast_nodes(node.parent())
|
||||
}
|
||||
|
||||
N::TSTypeAnnotation(ty) => node_as_ast_nodes(ty.parent()),
|
||||
N::TSTypeAnnotation(ty) => match ty.parent() {
|
||||
Node::Param(param) => parent_in_parameter(param),
|
||||
parent => node_as_ast_nodes(parent),
|
||||
},
|
||||
N::TSKeywordType(ty)
|
||||
| N::TSThisType(ty)
|
||||
| N::TSTypeReference(ty)
|
||||
@@ -1027,6 +1101,28 @@ impl<'a> AstNodes<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`AstNodes::span`], starting at Prettier's `locStart`. The decorators of a class can come before its `export`, and
|
||||
/// belong to the class and to the export: `@decorator export class A {}`.
|
||||
pub(crate) fn span_with_decorators(self) -> Span {
|
||||
use AstNodes as N;
|
||||
let span = self.span();
|
||||
let class = match self {
|
||||
N::Class(class) => Some(class),
|
||||
N::ExportNamedDeclaration(statement) | N::ExportDefaultDeclaration(statement) => {
|
||||
match statement.kind() {
|
||||
StmtKind::Class(class) => Some(class),
|
||||
StmtKind::ExportDefault(e) => e.as_class(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match class.and_then(|class| class.decorators().next()) {
|
||||
Some(first) => Span::new(N::Decorator(first).span().start.min(span.start), span.end),
|
||||
None => span,
|
||||
}
|
||||
}
|
||||
|
||||
/// The range that the node has in oxc.
|
||||
pub(crate) fn span(self) -> Span {
|
||||
use AstNodes as N;
|
||||
@@ -1040,9 +1136,14 @@ impl<'a> AstNodes<'a> {
|
||||
_ => e.span(),
|
||||
},
|
||||
N::Decorator(e) => {
|
||||
let outer = e.outer_span();
|
||||
let at = skip_trivia_back(e.file().text(), outer.start).saturating_sub(1);
|
||||
Span::new(at, outer.end)
|
||||
let modifiers = match e.parent() {
|
||||
Node::Class(class) => class.modifiers(),
|
||||
Node::Member(member) => member.modifiers(),
|
||||
Node::Param(param) => param.modifiers(),
|
||||
_ => return e.outer_span(),
|
||||
};
|
||||
(modifiers.around(e.span().start))
|
||||
.map_or_else(|| e.outer_span(), |modifier| modifier.span())
|
||||
}
|
||||
N::AssignmentTargetRest(e) => match (e.kind(), e.parent()) {
|
||||
(ExprKind::Spread(_), _) => e.span(),
|
||||
@@ -1096,9 +1197,13 @@ impl<'a> AstNodes<'a> {
|
||||
| N::AssignmentTargetWithDefault(e)
|
||||
| N::TypeCastExpression(e) => e.span(),
|
||||
N::AssignmentPattern(Node::PatProp(prop)) => match prop.default() {
|
||||
Some(default) => prop.value().span().to(default.span()),
|
||||
Some(default) => prop.value().span().to(default.outer_span()),
|
||||
None => prop.span(),
|
||||
},
|
||||
N::AssignmentPattern(Node::Param(param)) => match param.default() {
|
||||
Some(default) => param.pat().span().to(default.outer_span()),
|
||||
None => param.span(),
|
||||
},
|
||||
N::SpreadElement(node) | N::AssignmentPattern(node) | N::BindingRestElement(node) => {
|
||||
node.span()
|
||||
}
|
||||
@@ -1115,7 +1220,11 @@ impl<'a> AstNodes<'a> {
|
||||
let end = arrow_behind_parameters(func).unwrap_or(span.end);
|
||||
Span::new(span.start, end)
|
||||
}
|
||||
N::Class(class) => class.estree_span(),
|
||||
N::Class(class) => match class.owner() {
|
||||
// `estree_span` looks at every decorator for an `export`.
|
||||
Node::Stmt(statement) if !class.flags().contains(Flags::EXPORT) => statement.span(),
|
||||
_ => class.estree_span(),
|
||||
},
|
||||
N::ClassBody(class) => class.body_span(),
|
||||
N::FormalParameter(param)
|
||||
| N::FormalParameterRest(param)
|
||||
@@ -1196,6 +1305,13 @@ impl<'a> AstNodes<'a> {
|
||||
N::ExportSpecifier(spec) => spec.span(),
|
||||
N::BindingIdentifier(pat) | N::ObjectPattern(pat) | N::ArrayPattern(pat) => pat.span(),
|
||||
N::BindingProperty(prop) => prop.span(),
|
||||
// The `T` of `a is T` has no `:` to start at.
|
||||
N::TSTypeAnnotation(ty)
|
||||
if matches!(ty.parent(), Node::Type(parent)
|
||||
if matches!(parent.kind(), TypeKind::Predicate { .. })) =>
|
||||
{
|
||||
ty.outer_span()
|
||||
}
|
||||
N::TSTypeAnnotation(ty) => ty.annotation_span(),
|
||||
N::TSKeywordType(ty)
|
||||
| N::TSThisType(ty)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//! For the tests, which compare this with what Prettier does.
|
||||
|
||||
use super::surrounding_nodes::AttachContext;
|
||||
use super::{Marker, Options, Owner, Placement, attach};
|
||||
use crate::js::ast_nodes::AST_KIND_NAMES;
|
||||
use bun_lint::ast::comments::NodeOrName;
|
||||
use bun_lint::ast::{File, Node};
|
||||
use std::io::Write;
|
||||
|
||||
/// A line for each comment: where it is, and what owns it. And one for each node that is ignored by a comment that another
|
||||
/// node owns.
|
||||
pub(crate) fn list<'a>(
|
||||
file: &'a File<'a>,
|
||||
cast_parens: &[u32],
|
||||
options: Options,
|
||||
out: &mut Vec<u8>,
|
||||
) {
|
||||
let attached = attach(file, cast_parens, options);
|
||||
let cx = AttachContext::new(file, cast_parens, options);
|
||||
let describe = |owner: Owner<'a>| {
|
||||
let name = match owner {
|
||||
Owner::Node(node) => AST_KIND_NAMES.get(node.kind() as usize).copied(),
|
||||
Owner::Name(_) => Some("Name"),
|
||||
Owner::CastParens(..) => Some("ParenthesizedExpression"),
|
||||
};
|
||||
(name.unwrap_or_default(), cx.span_of(owner))
|
||||
};
|
||||
let mut lines: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||
for (comment, attachment) in file.comments_by_id().zip(&attached.attachments) {
|
||||
if attachment.marker == Marker::InIgnoredNode {
|
||||
continue;
|
||||
}
|
||||
let (name, span) = describe(attachment.owner);
|
||||
let placement = match attachment.placement {
|
||||
Placement::Leading => "L",
|
||||
Placement::Trailing => "T",
|
||||
Placement::Dangling => "D",
|
||||
};
|
||||
let marker = match attachment.marker {
|
||||
Marker::None | Marker::InIgnoredNode | Marker::ClosingFragment => "-",
|
||||
Marker::CommentBeforeArrow => "commentBeforeArrow",
|
||||
Marker::Implements => "implements",
|
||||
Marker::Extends => "extends",
|
||||
Marker::Mixins => "mixins",
|
||||
};
|
||||
let unignore = match attachment.is_unignored {
|
||||
true => " unignore",
|
||||
false => "",
|
||||
};
|
||||
let mut line = Vec::new();
|
||||
let _ = write!(
|
||||
line,
|
||||
"C {} {} {placement} {name} {} {} {marker}{unignore}",
|
||||
comment.span().start,
|
||||
comment.span().end,
|
||||
span.start,
|
||||
span.end,
|
||||
);
|
||||
lines.push((comment.span().start, line));
|
||||
}
|
||||
// The nodes that the parser recorded, to debug a wrong owner.
|
||||
for comment in file.comments_by_id() {
|
||||
let describe = |node: Option<NodeOrName<'a>>| match node {
|
||||
None => "-".to_owned(),
|
||||
Some(NodeOrName::Name(start)) => format!("name@{start}"),
|
||||
Some(NodeOrName::Node(node)) => {
|
||||
let kind = match node {
|
||||
Node::File(_) => "File",
|
||||
Node::Expr(_) => "Expr",
|
||||
Node::Stmt(_) => "Stmt",
|
||||
Node::Type(_) => "Type",
|
||||
Node::Pat(_) => "Pat",
|
||||
Node::PatProp(_) => "PatProp",
|
||||
Node::PatElem(_) => "PatElem",
|
||||
Node::Func(_) => "Func",
|
||||
Node::Param(_) => "Param",
|
||||
Node::TypeParam(_) => "TypeParam",
|
||||
Node::Class(_) => "Class",
|
||||
Node::Member(_) => "Member",
|
||||
Node::Prop(_) => "Prop",
|
||||
Node::VarDecl(_) => "VarDecl",
|
||||
Node::Case(_) => "Case",
|
||||
Node::EnumMember(_) => "EnumMember",
|
||||
Node::ImportSpec(_) => "ImportSpec",
|
||||
Node::ExportSpec(_) => "ExportSpec",
|
||||
Node::TupleElem(_) => "TupleElem",
|
||||
};
|
||||
format!("{kind}@{}..{}", node.span().start, node.span().end)
|
||||
}
|
||||
};
|
||||
let line = format!(
|
||||
"P {} tokens {}..{} enclosing {} preceding {} following {}",
|
||||
comment.span().start,
|
||||
comment.prev_token_end(),
|
||||
comment.next_token_start(),
|
||||
describe(comment.enclosing_node().map(NodeOrName::Node)),
|
||||
describe(comment.preceding_node()),
|
||||
describe(comment.following_node()),
|
||||
);
|
||||
lines.push((comment.span().start, line.into_bytes()));
|
||||
}
|
||||
for owner in &attached.ignored_nodes {
|
||||
let (name, span) = describe(*owner);
|
||||
let mut line = Vec::new();
|
||||
let _ = write!(line, "I {name} {} {}", span.start, span.end);
|
||||
lines.push((span.start, line));
|
||||
}
|
||||
crate::sort::sort_by_key(&mut lines, |line| line.0);
|
||||
for (_, line) in lines {
|
||||
out.extend_from_slice(&line);
|
||||
out.push(b'\n');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
//! Flow. The tree has the nodes of TypeScript: `src/sema/parser/parser/flow.rs` has the table of what stands for what. Some of
|
||||
//! them stand for something that is no node in Prettier's tree. These functions tell the rest of the attachment pass what
|
||||
//! Prettier's tree looks like. Call them only for a file that is Flow.
|
||||
|
||||
use crate::js::ast_nodes::{AsAstNodes, AstNodes as N};
|
||||
use crate::js::print::flow::{
|
||||
as_declared_predicate, as_match_expression, head_of_match_statement,
|
||||
is_directly_in_function_type, is_function_type_param, is_inexact_mark, is_unknown_members_mark,
|
||||
is_variance, keyword_of_function_type,
|
||||
};
|
||||
use bun_lint::ast::comments::NodeOrName;
|
||||
use bun_lint::ast::{
|
||||
BinOp, EnumMember, Expr, ExprKind, Func, Handle, List, Node, Param, Pat, StmtKind, TypeKind,
|
||||
TypeNode,
|
||||
};
|
||||
|
||||
fn second_to_last<'a, T: Handle<'a>>(list: List<'a, T>) -> Option<T> {
|
||||
list.get(list.len().checked_sub(2)?)
|
||||
}
|
||||
|
||||
/// If `node` is the `...` that ends `{ a: A, ... }`, `[A, ...]` or `enum A { B, ... }`: the member or the element before it.
|
||||
fn sibling_before_dots(node: Node<'_>) -> Option<Option<Node<'_>>> {
|
||||
match node {
|
||||
Node::Member(member) if is_inexact_mark(member) => Some(match member.parent() {
|
||||
Node::Type(ty) => match ty.kind() {
|
||||
TypeKind::Object(members) => second_to_last(members).map(Node::Member),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}),
|
||||
Node::TupleElem(element) if element.flow_type().is_none() => Some(match element.parent() {
|
||||
Node::Type(ty) => match ty.kind() {
|
||||
TypeKind::Tuple(elements) => second_to_last(elements).map(Node::TupleElem),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}),
|
||||
Node::EnumMember(member) if is_unknown_members_mark(member) => {
|
||||
Some(match member.parent() {
|
||||
Node::Stmt(statement) => match statement.kind() {
|
||||
StmtKind::Enum(declaration) => {
|
||||
second_to_last(declaration.members()).map(Node::EnumMember)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `match (a)` of a match expression or statement.
|
||||
fn is_head_of_match(head: Expr<'_>) -> bool {
|
||||
match head.parent() {
|
||||
Node::Expr(e) => as_match_expression(e).is_some_and(|(it, _)| it == head),
|
||||
Node::Stmt(statement) => head_of_match_statement(statement) == Some(head),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `{ .. }` of a match expression.
|
||||
fn is_braces_of_match(braces: Expr<'_>) -> bool {
|
||||
matches!(braces.parent(), Node::Expr(e)
|
||||
if as_match_expression(e).is_some_and(|(_, it)| it == braces))
|
||||
}
|
||||
|
||||
/// `pattern => body` in a match expression, which is a property, or in a match statement, which is a `case`.
|
||||
fn is_match_case(node: Node<'_>) -> bool {
|
||||
match node {
|
||||
Node::Prop(case) => {
|
||||
matches!(case.parent(), Node::Expr(braces) if is_braces_of_match(braces))
|
||||
}
|
||||
Node::Case(case) => matches!(case.parent(), Node::Stmt(statement)
|
||||
if head_of_match_statement(statement).is_some()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `e` is all that is before the `=>` of a match case.
|
||||
fn is_left_of_match_case(e: Expr<'_>) -> bool {
|
||||
match e.parent() {
|
||||
Node::Prop(case) => case.computed_key() == Some(e) && is_match_case(Node::Prop(case)),
|
||||
Node::Case(case) => case.test() == Some(e) && is_match_case(Node::Case(case)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `pattern if (guard)`, which is `pattern && guard` in the tree. Returns the two.
|
||||
fn as_pattern_with_guard(e: Expr<'_>) -> Option<(Expr<'_>, Expr<'_>)> {
|
||||
match e.kind() {
|
||||
ExprKind::Binary {
|
||||
op: BinOp::And,
|
||||
left,
|
||||
right,
|
||||
} if is_left_of_match_case(e) => Some((left, right)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `e` is the pattern of a match case, or a part of it.
|
||||
fn is_match_pattern(mut e: Expr<'_>) -> bool {
|
||||
loop {
|
||||
if is_left_of_match_case(e) {
|
||||
return true;
|
||||
}
|
||||
e = match e.parent() {
|
||||
Node::Expr(parent) => match as_pattern_with_guard(parent) {
|
||||
Some((pattern, _)) => return pattern == e,
|
||||
None => parent,
|
||||
},
|
||||
// A property of an object pattern.
|
||||
Node::Prop(property) => match property.parent() {
|
||||
Node::Expr(object) if !is_braces_of_match(object) => object,
|
||||
_ => return false,
|
||||
},
|
||||
_ => return false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// `a | b` in a pattern: Prettier's `MatchOrPattern`.
|
||||
pub(super) fn is_match_or_pattern(e: Expr<'_>) -> bool {
|
||||
e.binary_op() == Some(BinOp::BitOr) && is_match_pattern(e)
|
||||
}
|
||||
|
||||
/// `a | b | c` is one `MatchOrPattern` for Prettier and `(a | b) | c` in the tree. If `e` is the `a | b`, returns the `b`.
|
||||
fn last_of_inner_or_pattern(e: Expr<'_>) -> Option<Expr<'_>> {
|
||||
let ExprKind::Binary {
|
||||
op: BinOp::BitOr,
|
||||
right,
|
||||
..
|
||||
} = e.kind()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let is_inner = matches!(e.parent(), Node::Expr(parent) if matches!(parent.kind(),
|
||||
ExprKind::Binary { op: BinOp::BitOr, left, .. } if left == e));
|
||||
(is_inner && is_match_pattern(e)).then_some(right)
|
||||
}
|
||||
|
||||
/// A match expression or statement, a case of one, a pattern or a property of an object pattern.
|
||||
pub(super) fn is_part_of_match(node: N<'_>) -> bool {
|
||||
match node {
|
||||
N::NewExpression(e) if as_match_expression(e).is_some() => true,
|
||||
N::SwitchStatement(statement) => head_of_match_statement(statement).is_some(),
|
||||
N::SwitchCase(case) => is_match_case(Node::Case(case)),
|
||||
N::ObjectProperty(property) => {
|
||||
is_match_case(Node::Prop(property))
|
||||
|| matches!(property.parent(), Node::Expr(object) if is_match_pattern(object))
|
||||
}
|
||||
_ => node.as_expression().is_some_and(is_match_pattern),
|
||||
}
|
||||
}
|
||||
|
||||
/// `hook useA() {}` or `component A() {}`.
|
||||
pub(super) fn is_hook_or_component_declaration(func: Func<'_>) -> bool {
|
||||
func.has_body() && keyword_of_declaration(func).is_some()
|
||||
}
|
||||
|
||||
/// `hook` or `component`, if the declaration of `func` starts with the word.
|
||||
fn keyword_of_declaration(func: Func<'_>) -> Option<&[u8]> {
|
||||
let Node::Stmt(statement) = func.owner() else {
|
||||
return None;
|
||||
};
|
||||
(statement.modifiers().iter())
|
||||
.map(|modifier| statement.file().slice(modifier.token_span()))
|
||||
.find(|word| matches!(*word, b"hook" | b"component"))
|
||||
}
|
||||
|
||||
/// The argument and the first case of a match expression or statement.
|
||||
pub(super) fn argument_and_first_case_of_match(node: N<'_>) -> Option<(Expr<'_>, Option<N<'_>>)> {
|
||||
let (head, first_case) = match node {
|
||||
N::NewExpression(e) => {
|
||||
let (head, braces) = as_match_expression(e)?;
|
||||
let ExprKind::Object(cases) = braces.kind() else {
|
||||
return None;
|
||||
};
|
||||
(head, cases.first().map(N::ObjectProperty))
|
||||
}
|
||||
N::SwitchStatement(statement) => match statement.kind() {
|
||||
StmtKind::Switch { cases, .. } => (
|
||||
head_of_match_statement(statement)?,
|
||||
cases.first().map(N::SwitchCase),
|
||||
),
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
Some((head.as_call()?.args().first()?, first_case))
|
||||
}
|
||||
|
||||
/// The node of Flow that has the parameters and the return type of something that is no function.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub(super) enum FunctionType {
|
||||
/// A function type, a method or a call property of an object type, the signature of `declare function`.
|
||||
FunctionTypeAnnotation,
|
||||
/// `hook (a: A) => B`, the signature of `declare hook`.
|
||||
HookTypeAnnotation,
|
||||
/// `component(a: A) renders B`. `declare component` is a `DeclareComponent`, which has the same rules.
|
||||
ComponentTypeAnnotation,
|
||||
}
|
||||
|
||||
/// `None` if `func` is a function, a method of a class or an object, a hook or a component.
|
||||
pub(super) fn function_type(func: Func<'_>) -> Option<FunctionType> {
|
||||
let keyword: Option<&[u8]> = match func.owner() {
|
||||
Node::Type(ty) => keyword_of_function_type(ty, func).map(str::as_bytes),
|
||||
Node::Member(member)
|
||||
if matches!(
|
||||
member.as_ast_nodes(),
|
||||
N::TSMethodSignature(_) | N::TSCallSignatureDeclaration(_)
|
||||
) =>
|
||||
{
|
||||
None
|
||||
}
|
||||
Node::Stmt(_) if !func.has_body() => keyword_of_declaration(func),
|
||||
_ => return None,
|
||||
};
|
||||
Some(match keyword {
|
||||
Some(b"hook") => FunctionType::HookTypeAnnotation,
|
||||
Some(_) => FunctionType::ComponentTypeAnnotation,
|
||||
None => FunctionType::FunctionTypeAnnotation,
|
||||
})
|
||||
}
|
||||
|
||||
/// Where Prettier's `HookTypeAnnotation` starts: after the `hook`.
|
||||
pub(super) fn start_of_hook_type(ty: TypeNode<'_>) -> Option<u32> {
|
||||
let TypeKind::Fn(func) = ty.kind() else {
|
||||
return None;
|
||||
};
|
||||
if keyword_of_function_type(ty, func) != Some("hook") {
|
||||
return None;
|
||||
}
|
||||
match func.type_params().angle_brackets_span() {
|
||||
Some(type_parameters) => Some(type_parameters.start),
|
||||
None => func.open_paren(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `node` is no node in Prettier's tree.
|
||||
pub(super) fn is_no_node(node: N<'_>) -> bool {
|
||||
match node {
|
||||
N::TSPropertySignature(member) => is_inexact_mark(member),
|
||||
N::TSRestType(element) => element.flow_type().is_none(),
|
||||
N::TSEnumMember(member) => is_unknown_members_mark(member),
|
||||
N::CallExpression(e) => is_head_of_match(e),
|
||||
N::ObjectExpression(e) => is_braces_of_match(e),
|
||||
N::LogicalExpression(e) => as_pattern_with_guard(e).is_some(),
|
||||
N::BinaryExpression(e) => last_of_inner_or_pattern(e).is_some(),
|
||||
N::TSTypeAnnotation(ty) => {
|
||||
is_directly_in_function_type(ty)
|
||||
// `component A() renders T {}`
|
||||
|| ty.flow_type_operator_operand().is_some()
|
||||
// The `T` of `: T %checks`
|
||||
|| matches!(ty.parent(), Node::Type(parent) if matches!(parent.kind(),
|
||||
TypeKind::Predicate { param, .. } if param.is("%checks")))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `a` of `(a: A) => B`. A `FunctionTypeParam` has an `Identifier` for its name. In a real function the parameter itself is the
|
||||
/// `Identifier`.
|
||||
pub(super) fn is_name_of_function_type_param(name: Pat<'_>) -> bool {
|
||||
matches!(name.parent(), Node::Param(param) if is_function_type_param(param) && !is_this(param))
|
||||
}
|
||||
|
||||
/// `(this: A) => B`: the `FunctionTypeParam` has no name.
|
||||
fn is_this_of_function_type(param: Param<'_>) -> bool {
|
||||
is_function_type_param(param) && is_this(param)
|
||||
}
|
||||
|
||||
fn is_this(param: Param<'_>) -> bool {
|
||||
(param.func()).is_some_and(|it| it.this_param() == Some(param))
|
||||
}
|
||||
|
||||
/// The parent of the type `node`, where `AstNodes::parent` gives that of TypeScript's tree:
|
||||
/// - The `: T` around it, where Flow has a `TypeAnnotation` and TypeScript has no `TSTypeAnnotation`: the bound of a type
|
||||
/// parameter, `(e: T)` and `declare module.exports: T`.
|
||||
/// - The class, for a type after `mixins`.
|
||||
/// - The return type, for the `T` of `: T %checks(e)`.
|
||||
pub(super) fn parent_of_type(node: N<'_>) -> Option<N<'_>> {
|
||||
let Node::Type(ty) = node.raw() else {
|
||||
return None;
|
||||
};
|
||||
if node != ty.as_ast_nodes() {
|
||||
return None;
|
||||
}
|
||||
let has_type_annotation = match ty.parent() {
|
||||
Node::TypeParam(param) => {
|
||||
param.constraint() == Some(ty) && param.flow_token_after_name() == b":"
|
||||
}
|
||||
Node::Expr(e) => e.is_flow_type_cast(),
|
||||
Node::Stmt(statement) => matches!(statement.kind(), StmtKind::TypeAlias(alias)
|
||||
if alias.name().name().is("module.exports")),
|
||||
Node::Class(class) if class.flow_mixins().iter().any(|it| it == ty) => {
|
||||
return Some(N::Class(class));
|
||||
}
|
||||
// The `T` of `: T %checks(e)`, which is a type query with `T` as its type argument in the tree.
|
||||
Node::Type(parent) if as_declared_predicate(parent).is_some() => {
|
||||
return Some(parent.as_ast_nodes());
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
has_type_annotation.then_some(N::TSTypeAnnotation(ty))
|
||||
}
|
||||
|
||||
/// What Prettier's tree has in the place of `node`, which the parser recorded as the node before a comment in
|
||||
/// `enclosing_node`.
|
||||
pub(super) fn preceding_node<'a>(
|
||||
node: NodeOrName<'a>,
|
||||
enclosing_node: Option<Node<'a>>,
|
||||
comment_start: u32,
|
||||
) -> Option<NodeOrName<'a>> {
|
||||
let node = match node {
|
||||
// `declare function f(): /* comment */ A`: the enclosing node is the `FunctionTypeAnnotation`, which starts at the `(`.
|
||||
NodeOrName::Name(_) if is_in_function_type(enclosing_node, comment_start) => return None,
|
||||
NodeOrName::Name(start) => {
|
||||
let is_no_node = is_name_of_type_parameter(enclosing_node, start)
|
||||
|| members_of_enum_with_explicit_type(enclosing_node, start).is_some();
|
||||
return match is_no_node {
|
||||
true => unrecorded_preceding_node(enclosing_node, start),
|
||||
false => Some(node),
|
||||
};
|
||||
}
|
||||
NodeOrName::Node(node) => node,
|
||||
};
|
||||
if let Some(sibling) = sibling_before_dots(node) {
|
||||
return sibling.map(NodeOrName::Node);
|
||||
}
|
||||
if matches!(node, Node::Pat(name) if matches!(name.parent(), Node::Param(param)
|
||||
if is_this_of_function_type(param)))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let Node::Expr(mut e) = node else {
|
||||
return Some(NodeOrName::Node(node));
|
||||
};
|
||||
// The word `match`.
|
||||
if matches!(e.parent(), Node::Expr(head) if is_head_of_match(head)
|
||||
&& head.as_call().is_some_and(|call| call.callee() == e))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if is_head_of_match(e) {
|
||||
e = e.as_call()?.args().first()?;
|
||||
}
|
||||
if let Some((_, guard)) = as_pattern_with_guard(e) {
|
||||
e = guard;
|
||||
}
|
||||
while let Some(last) = last_of_inner_or_pattern(e) {
|
||||
e = last;
|
||||
}
|
||||
Some(NodeOrName::Node(Node::Expr(e)))
|
||||
}
|
||||
|
||||
/// `enum A of string { B }`: the members, if `enclosing_node` is the enum and the `string`, which is no node, starts at `start`.
|
||||
fn members_of_enum_with_explicit_type(
|
||||
enclosing_node: Option<Node<'_>>,
|
||||
start: u32,
|
||||
) -> Option<List<'_, EnumMember<'_>>> {
|
||||
let Some(Node::Stmt(statement)) = enclosing_node else {
|
||||
return None;
|
||||
};
|
||||
let StmtKind::Enum(declaration) = statement.kind() else {
|
||||
return None;
|
||||
};
|
||||
(declaration.flow_explicit_type()?.start == start).then(|| declaration.members())
|
||||
}
|
||||
|
||||
/// Whether `offset` is after the `(` of a function type that `enclosing_node` stands for together with its name.
|
||||
fn is_in_function_type(enclosing_node: Option<Node<'_>>, offset: u32) -> bool {
|
||||
let func = match enclosing_node {
|
||||
Some(Node::Member(member)) => member.func(),
|
||||
Some(Node::Stmt(statement)) => match statement.kind() {
|
||||
StmtKind::Fn(func) => Some(func),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
// A `DeclareComponent` has its name and its parameters itself.
|
||||
func.is_some_and(|func| {
|
||||
matches!(
|
||||
function_type(func),
|
||||
Some(FunctionType::FunctionTypeAnnotation | FunctionType::HookTypeAnnotation)
|
||||
) && func.open_paren().is_some_and(|open| open < offset)
|
||||
})
|
||||
}
|
||||
|
||||
/// `<T>`: the name of a type parameter is a string in Flow's tree.
|
||||
fn is_name_of_type_parameter(enclosing_node: Option<Node<'_>>, start: u32) -> bool {
|
||||
matches!(enclosing_node, Some(Node::TypeParam(param)) if param.name().span().start == start)
|
||||
}
|
||||
|
||||
/// A node of Flow's tree that ends before `offset` in `enclosing_node` and that the parser does not record:
|
||||
/// - `component A(a /* comment */ as b) {}`: the `a`.
|
||||
/// - `{ + /* comment */ a: A }`, `<+ /* comment */ T>`: the `Variance`.
|
||||
/// - `<const /* comment */ T>`: the `const`.
|
||||
pub(super) fn unrecorded_preceding_node(
|
||||
enclosing_node: Option<Node<'_>>,
|
||||
offset: u32,
|
||||
) -> Option<NodeOrName<'_>> {
|
||||
let modifiers = match enclosing_node? {
|
||||
Node::Param(param) => {
|
||||
let outer_name = param.flow_outer_name()?;
|
||||
return (outer_name.end <= offset).then_some(NodeOrName::Name(outer_name.start));
|
||||
}
|
||||
Node::Member(member) => member.modifiers(),
|
||||
// `<const /* comment */ T>`: Prettier cannot parse this. The name `T` is not a node in Flow's tree, so the modifier is the only
|
||||
// thing the comment can attach to.
|
||||
Node::TypeParam(param) => {
|
||||
let modifier = (param.modifiers().iter())
|
||||
.map(|it| it.token_span())
|
||||
.filter(|it| it.end <= offset)
|
||||
.last()?;
|
||||
return Some(NodeOrName::Name(modifier.start));
|
||||
}
|
||||
// `+[K in T]: V` is a member whose type is a mapped type with the same span.
|
||||
Node::Type(ty) => match (ty.kind(), ty.parent()) {
|
||||
(TypeKind::Mapped(_), Node::Member(member)) => member.modifiers(),
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
let variance = modifiers.iter().find(|it| is_variance(*it))?.token_span();
|
||||
(variance.end <= offset).then_some(NodeOrName::Name(variance.start))
|
||||
}
|
||||
|
||||
/// What Prettier's tree has in the place of `node`, which the parser recorded as the node after a comment in `enclosing_node`.
|
||||
pub(super) fn following_node<'a>(
|
||||
node: NodeOrName<'a>,
|
||||
enclosing_node: Option<Node<'a>>,
|
||||
) -> Option<NodeOrName<'a>> {
|
||||
let node = match node {
|
||||
NodeOrName::Name(start) if is_name_of_type_parameter(enclosing_node, start) => return None,
|
||||
// `infer /* comment */ A`
|
||||
NodeOrName::Name(start)
|
||||
if let Some(Node::Type(ty)) = enclosing_node
|
||||
&& let TypeKind::Infer(param) = ty.kind()
|
||||
&& param.name().span().start == start =>
|
||||
{
|
||||
return Some(NodeOrName::Node(Node::TypeParam(param)));
|
||||
}
|
||||
NodeOrName::Name(start) => {
|
||||
return match members_of_enum_with_explicit_type(enclosing_node, start) {
|
||||
Some(members) => (members.first())
|
||||
.filter(|first| !is_unknown_members_mark(*first))
|
||||
.map(|first| NodeOrName::Node(Node::EnumMember(first))),
|
||||
None => Some(node),
|
||||
};
|
||||
}
|
||||
NodeOrName::Node(node) => node,
|
||||
};
|
||||
if sibling_before_dots(node).is_some() {
|
||||
return None;
|
||||
}
|
||||
// `: /* comment */ T %checks(e)` is a type query with `T` as its type argument in the tree.
|
||||
if let Node::Type(ty) = node
|
||||
&& let Some((Some(return_type), _)) = as_declared_predicate(ty)
|
||||
{
|
||||
return Some(NodeOrName::Node(Node::Type(return_type)));
|
||||
}
|
||||
let Node::Expr(e) = node else {
|
||||
return Some(NodeOrName::Node(node));
|
||||
};
|
||||
if is_braces_of_match(e)
|
||||
&& let ExprKind::Object(cases) = e.kind()
|
||||
{
|
||||
return cases.first().map(|case| NodeOrName::Node(Node::Prop(case)));
|
||||
}
|
||||
let e = as_pattern_with_guard(e).map_or(e, |(pattern, _)| pattern);
|
||||
Some(NodeOrName::Node(Node::Expr(e)))
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
//! Attaches every comment to a node before printing starts. This is Prettier's `attachComments`.
|
||||
//!
|
||||
//! 1. The parser has already recorded the nodes around each comment ([`bun_lint::ast::comments::Comment`]).
|
||||
//! [`surrounding_nodes`] converts them to the nodes the printers use.
|
||||
//! 2. [`rules`] has the special cases from Prettier's `handle-comments.js`.
|
||||
//! 3. Any other comment is attached by its position in the line: a comment on its own line becomes a leading comment of the
|
||||
//! following node, and a comment at the end of a line becomes a trailing comment of the preceding node.
|
||||
//!
|
||||
//! Nothing here searches the tree. Of the source text, the rules look at the byte at an offset that the lexer has recorded, to
|
||||
//! tell which token is before or after a comment.
|
||||
|
||||
mod debug;
|
||||
mod flow;
|
||||
mod rules;
|
||||
mod surrounding_nodes;
|
||||
|
||||
use super::ast_nodes::AstNodes;
|
||||
use super::comments::{Comment, CommentExt};
|
||||
use crate::options::Flavor;
|
||||
use bun_lint::ast::{Expr, File};
|
||||
pub(crate) use debug::list;
|
||||
use surrounding_nodes::{AttachContext, SurroundingNodes};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum Placement {
|
||||
Leading,
|
||||
Trailing,
|
||||
Dangling,
|
||||
}
|
||||
|
||||
/// Prettier's `comment.marker`. It tells apart the dangling comments of one node that are printed in different places.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) enum Marker {
|
||||
#[default]
|
||||
None,
|
||||
/// `(a /* comment */) => b`
|
||||
CommentBeforeArrow,
|
||||
/// Before the `implements` keyword of a class.
|
||||
Implements,
|
||||
/// Before the `extends` keyword of a class or an interface.
|
||||
Extends,
|
||||
/// Before the `mixins` keyword of Flow's `declare class`.
|
||||
Mixins,
|
||||
/// In the `</>` of a fragment, which is a node for Prettier. A comment in the `<>` has no marker.
|
||||
ClosingFragment,
|
||||
/// The comment is inside an ignored node, so it is copied along with the node's source text. Prettier does not attach such
|
||||
/// a comment at all.
|
||||
InIgnoredNode,
|
||||
}
|
||||
|
||||
/// The thing a comment is attached to.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Owner<'a> {
|
||||
Node(AstNodes<'a>),
|
||||
/// An identifier or a literal key, given by its start offset. For Prettier it is an `Identifier` or `Literal` node.
|
||||
Name(u32),
|
||||
/// The parentheses after a `/** @type {T} */` cast: the expression inside them and the offset of the `(`. For Prettier it is
|
||||
/// Babel's `ParenthesizedExpression` node.
|
||||
CastParens(Expr<'a>, u32),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct Attachment<'a> {
|
||||
pub(crate) owner: Owner<'a>,
|
||||
pub(crate) placement: Placement,
|
||||
pub(crate) marker: Marker,
|
||||
/// Prettier's `comment.unignore`: a `prettier-ignore` comment that does not apply to its owner.
|
||||
pub(crate) is_unignored: bool,
|
||||
/// A `prettier-ignore` comment that applies to its owner.
|
||||
pub(crate) ignores_owner: bool,
|
||||
}
|
||||
|
||||
impl<'a> Attachment<'a> {
|
||||
fn new(owner: Owner<'a>, placement: Placement) -> Self {
|
||||
Attachment {
|
||||
owner,
|
||||
placement,
|
||||
marker: Marker::None,
|
||||
is_unignored: false,
|
||||
ignores_owner: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn leading(owner: Owner<'a>) -> Self {
|
||||
Self::new(owner, Placement::Leading)
|
||||
}
|
||||
|
||||
fn trailing(owner: Owner<'a>) -> Self {
|
||||
Self::new(owner, Placement::Trailing)
|
||||
}
|
||||
|
||||
fn dangling(owner: Owner<'a>) -> Self {
|
||||
Self::new(owner, Placement::Dangling)
|
||||
}
|
||||
|
||||
fn dangling_with_marker(owner: Owner<'a>, marker: Marker) -> Self {
|
||||
Attachment {
|
||||
marker,
|
||||
..Self::dangling(owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Attached<'a> {
|
||||
/// Indexed by comment id.
|
||||
pub(crate) attachments: Vec<Attachment<'a>>,
|
||||
/// Prettier's `node.prettierIgnore`: nodes that are ignored because of a comment attached to a different node.
|
||||
pub(crate) ignored_nodes: Vec<Owner<'a>>,
|
||||
/// Start offsets of the owners that are not next to their comment, or do not contain their dangling comment. Sorted. While
|
||||
/// a node is printed that has neither a comment nor one of these in it, nothing in it owns a comment.
|
||||
pub(crate) relocated_owner_starts: Vec<u32>,
|
||||
pub(crate) has_ignore_comments: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct Options {
|
||||
/// Prettier would parse this file with Babel instead of typescript-estree. Their trees differ in a few places.
|
||||
pub(crate) has_tree_of_babel: bool,
|
||||
pub(crate) experimental_ternaries: bool,
|
||||
pub(crate) flavor: Flavor,
|
||||
}
|
||||
|
||||
/// Prettier's `comment.placement`.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum TextPosition {
|
||||
/// Only whitespace before the comment on its line.
|
||||
OwnLine,
|
||||
/// Code before the comment and only whitespace after it.
|
||||
EndOfLine,
|
||||
/// Code after the comment on its line.
|
||||
Remaining,
|
||||
}
|
||||
|
||||
/// Everything a rule can look at.
|
||||
struct CommentContext<'a> {
|
||||
comment: Comment<'a>,
|
||||
is_ignore_comment: bool,
|
||||
text_position: TextPosition,
|
||||
nodes: SurroundingNodes<'a>,
|
||||
is_last_comment: bool,
|
||||
}
|
||||
|
||||
/// Whether only spaces and tabs separate two consecutive comments.
|
||||
fn are_on_same_line(first: Comment<'_>, second: Comment<'_>) -> bool {
|
||||
first.next_token_start() == second.next_token_start() && !second.has_newline_before()
|
||||
}
|
||||
|
||||
/// Prettier's `isOwnLineComment` and `isEndOfLineComment`. A run of comments on one line that share the same preceding (or
|
||||
/// following) node is classified as a whole.
|
||||
///
|
||||
/// Remembers the run of the comment that was asked about last, so that a long run is walked once and not once for each of its
|
||||
/// comments.
|
||||
struct TextPositions {
|
||||
/// The comments from `.0` to `.1` are a run with the same preceding node.
|
||||
with_same_preceding_node: (usize, usize),
|
||||
/// The same for the following node.
|
||||
with_same_following_node: (usize, usize),
|
||||
}
|
||||
|
||||
impl TextPositions {
|
||||
const NONE_ASKED_YET: TextPositions = TextPositions {
|
||||
with_same_preceding_node: (1, 0),
|
||||
with_same_following_node: (1, 0),
|
||||
};
|
||||
|
||||
fn of<'a>(
|
||||
&mut self,
|
||||
file: &'a File<'a>,
|
||||
nodes: &[SurroundingNodes<'a>],
|
||||
id: usize,
|
||||
) -> TextPosition {
|
||||
let comment = |id: usize| file.comment(id as u32);
|
||||
let (first, last) = self.with_same_preceding_node;
|
||||
if !(first <= id && id <= last) {
|
||||
let is_in_run = |id: usize| {
|
||||
are_on_same_line(comment(id - 1), comment(id))
|
||||
&& nodes[id - 1].preceding_node == nodes[id].preceding_node
|
||||
};
|
||||
let (mut first, mut last) = (id, id);
|
||||
if nodes[id].preceding_node.is_some() {
|
||||
while first > 0 && is_in_run(first) {
|
||||
first -= 1;
|
||||
}
|
||||
while last + 1 < nodes.len() && is_in_run(last + 1) {
|
||||
last += 1;
|
||||
}
|
||||
}
|
||||
self.with_same_preceding_node = (first, last);
|
||||
}
|
||||
if comment(self.with_same_preceding_node.0).has_newline_before() {
|
||||
return TextPosition::OwnLine;
|
||||
}
|
||||
let (first, last) = self.with_same_following_node;
|
||||
if !(first <= id && id <= last) {
|
||||
let mut last = id;
|
||||
while nodes[id].following_node.is_some()
|
||||
&& last + 1 < nodes.len()
|
||||
&& are_on_same_line(comment(last), comment(last + 1))
|
||||
&& nodes[last + 1].following_node == nodes[id].following_node
|
||||
{
|
||||
last += 1;
|
||||
}
|
||||
self.with_same_following_node = (id, last);
|
||||
}
|
||||
match comment(self.with_same_following_node.1).has_newline_after() {
|
||||
true => TextPosition::EndOfLine,
|
||||
false => TextPosition::Remaining,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The default for a comment that no rule handles. Returns `None` for a comment between two nodes on the same line, which
|
||||
/// [`break_ties`] decides.
|
||||
fn attach_by_text_position<'a>(
|
||||
text_position: TextPosition,
|
||||
nodes: &SurroundingNodes<'a>,
|
||||
program: Owner<'a>,
|
||||
) -> Option<Attachment<'a>> {
|
||||
Some(
|
||||
match (text_position, nodes.preceding_node, nodes.following_node) {
|
||||
(TextPosition::Remaining, Some(_), Some(_)) => return None,
|
||||
(TextPosition::OwnLine, _, Some(following_node)) => Attachment::leading(following_node),
|
||||
(_, Some(preceding_node), _) => Attachment::trailing(preceding_node),
|
||||
(_, None, Some(following_node)) => Attachment::leading(following_node),
|
||||
(_, None, None) => Attachment::dangling(nodes.enclosing_node.unwrap_or(program)),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Prettier's `breakTies`. `ties` are consecutive comments between the same two nodes on one line. The ones at the end that
|
||||
/// are separated from the following node only by whitespace and `(` become its leading comments. The rest become trailing
|
||||
/// comments of the preceding node.
|
||||
fn break_ties<'a>(
|
||||
cx: &AttachContext<'a, '_>,
|
||||
ties: &mut Vec<usize>,
|
||||
nodes: &[SurroundingNodes<'a>],
|
||||
attachments: &mut [Attachment<'a>],
|
||||
) {
|
||||
let Some(&SurroundingNodes {
|
||||
preceding_node: Some(preceding_node),
|
||||
following_node: Some(following_node),
|
||||
..
|
||||
}) = ties.first().and_then(|first| nodes.get(*first))
|
||||
else {
|
||||
return ties.clear();
|
||||
};
|
||||
let mut gap_end = cx.start_of(following_node);
|
||||
let mut first_leading = ties.len();
|
||||
while first_leading > 0 {
|
||||
let comment = cx.file.comment(ties[first_leading - 1] as u32).span();
|
||||
if !cx.is_gap(comment.end, gap_end) {
|
||||
break;
|
||||
}
|
||||
gap_end = comment.start;
|
||||
first_leading -= 1;
|
||||
}
|
||||
for (index, id) in ties.drain(..).enumerate() {
|
||||
(attachments[id].owner, attachments[id].placement) = match index < first_leading {
|
||||
true => (preceding_node, Placement::Trailing),
|
||||
false => (following_node, Placement::Leading),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an ignore comment that trails `owner` ignores it in the oxfmt flavor. For Prettier every comment of a node counts.
|
||||
/// For oxfmt the comment has to come right after the node, with nothing but separators between them, and end its line.
|
||||
///
|
||||
/// ```js
|
||||
/// a + b; // prettier-ignore is copied
|
||||
/// f(a + b /* prettier-ignore */); is formatted: the comment does not end its line
|
||||
/// (a + b) // prettier-ignore is formatted: a `)` is between the two
|
||||
/// .c;
|
||||
/// ```
|
||||
fn trailing_ignore_comment_counts_for_oxfmt<'a>(
|
||||
cx: &AttachContext<'a, '_>,
|
||||
comment: Comment<'a>,
|
||||
owner: Owner<'a>,
|
||||
text_position: TextPosition,
|
||||
) -> bool {
|
||||
let text = cx.file.text();
|
||||
let owner_end = match owner {
|
||||
// `span_of` gives a name no width.
|
||||
Owner::Name(start) => {
|
||||
let name = text.get(start as usize..).unwrap_or_default();
|
||||
start as usize + bun_lint::tokens::token_len(name)
|
||||
}
|
||||
_ => cx.file.without_semicolon(cx.span_of(owner)).end as usize,
|
||||
};
|
||||
let between = (text.get(owner_end..comment.prev_token_end() as usize)).unwrap_or_default();
|
||||
text_position == TextPosition::EndOfLine
|
||||
&& (between.iter()).all(|byte| matches!(byte, b' ' | b'\t' | b'=' | b':' | b',' | b';'))
|
||||
}
|
||||
|
||||
/// `cast_parens`: see [`super::comments::cast_parens`].
|
||||
pub(crate) fn attach<'a>(
|
||||
file: &'a File<'a>,
|
||||
cast_parens: &[u32],
|
||||
options: Options,
|
||||
) -> Attached<'a> {
|
||||
let cx = AttachContext::new(file, cast_parens, options);
|
||||
let program = cx.program();
|
||||
let nodes = cx.surrounding_nodes_of_all_comments();
|
||||
let mut attached = Attached {
|
||||
attachments: vec![Attachment::dangling(program); nodes.len()],
|
||||
..Attached::default()
|
||||
};
|
||||
let mut ties: Vec<usize> = Vec::new();
|
||||
let mut text_positions = TextPositions::NONE_ASKED_YET;
|
||||
for (id, comment) in file.comments_by_id().enumerate() {
|
||||
// The usual case. See `AttachContext::list_elements_around`.
|
||||
if nodes[id].is_before_list_element
|
||||
&& comment.has_newline_before()
|
||||
&& let Some(following_node) = nodes[id].following_node
|
||||
{
|
||||
let ignores_owner = comment.is_ignore_comment(options.flavor);
|
||||
attached.has_ignore_comments |= ignores_owner;
|
||||
attached.attachments[id] = Attachment {
|
||||
ignores_owner,
|
||||
..Attachment::leading(following_node)
|
||||
};
|
||||
continue;
|
||||
}
|
||||
let context = CommentContext {
|
||||
comment,
|
||||
is_ignore_comment: comment.is_ignore_comment(options.flavor),
|
||||
text_position: text_positions.of(file, &nodes, id),
|
||||
nodes: nodes[id],
|
||||
is_last_comment: id + 1 == nodes.len(),
|
||||
};
|
||||
attached.has_ignore_comments |= context.is_ignore_comment;
|
||||
let mut ignored_node = None;
|
||||
// `handleClosureTypeCastComments` is the only rule that does not ask what the enclosing node is.
|
||||
let has_no_rule = context.nodes.is_before_list_element
|
||||
&& (context.text_position != TextPosition::EndOfLine
|
||||
|| !comment.looks_like_type_cast());
|
||||
let attachment = match has_no_rule {
|
||||
true => None,
|
||||
false => rules::handle_comment(&cx, &context, &mut ignored_node),
|
||||
}
|
||||
.or_else(|| attach_by_text_position(context.text_position, &context.nodes, program));
|
||||
attached.attachments[id].is_unignored = ignored_node.is_some();
|
||||
attached.attachments[id].ignores_owner =
|
||||
context.is_ignore_comment && ignored_node.is_none();
|
||||
attached.ignored_nodes.extend(ignored_node);
|
||||
let Some(attachment) = attachment else {
|
||||
let starts_new_run = (ties.last())
|
||||
.is_some_and(|last| nodes[*last].following_node != nodes[id].following_node);
|
||||
if starts_new_run {
|
||||
break_ties(&cx, &mut ties, &nodes, &mut attached.attachments);
|
||||
}
|
||||
ties.push(id);
|
||||
continue;
|
||||
};
|
||||
let SurroundingNodes {
|
||||
enclosing_node,
|
||||
preceding_node,
|
||||
following_node,
|
||||
..
|
||||
} = nodes[id];
|
||||
let nearby = [
|
||||
enclosing_node,
|
||||
preceding_node,
|
||||
following_node,
|
||||
Some(program),
|
||||
];
|
||||
// `class A // comment ⏎ {}`: the comment dangles from the body, and is not in it.
|
||||
let is_outside_of_owner = attachment.placement == Placement::Dangling
|
||||
&& !cx.span_of(attachment.owner).contains(comment.span());
|
||||
if is_outside_of_owner || !nearby.contains(&Some(attachment.owner)) {
|
||||
(attached.relocated_owner_starts).push(cx.start_of(attachment.owner));
|
||||
}
|
||||
// `interface A {⏎ // prettier-ignore ⏎}`: for oxfmt a comment in an empty node ignores nothing.
|
||||
let is_unignored = ignored_node.is_some()
|
||||
|| attachment.is_unignored
|
||||
|| (options.flavor.is_oxfmt() && attachment.placement == Placement::Dangling);
|
||||
attached.attachments[id] = Attachment {
|
||||
is_unignored,
|
||||
ignores_owner: context.is_ignore_comment && !is_unignored,
|
||||
..attachment
|
||||
};
|
||||
}
|
||||
break_ties(&cx, &mut ties, &nodes, &mut attached.attachments);
|
||||
if options.flavor.is_oxfmt() && attached.has_ignore_comments {
|
||||
for (id, comment) in file.comments_by_id().enumerate() {
|
||||
let attachment = &mut attached.attachments[id];
|
||||
if attachment.ignores_owner
|
||||
&& attachment.placement == Placement::Trailing
|
||||
&& !trailing_ignore_comment_counts_for_oxfmt(
|
||||
&cx,
|
||||
comment,
|
||||
attachment.owner,
|
||||
text_positions.of(file, &nodes, id),
|
||||
)
|
||||
{
|
||||
attachment.ignores_owner = false;
|
||||
attachment.is_unignored = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let ignored_starts = attached.ignored_nodes.iter().map(|node| cx.start_of(*node));
|
||||
attached.relocated_owner_starts.extend(ignored_starts);
|
||||
// `let a: /* comment */ | B`: the comment belongs to `B`, and the printers get to `B` by way of the union `| B`, which does not
|
||||
// contain the comment.
|
||||
let starts_in_unions = (attached.attachments.iter())
|
||||
.filter(|attachment| cx.is_only_member_of_union(attachment.owner))
|
||||
.map(|attachment| cx.start_of(attachment.owner));
|
||||
attached.relocated_owner_starts.extend(starts_in_unions);
|
||||
crate::sort::sort(&mut attached.relocated_owner_starts);
|
||||
attached
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -17,9 +17,32 @@ impl<'a> Formatter<'a> {
|
||||
#[inline]
|
||||
pub(crate) fn lines_before(&self, span: Span) -> usize {
|
||||
let looks_past_comma = !comma_ends_the_search_for_line_breaks(self);
|
||||
if self.is_quiet() {
|
||||
return (self.source_text()).get_lines_before(span, None, looks_past_comma);
|
||||
}
|
||||
self.lines_before_node_or_its_comments(span, looks_past_comma)
|
||||
}
|
||||
|
||||
fn lines_before_node_or_its_comments(&self, span: Span, looks_past_comma: bool) -> usize {
|
||||
let directly_before = self.comments().first_leading_comment_before(span.start);
|
||||
// `a\n\n// comment\n, b`: the comment leads `b` too.
|
||||
let end_of_token_before = match directly_before {
|
||||
Some(comment) => comment.prev_token_end() as usize,
|
||||
None => {
|
||||
let before = self.source_text().as_bytes().get(..span.start as usize);
|
||||
before.unwrap_or_default().trim_ascii_end().len()
|
||||
}
|
||||
};
|
||||
let before_comma = (self.source_text().as_bytes().get(..end_of_token_before))
|
||||
.and_then(|before| before.strip_suffix(b","))
|
||||
.and_then(|before| {
|
||||
self.comments()
|
||||
.first_leading_comment_before(before.len() as u32)
|
||||
});
|
||||
let first_leading_comment = before_comma.or(directly_before);
|
||||
self.source_text().get_lines_before(
|
||||
span,
|
||||
self.comments().first_unprinted_span(),
|
||||
first_leading_comment.map(|comment| comment.span()),
|
||||
looks_past_comma,
|
||||
)
|
||||
}
|
||||
@@ -66,8 +89,6 @@ impl<'a> Formatter<'a> {
|
||||
/// printed.
|
||||
pub(crate) fn speculate_will_break(&mut self, content: &(impl Format<'a> + Spanned)) -> bool {
|
||||
let snapshot = self.comments().snapshot();
|
||||
self.comments_mut()
|
||||
.skip_comments_before(content.span().start);
|
||||
let will_break = match self.intern(content) {
|
||||
Some(element) => element.will_break(self),
|
||||
None => false,
|
||||
@@ -119,6 +140,15 @@ impl<'fmt, 'a, Separator: Format<'a>> JoinNodesBuilder<'fmt, 'a, Separator> {
|
||||
content.fmt(self.fmt);
|
||||
}
|
||||
|
||||
/// [`JoinNodesBuilder::entry`], with an empty line before it.
|
||||
pub(crate) fn entry_after_empty_line(&mut self, content: &(impl Format<'a> + ?Sized)) {
|
||||
if self.has_elements {
|
||||
write!(self.fmt, empty_line());
|
||||
}
|
||||
self.has_elements = true;
|
||||
content.fmt(self.fmt);
|
||||
}
|
||||
|
||||
/// [`JoinNodesBuilder::entry`], before which an empty line is not kept.
|
||||
pub(crate) fn entry_without_empty_line(&mut self, content: &(impl Format<'a> + ?Sized)) {
|
||||
if self.has_elements {
|
||||
@@ -128,10 +158,6 @@ impl<'fmt, 'a, Separator: Format<'a>> JoinNodesBuilder<'fmt, 'a, Separator> {
|
||||
content.fmt(self.fmt);
|
||||
}
|
||||
|
||||
pub(crate) fn has_comment_before(&self, start: u32) -> bool {
|
||||
self.fmt.comments().has_comment_before(start)
|
||||
}
|
||||
|
||||
pub(crate) fn separator_no_entry(&mut self, span: Span) {
|
||||
if self.has_elements {
|
||||
if self.has_lines_before(span) {
|
||||
|
||||
+575
-1650
File diff suppressed because it is too large.
Load diff
+43
-22
@@ -1,7 +1,6 @@
|
||||
//! What the code that formats a file can see besides the node it is formatting.
|
||||
|
||||
use super::comments::{Comment, Comments};
|
||||
use super::source_text::SourceText;
|
||||
use super::comments::{Comments, FileComments};
|
||||
use super::utils::tailwindcss::TailwindContext;
|
||||
use crate::cursor::CursorRegion;
|
||||
use crate::ir::element::FormatElement;
|
||||
@@ -11,6 +10,17 @@ use bun_lint::span::{Span, Spanned};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::cell::Cell;
|
||||
|
||||
/// Whether Prettier reads the code with a parser that has no `ChainExpression` around an optional chain: Babel, which reads
|
||||
/// JavaScript and every expression in HTML, in TypeScript too, or `angular-estree-parser`. Prettier's `flow` parser produces ESTree's tree, with the
|
||||
/// `ChainExpression`.
|
||||
pub(crate) fn has_tree_of_babel(file: &File<'_>, options: &FormatOptions) -> bool {
|
||||
let is_read_by_flow_parser =
|
||||
file.is_flow() && !super::print::flow::goes_to_babel(options, file.path());
|
||||
(file.is_javascript() && !is_read_by_flow_parser)
|
||||
|| options.in_html.has_tree_of_babel
|
||||
|| !matches!(options.in_html.root, HtmlRoot::None | HtmlRoot::Program)
|
||||
}
|
||||
|
||||
pub(crate) struct JsFormatContext<'a> {
|
||||
/// `None` for a document that is not written for JavaScript.
|
||||
file: Option<&'a File<'a>>,
|
||||
@@ -18,6 +28,9 @@ pub(crate) struct JsFormatContext<'a> {
|
||||
has_tree_of_babel: bool,
|
||||
options: FormatOptions,
|
||||
comments: Comments<'a>,
|
||||
/// The next node that is written leaves its trailing comments to whoever asked for that:
|
||||
/// [`FormatNodeWithoutTrailingComments`](super::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments).
|
||||
skip_trailing_comments: bool,
|
||||
/// What has been formatted ahead of its turn, by the span of the node: an argument that was
|
||||
/// formatted to decide on the layout of the call. A node whose span is here is not formatted
|
||||
/// again, which would print its comments twice.
|
||||
@@ -33,6 +46,9 @@ pub(crate) struct JsFormatContext<'a> {
|
||||
stack_check: bun_core::StackCheck,
|
||||
/// Something has not been written because it is nested too deeply.
|
||||
pub(crate) ran_out_of_stack: bool,
|
||||
/// The ids of the comments that no printer wrote when the file was formatted the first time. See
|
||||
/// [`crate::ir::run::format_with_marks`].
|
||||
pub(crate) comments_without_printer: Vec<u32>,
|
||||
/// The last member access with many member accesses around it, and whether it stays on the line
|
||||
/// of its object. The one around it has the same answer.
|
||||
pub(crate) long_member_chain: Cell<Option<(Expr<'a>, bool)>>,
|
||||
@@ -45,30 +61,29 @@ pub(crate) struct JsFormatContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> JsFormatContext<'a> {
|
||||
pub(crate) fn new(file: &'a File<'a>, options: FormatOptions, comments: &'a [Comment]) -> Self {
|
||||
pub(crate) fn new(
|
||||
file: &'a File<'a>,
|
||||
options: FormatOptions,
|
||||
comments: &'a FileComments,
|
||||
) -> Self {
|
||||
Self {
|
||||
file: Some(file),
|
||||
has_tree_of_babel: file.is_javascript()
|
||||
|| options.in_html.has_tree_of_babel
|
||||
|| !matches!(options.in_html.root, HtmlRoot::None | HtmlRoot::Program),
|
||||
..Self::without_file(file.text(), options, comments)
|
||||
has_tree_of_babel: has_tree_of_babel(file, &options),
|
||||
comments: Comments::new(Some(file), options.flavor, comments),
|
||||
..Self::without_file(options, comments)
|
||||
}
|
||||
}
|
||||
|
||||
/// For a document that is written for `source`, which is in another language. Nothing that
|
||||
/// writes it asks for the [file](JsFormatContext::file).
|
||||
pub(crate) fn without_file(
|
||||
source: &'a [u8],
|
||||
options: FormatOptions,
|
||||
comments: &'a [Comment],
|
||||
) -> Self {
|
||||
/// For a document in another language. Nothing that writes it asks for the [file](JsFormatContext::file).
|
||||
pub(crate) fn without_file(options: FormatOptions, comments: &'a FileComments) -> Self {
|
||||
let tailwind = options.tailwind.as_deref();
|
||||
let of_plugin = (tailwind.filter(|it| it.follows_plugin)).map(TailwindContext::of_plugin);
|
||||
Self {
|
||||
file: None,
|
||||
has_tree_of_babel: false,
|
||||
comments: Comments::new(None, options.flavor, comments),
|
||||
options,
|
||||
comments: Comments::new(SourceText::new(source), comments),
|
||||
skip_trailing_comments: false,
|
||||
cached_elements: FxHashMap::default(),
|
||||
quote_needed_stack: Vec::new(),
|
||||
tailwind_contexts: of_plugin.into_iter().collect(),
|
||||
@@ -76,6 +91,7 @@ impl<'a> JsFormatContext<'a> {
|
||||
cursor: CursorRegion::NONE,
|
||||
stack_check: bun_core::StackCheck::init(),
|
||||
ran_out_of_stack: false,
|
||||
comments_without_printer: Vec::new(),
|
||||
line_start: Cell::new((0, 0)),
|
||||
long_member_chain: Cell::new(None),
|
||||
is_vue_filter_sequence: Cell::new(false),
|
||||
@@ -89,8 +105,7 @@ impl<'a> JsFormatContext<'a> {
|
||||
.expect("only what writes JavaScript asks for the file, and there is one then")
|
||||
}
|
||||
|
||||
/// Whether Prettier reads the code with a parser that has no `ChainExpression` around an optional chain: Babel, which
|
||||
/// reads JavaScript and every expression in HTML, in TypeScript too, or `angular-estree-parser`.
|
||||
/// See [`has_tree_of_babel`].
|
||||
#[inline]
|
||||
pub(crate) fn has_tree_of_babel(&self) -> bool {
|
||||
self.has_tree_of_babel
|
||||
@@ -112,6 +127,17 @@ impl<'a> JsFormatContext<'a> {
|
||||
&self.options
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn set_skip_trailing_comments(&mut self, skip: bool) {
|
||||
self.skip_trailing_comments = skip;
|
||||
}
|
||||
|
||||
/// Reads the flag and clears it, so that it only applies to one node.
|
||||
#[inline]
|
||||
pub(crate) fn take_skip_trailing_comments(&mut self) -> bool {
|
||||
std::mem::take(&mut self.skip_trailing_comments)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn comments(&self) -> &Comments<'a> {
|
||||
&self.comments
|
||||
@@ -122,11 +148,6 @@ impl<'a> JsFormatContext<'a> {
|
||||
&mut self.comments
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn has_cached_elements(&self) -> bool {
|
||||
!self.cached_elements.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn get_cached_element(&self, key: &impl Spanned) -> Option<FormatElement> {
|
||||
self.cached_elements.get(&key.span()).copied()
|
||||
}
|
||||
|
||||
@@ -107,10 +107,6 @@ pub(crate) trait StmtFields<'a>: Copy {
|
||||
fn for_left(self) -> Option<Stmt<'a>>;
|
||||
/// `ForStatement.init`
|
||||
fn for_init(self) -> Option<Stmt<'a>>;
|
||||
/// `IfStatement.consequent`
|
||||
fn consequent(self) -> Option<Stmt<'a>>;
|
||||
/// `IfStatement.alternate`
|
||||
fn alternate(self) -> Option<Stmt<'a>>;
|
||||
/// `TryStatement.finalizer`
|
||||
fn finalizer(self) -> Option<Stmt<'a>>;
|
||||
/// `for await`
|
||||
@@ -132,20 +128,6 @@ impl<'a> StmtFields<'a> for Stmt<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn consequent(self) -> Option<Stmt<'a>> {
|
||||
match self.kind() {
|
||||
StmtKind::If { yes, .. } => Some(yes),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn alternate(self) -> Option<Stmt<'a>> {
|
||||
match self.kind() {
|
||||
StmtKind::If { no, .. } => no,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn finalizer(self) -> Option<Stmt<'a>> {
|
||||
match self.kind() {
|
||||
StmtKind::Try { finalizer, .. } => finalizer,
|
||||
|
||||
+404
-486
File diff suppressed because it is too large.
Load diff
@@ -26,7 +26,7 @@ use bun_lint::ast::jsdoc::JSDoc;
|
||||
|
||||
/// Writes `comment` formatted, if the `jsdoc` option is set and it is a JSDoc comment that formatting changes.
|
||||
/// Returns whether it has been written.
|
||||
pub(crate) fn write_comment<'a>(comment: &Comment, f: &mut Formatter<'a>) -> bool {
|
||||
pub(crate) fn write_comment<'a>(comment: Comment<'_>, f: &mut Formatter<'a>) -> bool {
|
||||
let Some(options) = f.options().jsdoc else {
|
||||
return false;
|
||||
};
|
||||
@@ -37,24 +37,24 @@ pub(crate) fn write_comment<'a>(comment: &Comment, f: &mut Formatter<'a>) -> boo
|
||||
}
|
||||
|
||||
fn write_formatted<'a>(
|
||||
comment: &Comment,
|
||||
comment: Comment<'_>,
|
||||
options: &JsdocOptions,
|
||||
stack: &Stack,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
let content = f.source_text().text_for(&comment.span);
|
||||
let content = f.source_text().text_for(&comment.span());
|
||||
if !content.starts_with(b"/**") {
|
||||
return false;
|
||||
}
|
||||
let docs = f.file().jsdoc();
|
||||
// `/*****/` is none.
|
||||
let Some(first) = docs
|
||||
.at(comment.span.start)
|
||||
.at(comment.span().start)
|
||||
.filter(|doc| !doc.is_all_asterisks())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let before = f.source_text().text_for(&Span::before(0, comment.span));
|
||||
let before = f.source_text().text_for(&Span::before(0, comment.span()));
|
||||
let tab_width = f.options().indent_width.value() as usize;
|
||||
let indent: usize = before
|
||||
.iter()
|
||||
@@ -86,27 +86,6 @@ fn write_formatted<'a>(
|
||||
)
|
||||
};
|
||||
|
||||
// Comments that directly follow each other have been made one. Each is formatted by itself, and they go on
|
||||
// following each other directly if each has several lines.
|
||||
if first.comment_span().end < comment.span.end {
|
||||
let mut formatted = Vec::new();
|
||||
let mut start = comment.span.start;
|
||||
while start < comment.span.end {
|
||||
let Some(doc) = docs.at(start) else {
|
||||
return false;
|
||||
};
|
||||
match format(doc, f) {
|
||||
Some(FormattedJsdoc::MultiLine(lines)) => formatted.push(lines),
|
||||
_ => return false,
|
||||
}
|
||||
start = doc.comment_span().end;
|
||||
}
|
||||
for lines in &formatted {
|
||||
write_lines(lines, f);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
match format(first, f) {
|
||||
None => return false,
|
||||
Some(FormattedJsdoc::Empty) => {}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
//! JavaScript, JSX and TypeScript.
|
||||
|
||||
pub(crate) mod ast_nodes;
|
||||
pub(crate) mod attach;
|
||||
pub(crate) mod builders;
|
||||
pub(crate) mod comments;
|
||||
pub(crate) mod context;
|
||||
pub(crate) mod fields;
|
||||
pub(crate) mod format;
|
||||
pub(crate) mod jsdoc;
|
||||
pub(crate) mod owners;
|
||||
pub(crate) mod parentheses;
|
||||
pub(crate) mod print;
|
||||
pub(crate) mod siblings;
|
||||
pub(crate) mod sort_imports;
|
||||
pub(crate) mod source_text;
|
||||
pub(crate) mod trivia;
|
||||
@@ -20,12 +21,12 @@ use crate::prelude::*;
|
||||
pub(crate) mod prelude {
|
||||
pub(crate) use super::ast_nodes::{AsAstNodes, AstNodes, ChainElement};
|
||||
pub(crate) use super::builders::FormatSeparatedIter;
|
||||
pub(crate) use super::comments::{Comment, Comments};
|
||||
pub(crate) use super::comments::{Comment, CommentExt, CommentList, Comments};
|
||||
pub(crate) use super::fields::{ExprFields, StmtFields};
|
||||
pub(crate) use super::source_text::SourceText;
|
||||
pub(crate) use super::trivia::{
|
||||
DanglingIndentMode, FormatDanglingComments, FormatLeadingComments, FormatTrailingComments,
|
||||
format_dangling_comments, format_leading_comments, format_trailing_comments,
|
||||
dangling_comments, leading_comments, trailing_comments,
|
||||
};
|
||||
pub(crate) use super::utils::operators::{BinOpExt, Precedence, UnOpExt};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
//! The lookup table from a node to the ids of the comments attached to it.
|
||||
//!
|
||||
//! Almost every node that is looked up has no comments, so there is one bit per node that says whether it has any. Only if the
|
||||
//! bit is set is a list searched, which is sorted by [`Node::index`](bun_lint::ast::Node::index). The HIR stores which
|
||||
//! expressions are parenthesized the same way.
|
||||
|
||||
use super::ast_nodes::AstNodes;
|
||||
use super::attach::{Attached, Owner, Placement};
|
||||
use bun_lint::ast::{File, Node};
|
||||
use bun_lint::span::Span;
|
||||
|
||||
const LEADING: u8 = Placement::Leading as u8;
|
||||
const TRAILING: u8 = Placement::Trailing as u8;
|
||||
/// Used instead of a [`Placement`] for a group that has no comments and only marks the node as ignored (Prettier's
|
||||
/// `node.prettierIgnore`).
|
||||
const IGNORED: u8 = 3;
|
||||
|
||||
/// The comments of one node with one placement.
|
||||
#[derive(Copy, Clone)]
|
||||
struct CommentGroup {
|
||||
/// The [`AstKind`](super::ast_nodes::AstKind) of the node. Several `AstNodes` can share one node of `bun_lint::ast`.
|
||||
kind: u8,
|
||||
placement: u8,
|
||||
/// One of the comments is a `prettier-ignore` comment that applies to the node.
|
||||
ignores_owner: bool,
|
||||
/// A range of `Owners::comment_ids`.
|
||||
first: u32,
|
||||
len: u32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct OwnerEntry {
|
||||
/// [`Node::index`](bun_lint::ast::Node::index), or the start offset of a name or a `(`.
|
||||
id: u32,
|
||||
first_group: u32,
|
||||
groups: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Owners {
|
||||
/// Grouped by owner and placement, and in source order within a group.
|
||||
comment_ids: Vec<u32>,
|
||||
groups: Vec<CommentGroup>,
|
||||
/// Each is sorted by `id`.
|
||||
nodes: Vec<OwnerEntry>,
|
||||
names: Vec<OwnerEntry>,
|
||||
cast_parens: Vec<OwnerEntry>,
|
||||
/// One bit per node, by `Node::index`: whether the node is in `nodes`.
|
||||
owner_bitset: Vec<u64>,
|
||||
/// Start offsets of the owners that are not next to their comment. Sorted.
|
||||
relocated_owner_starts: Vec<u32>,
|
||||
}
|
||||
|
||||
/// The ids of the comments attached to one node.
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub(crate) struct OwnedIds<'a> {
|
||||
pub(crate) leading: &'a [u32],
|
||||
pub(crate) trailing: &'a [u32],
|
||||
pub(crate) dangling: &'a [u32],
|
||||
pub(crate) is_ignored: bool,
|
||||
}
|
||||
|
||||
fn sort_key(owner: Owner<'_>, placement: u8) -> u64 {
|
||||
let (owner_class, id, kind) = match owner {
|
||||
Owner::Node(node) => (0, node.raw().index(), node.kind() as u8),
|
||||
Owner::Name(start) => (1, start, 0),
|
||||
Owner::CastParens(_, open) => (2, open, 0),
|
||||
};
|
||||
(owner_class << 48) | (u64::from(id) << 16) | (u64::from(kind) << 8) | u64::from(placement)
|
||||
}
|
||||
|
||||
impl Owners {
|
||||
pub(crate) const EMPTY: Owners = Owners {
|
||||
comment_ids: Vec::new(),
|
||||
groups: Vec::new(),
|
||||
nodes: Vec::new(),
|
||||
names: Vec::new(),
|
||||
cast_parens: Vec::new(),
|
||||
owner_bitset: Vec::new(),
|
||||
relocated_owner_starts: Vec::new(),
|
||||
};
|
||||
|
||||
pub(crate) fn new(file: &File<'_>, attached: Attached<'_>) -> Owners {
|
||||
let mut sort_keys: Vec<(u64, u32)> =
|
||||
Vec::with_capacity(attached.attachments.len() + attached.ignored_nodes.len());
|
||||
for (index, attachment) in attached.attachments.iter().enumerate() {
|
||||
sort_keys.push((
|
||||
sort_key(attachment.owner, attachment.placement as u8),
|
||||
index as u32,
|
||||
));
|
||||
}
|
||||
for owner in &attached.ignored_nodes {
|
||||
sort_keys.push((sort_key(*owner, IGNORED), u32::MAX));
|
||||
}
|
||||
if !sort_keys.is_sorted() {
|
||||
crate::sort::sort(&mut sort_keys);
|
||||
}
|
||||
let mut owners = Owners {
|
||||
comment_ids: Vec::with_capacity(attached.attachments.len()),
|
||||
owner_bitset: vec![0; file.node_count() as usize / 64 + 1],
|
||||
relocated_owner_starts: attached.relocated_owner_starts,
|
||||
..Owners::default()
|
||||
};
|
||||
let mut previous_key = u64::MAX;
|
||||
for (sort_key, index) in sort_keys {
|
||||
let (owner_class, id) = (sort_key >> 48, (sort_key >> 16) as u32);
|
||||
if sort_key >> 16 != previous_key >> 16 || previous_key == u64::MAX {
|
||||
let entry = OwnerEntry {
|
||||
id,
|
||||
first_group: owners.groups.len() as u32,
|
||||
groups: 0,
|
||||
};
|
||||
match owner_class {
|
||||
0 => {
|
||||
owners.nodes.push(entry);
|
||||
if let Some(word) = owners.owner_bitset.get_mut(id as usize / 64) {
|
||||
*word |= 1 << (id % 64);
|
||||
}
|
||||
}
|
||||
1 => owners.names.push(entry),
|
||||
_ => owners.cast_parens.push(entry),
|
||||
}
|
||||
}
|
||||
let entries = match owner_class {
|
||||
0 => &mut owners.nodes,
|
||||
1 => &mut owners.names,
|
||||
_ => &mut owners.cast_parens,
|
||||
};
|
||||
if sort_key != previous_key
|
||||
&& let Some(entry) = entries.last_mut()
|
||||
{
|
||||
entry.groups += 1;
|
||||
owners.groups.push(CommentGroup {
|
||||
kind: (sort_key >> 8) as u8,
|
||||
placement: sort_key as u8,
|
||||
ignores_owner: false,
|
||||
first: owners.comment_ids.len() as u32,
|
||||
len: 0,
|
||||
});
|
||||
}
|
||||
previous_key = sort_key;
|
||||
if let (Some(attachment), Some(group)) = (
|
||||
attached.attachments.get(index as usize),
|
||||
owners.groups.last_mut(),
|
||||
) {
|
||||
owners.comment_ids.push(index);
|
||||
group.len += 1;
|
||||
group.ignores_owner |= attachment.ignores_owner;
|
||||
}
|
||||
}
|
||||
owners
|
||||
}
|
||||
|
||||
fn lookup(&self, entries: &[OwnerEntry], id: u32, kind: u8) -> OwnedIds<'_> {
|
||||
let mut owned = OwnedIds::default();
|
||||
let Ok(at) = entries.binary_search_by_key(&id, |entry| entry.id) else {
|
||||
return owned;
|
||||
};
|
||||
let Some(entry) = entries.get(at) else {
|
||||
return owned;
|
||||
};
|
||||
let groups = (self
|
||||
.groups
|
||||
.get(entry.first_group as usize..)
|
||||
.unwrap_or_default())
|
||||
.iter()
|
||||
.take(entry.groups as usize)
|
||||
.filter(|group| group.kind == kind);
|
||||
for group in groups {
|
||||
let ids = (self.comment_ids)
|
||||
.get(group.first as usize..(group.first + group.len) as usize)
|
||||
.unwrap_or_default();
|
||||
match group.placement {
|
||||
IGNORED => owned.is_ignored = true,
|
||||
LEADING => owned.leading = ids,
|
||||
TRAILING => owned.trailing = ids,
|
||||
_ => owned.dangling = ids,
|
||||
}
|
||||
owned.is_ignored |= group.ignores_owner;
|
||||
}
|
||||
owned
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn of_node(&self, node: AstNodes<'_>) -> OwnedIds<'_> {
|
||||
let raw = node.raw();
|
||||
match self.is_owner(raw) {
|
||||
true => self.lookup(&self.nodes, raw.index(), node.kind() as u8),
|
||||
false => OwnedIds::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether one of the `AstNodes` that are made of `raw` owns a comment or is ignored.
|
||||
#[inline]
|
||||
pub(crate) fn is_owner(&self, raw: Node<'_>) -> bool {
|
||||
let index = raw.index();
|
||||
(self.owner_bitset.get(index as usize / 64))
|
||||
.is_some_and(|word| word & (1 << (index % 64)) != 0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn of_name(&self, start: u32) -> OwnedIds<'_> {
|
||||
match self.names.is_empty() {
|
||||
true => OwnedIds::default(),
|
||||
false => self.lookup(&self.names, start, 0),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn of_cast_parens(&self, open: u32) -> OwnedIds<'_> {
|
||||
match self.cast_parens.is_empty() {
|
||||
true => OwnedIds::default(),
|
||||
false => self.lookup(&self.cast_parens, open, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a node that starts inside `span` owns a comment that is not next to it.
|
||||
#[inline]
|
||||
pub(crate) fn has_relocated_owner_in(&self, span: Span) -> bool {
|
||||
!self.relocated_owner_starts.is_empty() && {
|
||||
let at = self
|
||||
.relocated_owner_starts
|
||||
.partition_point(|start| *start < span.start);
|
||||
self.relocated_owner_starts
|
||||
.get(at)
|
||||
.is_some_and(|start| *start < span.end)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use crate::js::ast_nodes::ExpressionStatement;
|
||||
use crate::js::print::binary_like_expression::{is_angular_pipe, should_flatten};
|
||||
use crate::js::print::expressions::unary_argument_has_comments;
|
||||
use crate::js::utils::typecast::is_cast_target;
|
||||
use crate::js::print::return_or_throw_statement::has_argument_leading_comments;
|
||||
use crate::js::utils::typecast::{has_comments, has_comments_that_count, is_cast_target};
|
||||
use crate::prelude::*;
|
||||
use AstNodes as N;
|
||||
|
||||
@@ -40,6 +40,190 @@ pub(crate) fn chain_expression_needs_parentheses<'a>(e: Expr<'a>, f: &Formatter<
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the source text of `e`, which is in parentheses there, means something else without them, where `e` itself
|
||||
/// needs none. What is formatted gets them further in, or is written otherwise: `({}.a) && b` is `({}).a && b`,
|
||||
/// `(new A).b` is `new A().b`. Prettier leaves them out of what it writes as it is.
|
||||
pub(crate) fn source_text_needs_its_parentheses<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
// The parentheses of a type cast are written anyway.
|
||||
if (e.parens()).all(|paren| f.comments().is_cast_parenthesis(paren.start)) {
|
||||
return false;
|
||||
}
|
||||
// What follows would be taken for a part of it, or `<T>` for two comparisons.
|
||||
let continues_with: &[u8] = match e.kind() {
|
||||
// `(a<T>) + b`, `(a<T>).b`, `(a<T>)()`, `(a<T>)<U>`, `(a<T>)!`
|
||||
ExprKind::Instantiation { .. } => b"+-<>.[(`!",
|
||||
// `(new A).b`, `(new A)()`, `(new A)<T>`, `(new A)!.b`
|
||||
ExprKind::New(call) if call.close_paren().is_none() => b".[(`<!",
|
||||
_ => b"",
|
||||
};
|
||||
if !continues_with.is_empty()
|
||||
&& start_of_token_after(e, f)
|
||||
.is_some_and(|byte| bun_core::strings::contains_char(continues_with, byte))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// What it starts with: `{`, `function`, `class` and `let[` start a statement.
|
||||
let mut first = e;
|
||||
while let Some(next) = match first.kind() {
|
||||
ExprKind::Dot { obj, .. } | ExprKind::Index { obj, .. } => Some(obj),
|
||||
ExprKind::Call(call) | ExprKind::TaggedTemplate(call) => Some(call.callee()),
|
||||
ExprKind::Cond { test, .. } => Some(test),
|
||||
ExprKind::As { expr, .. }
|
||||
| ExprKind::AsConst(expr)
|
||||
| ExprKind::Satisfies { expr, .. }
|
||||
| ExprKind::NonNull(expr)
|
||||
| ExprKind::Instantiation { expr, .. }
|
||||
if !first.is_angle_bracket_assertion() =>
|
||||
{
|
||||
Some(expr)
|
||||
}
|
||||
ExprKind::Unary {
|
||||
op: UnOp::PostInc | UnOp::PostDec,
|
||||
operand,
|
||||
} => Some(operand),
|
||||
_ => first.left(),
|
||||
} {
|
||||
if next.is_parenthesized() {
|
||||
break;
|
||||
}
|
||||
if match next.kind() {
|
||||
ExprKind::Ident(_) => needs_parentheses(next, f),
|
||||
_ => would_start_statement(next, e),
|
||||
} {
|
||||
return true;
|
||||
}
|
||||
first = next;
|
||||
}
|
||||
// `in` ends the initializer of a `for`.
|
||||
is_in_for_statement_initializer(e, f) && has_bare_in(e)
|
||||
}
|
||||
|
||||
/// The first byte of the token that is written after `e`, if it is a token of an expression that `e` is in. `None` for what ends
|
||||
/// an expression: `)`, `]`, `}`, `;`.
|
||||
///
|
||||
/// `b + e + c`: `e` is the end of `b + e`, and the `+` of `(b + e) + c` is written after it.
|
||||
fn start_of_token_after<'a>(e: Expr<'a>, f: &Formatter<'a>) -> Option<u8> {
|
||||
let mut current = e;
|
||||
loop {
|
||||
let parent = match current.parent() {
|
||||
Node::Expr(parent) => parent,
|
||||
// The body of an arrow function.
|
||||
Node::Func(func) => func.owner().as_expr()?,
|
||||
_ => return None,
|
||||
};
|
||||
let is = |child: Expr<'a>| child == current;
|
||||
match parent.kind() {
|
||||
ExprKind::Dot { obj, .. } if is(obj) => return Some(b'.'),
|
||||
ExprKind::Index { obj, .. } if is(obj) => return Some(b'['),
|
||||
ExprKind::Call(call) | ExprKind::New(call) if is(call.callee()) => return Some(b'('),
|
||||
ExprKind::TaggedTemplate(call) if is(call.callee()) => return Some(b'`'),
|
||||
ExprKind::NonNull(_) => return Some(b'!'),
|
||||
ExprKind::Instantiation { .. } => return Some(b'<'),
|
||||
ExprKind::Binary { op, left, .. } if is(left) => {
|
||||
return op.as_str().as_bytes().first().copied();
|
||||
}
|
||||
ExprKind::Unary { op, .. } if !op.is_prefix() => {
|
||||
return op.as_str().as_bytes().first().copied();
|
||||
}
|
||||
ExprKind::Cond { test, .. } if is(test) => return Some(b'?'),
|
||||
ExprKind::Cond { yes, .. } if is(yes) => return Some(b':'),
|
||||
ExprKind::Assign { target, .. } if is(target) => return Some(b'='),
|
||||
ExprKind::As { .. } | ExprKind::AsConst(_) | ExprKind::Satisfies { .. }
|
||||
if !parent.is_angle_bracket_assertion() =>
|
||||
{
|
||||
return Some(b'a');
|
||||
}
|
||||
// It writes parentheses around an operand with comments.
|
||||
ExprKind::Unary { .. } if !f.is_quiet() && has_comments_that_count(current, f) => {
|
||||
return None;
|
||||
}
|
||||
// `current` is the end of `parent`.
|
||||
ExprKind::Binary { .. }
|
||||
| ExprKind::Unary { .. }
|
||||
| ExprKind::Cond { .. }
|
||||
| ExprKind::Assign { .. }
|
||||
| ExprKind::As { .. }
|
||||
| ExprKind::AsConst(_)
|
||||
| ExprKind::Await(_)
|
||||
| ExprKind::Yield { .. }
|
||||
| ExprKind::Fn(_) => {}
|
||||
// An argument, an element, an index, ..
|
||||
_ => return None,
|
||||
}
|
||||
if is_cast_target(parent, f) || expression_needs_parentheses(parent, f) {
|
||||
return None;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `first`, which the text of `e` starts with, would be taken for the start of a block, a function declaration or a
|
||||
/// class declaration if `e` had no parentheses.
|
||||
fn would_start_statement<'a>(first: Expr<'a>, e: Expr<'a>) -> bool {
|
||||
let (is_object, is_declaration) = match first.kind() {
|
||||
ExprKind::Object(_) => (true, false),
|
||||
ExprKind::Fn(func) => (false, !func.is_arrow()),
|
||||
ExprKind::Class(_) => (false, true),
|
||||
_ => (false, false),
|
||||
};
|
||||
if !is_object && !is_declaration {
|
||||
return false;
|
||||
}
|
||||
let (outermost, container) = left_edge_end(e, e.as_ast_nodes());
|
||||
let is_sequence = outermost.binary_op() == Some(BinOp::Comma);
|
||||
match container {
|
||||
// A sequence is in parentheses there.
|
||||
N::ExpressionStatement(ExpressionStatement::Stmt(_)) => !is_sequence,
|
||||
// So are a sequence and an assignment there.
|
||||
N::ExpressionStatement(ExpressionStatement::ArrowBody(_)) => {
|
||||
is_object && !is_sequence && !matches!(outermost.kind(), ExprKind::Assign { .. })
|
||||
}
|
||||
N::ExportDefaultDeclaration(_) => is_declaration,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether there is an `a in b` in `e` that no parentheses, brackets or braces in `e` are around.
|
||||
fn has_bare_in(e: Expr<'_>) -> bool {
|
||||
// A loop: what is written as it is can be nested to any depth.
|
||||
let mut pending = vec![e];
|
||||
while let Some(next) = pending.pop() {
|
||||
if next.binary_op() == Some(BinOp::In) {
|
||||
return true;
|
||||
}
|
||||
let (first, second) = match next.kind() {
|
||||
ExprKind::Binary { left, right, .. } => (Some(left), Some(right)),
|
||||
ExprKind::Assign { target, value, .. } => (Some(target), Some(value)),
|
||||
// Any expression can be between `?` and `:`.
|
||||
ExprKind::Cond { test, no, .. } => (Some(test), Some(no)),
|
||||
ExprKind::Dot { obj, .. } | ExprKind::Index { obj, .. } => (Some(obj), None),
|
||||
ExprKind::Call(call) | ExprKind::New(call) | ExprKind::TaggedTemplate(call) => {
|
||||
(Some(call.callee()), None)
|
||||
}
|
||||
ExprKind::Unary { operand: it, .. }
|
||||
| ExprKind::Await(it)
|
||||
| ExprKind::As { expr: it, .. }
|
||||
| ExprKind::Satisfies { expr: it, .. }
|
||||
| ExprKind::AsConst(it)
|
||||
| ExprKind::NonNull(it)
|
||||
| ExprKind::Instantiation { expr: it, .. } => (Some(it), None),
|
||||
ExprKind::Yield { value, .. } => (value, None),
|
||||
ExprKind::Fn(func) => match func.body() {
|
||||
FnBody::Expr(body) if func.is_arrow() => (Some(body), None),
|
||||
_ => (None, None),
|
||||
},
|
||||
_ => (None, None),
|
||||
};
|
||||
pending.extend(
|
||||
[first, second]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|it| !it.is_parenthesized()),
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether `e` itself needs parentheses, not the `ChainExpression` around it.
|
||||
#[inline]
|
||||
pub(crate) fn needs_parentheses<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
@@ -252,8 +436,9 @@ fn needs_parentheses_where_it_is<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
op: BinOp::Comma, ..
|
||||
} => match parent {
|
||||
N::ForStatement(_) => false,
|
||||
// Written by `print/return_or_throw_statement.rs` and `print/arrow_function_expression.rs`.
|
||||
N::ReturnStatement(_) | N::ThrowStatement(_) => false,
|
||||
// The statement writes parentheses around a comment and what follows it.
|
||||
N::ReturnStatement(_) | N::ThrowStatement(_) => !has_argument_leading_comments(e, f),
|
||||
// Written by `print/arrow_function_expression.rs`.
|
||||
N::ExpressionStatement(statement) => !statement.is_arrow_function_body(),
|
||||
_ => true,
|
||||
},
|
||||
@@ -605,27 +790,11 @@ fn assignment_needs_parentheses<'a>(
|
||||
!matches!(parent.parent(), N::ForStatement(statement) if is_init_or_update(statement, sequence))
|
||||
}
|
||||
// The statement writes parentheses around a comment and what follows it.
|
||||
N::ReturnStatement(statement) | N::ThrowStatement(statement) => {
|
||||
!has_own_line_comment_between(statement.span().start, e.span().start, f)
|
||||
}
|
||||
N::ReturnStatement(_) | N::ThrowStatement(_) => !has_argument_leading_comments(e, f),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether one of the comments between `start` and `end`, printed or not, ends its line or spans
|
||||
/// several.
|
||||
pub(crate) fn has_own_line_comment_between(start: u32, end: u32, f: &Formatter<'_>) -> bool {
|
||||
let breaks = |comment: &Comment| comment.followed_by_newline() || comment.is_multiline_block();
|
||||
let comments = f.comments();
|
||||
comments
|
||||
.printed_comments()
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|comment| comment.start() >= start)
|
||||
.any(breaks)
|
||||
|| comments.comments_before_iter(end).any(breaks)
|
||||
}
|
||||
|
||||
/// `<T>a | 0`: Prettier puts `<T>a` in parentheses next to `|`, `&`, `^` and the shifts. oxfmt only where it has to: before
|
||||
/// and after `<<`, and before `**`.
|
||||
fn type_assertion_is_like_unary_expression(f: &Formatter<'_>) -> bool {
|
||||
@@ -659,9 +828,8 @@ fn binary_or_cast_needs_parentheses<'a>(
|
||||
| N::TSNonNullExpression(_)
|
||||
| N::UpdateExpression(_) => return true,
|
||||
// It writes parentheses around an argument with comments. For oxfmt `a as T` has its own in them.
|
||||
N::UnaryExpression(unary) => {
|
||||
return (is_binary_cast && f.options().flavor.is_oxfmt())
|
||||
|| !unary_argument_has_comments(unary, e, f);
|
||||
N::UnaryExpression(_) => {
|
||||
return (is_binary_cast && f.options().flavor.is_oxfmt()) || !has_comments(e, f);
|
||||
}
|
||||
N::AssignmentExpression(assignment) | N::AssignmentTargetWithDefault(assignment) => {
|
||||
return operator.is_none() && assignment.left() == Some(e);
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
use crate::js::format::write_trailing_comments_of;
|
||||
use crate::js::utils::array::{is_line_after_element_empty, write_array_node};
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::typecast::has_comments;
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
|
||||
/// The elements of the array literal `array`.
|
||||
/// The elements of an array literal.
|
||||
pub(crate) struct ArrayElementList<'a> {
|
||||
array: Expr<'a>,
|
||||
elements: List<'a, Expr<'a>>,
|
||||
group_id: GroupId,
|
||||
}
|
||||
|
||||
impl<'a> ArrayElementList<'a> {
|
||||
pub(crate) fn new(array: Expr<'a>, elements: List<'a, Expr<'a>>, group_id: GroupId) -> Self {
|
||||
Self {
|
||||
array,
|
||||
elements,
|
||||
group_id,
|
||||
}
|
||||
pub(crate) fn new(elements: List<'a, Expr<'a>>, group_id: GroupId) -> Self {
|
||||
Self { elements, group_id }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for ArrayElementList<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if !can_concisely_print_array_list(self.array.span(), self.elements, f) {
|
||||
if !can_concisely_print_array_list(self.elements, f) {
|
||||
// One per line, if they do not fit on one line.
|
||||
return write_array_node(
|
||||
self.elements.len(),
|
||||
@@ -40,11 +36,8 @@ impl<'a> Format<'a> for ArrayElementList<'a> {
|
||||
// a line break in an item of a fill puts the item on a line of its own.
|
||||
let last_with_own_line_comment = self.elements.last().filter(|last| {
|
||||
!f.is_quiet()
|
||||
&& (f
|
||||
.comments()
|
||||
.comments_in(Span::after(last.span(), self.array.span().end))
|
||||
.first())
|
||||
.is_some_and(|comment| comment.preceded_by_newline())
|
||||
&& (f.comments().of_node(last.as_ast_nodes()).trailing.first())
|
||||
.is_some_and(|comment| comment.has_newline_before())
|
||||
});
|
||||
let mut filler = f.fill();
|
||||
let mut previous_end = 0;
|
||||
@@ -59,12 +52,14 @@ impl<'a> Format<'a> for ArrayElementList<'a> {
|
||||
previous_end as usize,
|
||||
) {
|
||||
write!(f, empty_line());
|
||||
} else if f.comments().comments_before_iter(element.span().start).any(
|
||||
|comment| match block_comment_that_ends_line_starts_one(f) {
|
||||
true => comment.followed_by_newline(),
|
||||
false => comment.is_line(),
|
||||
},
|
||||
) {
|
||||
} else if !f.is_quiet()
|
||||
&& (f.comments().of_node(element.as_ast_nodes()).leading.iter()).any(
|
||||
|comment| match block_comment_that_ends_line_starts_one(f) {
|
||||
true => comment.has_newline_after(),
|
||||
false => comment.is_line(),
|
||||
},
|
||||
)
|
||||
{
|
||||
write!(f, hard_line_break());
|
||||
} else {
|
||||
write!(f, soft_line_break_or_space());
|
||||
@@ -89,50 +84,39 @@ fn block_comment_that_ends_line_starts_one(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier's `isConciselyPrintedArray`: all elements are numbers, with or without a sign.
|
||||
/// Prettier's `isConciselyPrintedArray`: all elements are numbers, with or without a sign, and none is followed by a line
|
||||
/// comment on its line.
|
||||
pub(crate) fn can_concisely_print_array_list<'a>(
|
||||
array_expression_span: Span,
|
||||
list: List<'a, Expr<'a>>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
let Some(first) = list.first() else {
|
||||
return false;
|
||||
};
|
||||
let comments = f.comments();
|
||||
let mut comments_iter = comments
|
||||
.comments_before_iter(array_expression_span.end)
|
||||
.peekable();
|
||||
|
||||
for item in list {
|
||||
match item.kind() {
|
||||
ExprKind::Number(_) => {}
|
||||
ExprKind::Unary {
|
||||
op: UnOp::Plus | UnOp::Minus,
|
||||
operand,
|
||||
} if matches!(operand.kind(), ExprKind::Number(_)) => {
|
||||
// `-(/* comment */ 1)`
|
||||
let span = item.span();
|
||||
while comments_iter
|
||||
.next_if(|comment| comment.span.start <= span.start)
|
||||
.is_some()
|
||||
{}
|
||||
if comments_iter
|
||||
.peek()
|
||||
.is_some_and(|it| span.contains(it.span))
|
||||
{
|
||||
return false;
|
||||
!list.is_empty()
|
||||
&& list.iter().all(|element| {
|
||||
let is_number = match element.kind() {
|
||||
ExprKind::Number(_) => true,
|
||||
ExprKind::Unary {
|
||||
op: UnOp::Plus | UnOp::Minus,
|
||||
operand,
|
||||
} => {
|
||||
matches!(operand.kind(), ExprKind::Number(_))
|
||||
&& (f.is_quiet() || !has_comments(operand, f))
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
|
||||
// Not with a line comment behind an element.
|
||||
!comments
|
||||
.comments_before_iter(array_expression_span.end)
|
||||
.any(|comment| {
|
||||
comment.is_line()
|
||||
&& !comment.preceded_by_newline()
|
||||
&& comment.span.start > first.span().start
|
||||
_ => false,
|
||||
};
|
||||
is_number && !has_line_comment_behind_code(element, f)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a line comment trails `element` on its line. For oxfmt also one that leads it: `[ // comment⏎1, 2]`.
|
||||
fn has_line_comment_behind_code<'a>(element: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
let owned = f.comments().of_node(element.as_ast_nodes());
|
||||
let leading = match f.options().flavor.is_oxfmt() {
|
||||
true => owned.leading,
|
||||
false => CommentList::empty(),
|
||||
};
|
||||
(leading.iter().chain(owned.trailing))
|
||||
.any(|comment| comment.is_line() && !comment.has_newline_before())
|
||||
}
|
||||
@@ -8,52 +8,28 @@ pub(crate) fn write_array_expression<'a>(
|
||||
elements: List<'a, Expr<'a>>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let node = AstNodes::ArrayExpression(e);
|
||||
write!(f, "[");
|
||||
if elements.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
format_dangling_comments(e.span()).with_soft_block_indent()
|
||||
);
|
||||
write!(f, dangling_comments(node).with_soft_block_indent());
|
||||
} else {
|
||||
let group_id = f.group_id("array");
|
||||
// `[, , /* comment */]`: there is no element that the comment could belong to.
|
||||
let has_line_comment = !f.is_quiet()
|
||||
&& elements.iter().all(Expr::is_missing)
|
||||
&& f.comments()
|
||||
.comments_in(e.span())
|
||||
.iter()
|
||||
.any(|comment| comment.is_line());
|
||||
// Where the comma ends that follows the last element that is no hole, if holes follow it.
|
||||
let holes_start = (!f.is_quiet()
|
||||
&& comments_stay_behind_holes(f)
|
||||
&& elements.last().is_some_and(Expr::is_missing))
|
||||
.then(|| elements.iter().rev().find(|it| !it.is_missing()))
|
||||
.flatten()
|
||||
.map(|last| f.comments().position_after_character(last.span().end, b','));
|
||||
let list = format_with(|f| {
|
||||
let previous_limit =
|
||||
holes_start.map(|start| f.comments_mut().limit_comments_up_to(start));
|
||||
write!(f, ArrayElementList::new(e, elements, group_id));
|
||||
if let Some(previous_limit) = previous_limit {
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
}
|
||||
});
|
||||
let content = format_args!(list, format_dangling_comments(e.span()));
|
||||
let has_dangling_line_comment = !f.is_quiet()
|
||||
&& (f.comments().of_node(node).dangling.iter()).any(|comment| comment.is_line());
|
||||
let content = format_args!(
|
||||
ArrayElementList::new(elements, group_id),
|
||||
dangling_comments(node)
|
||||
);
|
||||
write!(
|
||||
f,
|
||||
group(&soft_block_indent(&content))
|
||||
.with_group_id(Some(group_id))
|
||||
.should_expand(has_line_comment || should_break(elements))
|
||||
.should_expand(has_dangling_line_comment || should_break(elements))
|
||||
);
|
||||
}
|
||||
write!(f, "]");
|
||||
}
|
||||
|
||||
/// `[1, , /* comment */]`: Prettier attaches the comment to the `1`. oxfmt leaves it where it is.
|
||||
fn comments_stay_behind_holes(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier's `isConciselyPrintedArray`'s opposite `shouldBreak`: there are at least two elements,
|
||||
/// all are arrays or all are objects, and each has at least two elements.
|
||||
fn should_break<'a>(elements: List<'a, Expr<'a>>) -> bool {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
use super::class::dangling_comments_with_marker;
|
||||
use super::function::{FormatContentWithCacheMode, FormatFunctionBody};
|
||||
use super::parameters::{FormatFormalParameters, comments_between, has_only_simple_parameters};
|
||||
use super::sequence_expression::span_that_comments_lead;
|
||||
use super::parameters::{FormatFormalParameters, has_only_simple_parameters};
|
||||
use super::type_parameters::type_parameters;
|
||||
use crate::js::attach::Marker;
|
||||
use crate::js::format::{ExprOptions, FormatTypeAnnotation, write_expression};
|
||||
use crate::js::trivia::comments_stay_between_head_and_body;
|
||||
use crate::js::utils::assignment_like::AssignmentLikeLayout;
|
||||
use crate::js::utils::expression::ExpressionLeftSide;
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::js::utils::typecast::is_cast_target;
|
||||
use crate::js::utils::typecast::{
|
||||
cast_parentheses, has_leading_own_line_comment, is_cast_target,
|
||||
is_cast_target_that_hides_its_kind,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub(crate) struct FormatJsArrowFunctionExpressionOptions {
|
||||
@@ -64,9 +68,12 @@ fn is_sequence(e: Expr<'_>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// The arrow function that is the body of `arrow`.
|
||||
fn next_in_chain(arrow: Func<'_>) -> Option<Func<'_>> {
|
||||
get_expression(arrow).and_then(|body| body.arrow_function())
|
||||
/// The arrow function that is the body of `arrow`. For Prettier one in the parentheses of a type cast is a
|
||||
/// `ParenthesizedExpression`: `() => /** @type {T} */ ((a) => a)`
|
||||
fn next_in_chain<'a>(arrow: Func<'a>, f: &Formatter<'a>) -> Option<Func<'a>> {
|
||||
get_expression(arrow)
|
||||
.filter(|body| !is_cast_target_that_hides_its_kind(*body, f))
|
||||
.and_then(|body| body.arrow_function())
|
||||
}
|
||||
|
||||
/// `f(a, b =>⏎ // comment⏎ c ? d : e)`: for oxfmt the line of the `=>` is one column longer than it
|
||||
@@ -95,6 +102,12 @@ fn first_signature_starts_where_chain_starts(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `((a) /* comment */ => (b) =>⏎c⏎)()` for Prettier, for which any comment of the first arrow function counts.
|
||||
/// `(⏎(a) /* comment */ => (b) =>⏎c⏎)()` for oxfmt, for which `a /* comment */ = (b) => (c) => d` does not count either.
|
||||
fn only_comment_directly_before_chain_replaces_line_break(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier's `printArrowFunction`. `e`: the expression that `arrow` is.
|
||||
pub(crate) fn write_arrow_function_expression<'a>(
|
||||
e: Expr<'a>,
|
||||
@@ -124,20 +137,26 @@ fn write_arrow<'a>(
|
||||
// (b) =>
|
||||
// (c) =>
|
||||
// d;
|
||||
let should_print_as_chain = !expand_last_arg && next_in_chain(arrow).is_some();
|
||||
let should_print_as_chain = !expand_last_arg && next_in_chain(arrow, f).is_some();
|
||||
let mut tail = arrow;
|
||||
let mut should_break_chain = false;
|
||||
if should_print_as_chain {
|
||||
loop {
|
||||
should_break_chain = should_break_chain || has_complex_signature(tail);
|
||||
match next_in_chain(tail) {
|
||||
match next_in_chain(tail, f) {
|
||||
Some(next) => tail = next,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_own_line_comment = has_leading_own_line_comment(tail, options.cache_mode, f);
|
||||
let has_own_line_comment = body_has_leading_own_line_comment(tail, f);
|
||||
let is_block_after_comments = has_own_line_comment
|
||||
&& options.call_argument_layout.is_some()
|
||||
&& !is_nested
|
||||
&& get_expression(tail).is_none()
|
||||
&& block_of_grouped_argument_stays_after_arrow(f);
|
||||
let has_own_line_comment = has_own_line_comment && !is_block_after_comments;
|
||||
// For Prettier the body is a `ParenthesizedExpression` then, whatever is in it.
|
||||
let is_body_type_cast = get_expression(tail).is_some_and(|body| is_cast_target(body, f));
|
||||
let add_parens_if_not_break = !is_body_type_cast && should_add_parens_if_not_break(tail);
|
||||
@@ -160,20 +179,27 @@ fn write_arrow<'a>(
|
||||
options,
|
||||
has_own_line_comment,
|
||||
};
|
||||
// Prettier's `bodyComments`: the trailing comments of the arrow functions after the first in a chain.
|
||||
let format_body_comments = format_with(|f| {
|
||||
if should_print_as_chain && !f.is_quiet() {
|
||||
write_trailing_comments_of_chain(arrow, options.cache_mode, f);
|
||||
}
|
||||
});
|
||||
// Prettier's `printArrowFunctionBody`.
|
||||
let format_body = format_with(|f| {
|
||||
if is_body_on_same_line && !add_parens_if_not_break {
|
||||
return write!(f, [space(), format_body]);
|
||||
if is_block_after_comments
|
||||
&& write_comments_on_line_of_arrow_for_oxfmt(tail, options.cache_mode, f)
|
||||
{
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
return write!(f, [space(), format_body, format_body_comments]);
|
||||
}
|
||||
let trailing_comma = expand_last_arg.then_some(FormatTrailingCommas::All);
|
||||
// The `)` of the call, or the `}` in JSX, is on a line of its own if the body is.
|
||||
let should_add_soft_line = (expand_last_arg
|
||||
&& !has_dangling_comments(arrow, options.cache_mode, f))
|
||||
|| matches!(
|
||||
e.ast_parent(),
|
||||
container @ AstNodes::JSXExpressionContainer(_)
|
||||
if !f.comments().has_comment_in_range(e.span().end, container.span().end)
|
||||
);
|
||||
|| matches!(e.ast_parent(), AstNodes::JSXExpressionContainer(_)))
|
||||
&& !f.comments().has_comments(arrow.as_ast_nodes());
|
||||
let trailing_line = should_add_soft_line.then_some(soft_line_break());
|
||||
if breaks_with_chain {
|
||||
write!(
|
||||
@@ -182,7 +208,8 @@ fn write_arrow<'a>(
|
||||
soft_line_indent_or_space(&format_args!(
|
||||
if_group_fits_on_line(&"("),
|
||||
format_body,
|
||||
if_group_fits_on_line(&")")
|
||||
if_group_fits_on_line(&")"),
|
||||
format_body_comments
|
||||
)),
|
||||
trailing_comma,
|
||||
trailing_line
|
||||
@@ -193,23 +220,18 @@ fn write_arrow<'a>(
|
||||
f,
|
||||
[
|
||||
space(),
|
||||
FormatConditionalBody(&format_body, trailing_comma, trailing_line)
|
||||
FormatConditionalBody(&format_body, trailing_comma, trailing_line),
|
||||
format_body_comments
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// See `comments_stay_between_head_and_body`.
|
||||
if !f.is_quiet()
|
||||
&& comments_stay_between_head_and_body(f)
|
||||
&& matches!(tail.body(), FnBody::Block(_))
|
||||
&& let Some(arrow_token) = tail.arrow_span()
|
||||
{
|
||||
let comments = f.comments().end_of_line_comments_after(arrow_token.end);
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
if !should_print_as_chain {
|
||||
write_comments_on_line_of_arrow_for_oxfmt(tail, options.cache_mode, f);
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
soft_line_indent_or_space(&format_body),
|
||||
soft_line_indent_or_space(&format_args!(format_body, format_body_comments)),
|
||||
trailing_comma,
|
||||
trailing_line
|
||||
]
|
||||
@@ -238,10 +260,15 @@ fn write_arrow<'a>(
|
||||
let is_callee = parent.is_call_like_callee(e);
|
||||
// As a callee or on the right of an assignment, all signatures are indented by one level.
|
||||
let should_indent_signatures = is_callee || options.assignment_layout.is_some();
|
||||
// Not after a comment, which has a line break of its own.
|
||||
// Prettier asks for `CommentCheckFlags.Leading & CommentCheckFlags.Line`, which is no flag at all.
|
||||
let follows_comment = match only_comment_directly_before_chain_replaces_line_break(f) {
|
||||
true => (f.comments().of_node(arrow.as_ast_nodes()).leading.last())
|
||||
.is_some_and(|comment| comment.next_token_start() >= e.outer_span().start),
|
||||
false => f.comments().has_comments(arrow.as_ast_nodes()),
|
||||
};
|
||||
let should_print_soft_line = should_indent_signatures
|
||||
&& options.assignment_layout != Some(AssignmentLikeLayout::BreakAfterOperator)
|
||||
&& !has_leading_comment(e, is_callee, f);
|
||||
&& !follows_comment;
|
||||
let should_break_signatures = options.assignment_layout
|
||||
== Some(AssignmentLikeLayout::ChainTailArrowFunction)
|
||||
|| (is_callee && (!is_body_on_same_line || breaks_with_chain));
|
||||
@@ -259,16 +286,9 @@ fn write_arrow<'a>(
|
||||
// Prettier's `printArrowFunctionSignatures`.
|
||||
let format_rest = format_with(|f| {
|
||||
let mut current = arrow;
|
||||
while let Some(next) = next_in_chain(current).filter(|_| current != tail) {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
space(),
|
||||
"=>",
|
||||
soft_line_break_or_space(),
|
||||
FormatCommentsBeforeArrow(next, options.cache_mode)
|
||||
]
|
||||
);
|
||||
while let Some(next) = next_in_chain(current, f).filter(|_| current != tail) {
|
||||
write!(f, [space(), "=>", soft_line_break_or_space()]);
|
||||
write_leading_comments_of_arrow(next, options.cache_mode, f);
|
||||
write!(f, FormatSignature::new(next, options, false));
|
||||
current = next;
|
||||
}
|
||||
@@ -312,7 +332,11 @@ fn write_arrow<'a>(
|
||||
space(),
|
||||
"=>",
|
||||
indent_if_group_breaks(&format_body, group_id),
|
||||
is_callee.then_some(if_group_breaks(&soft_line_break()).with_group_id(Some(group_id)))
|
||||
// `(⏎// comment⏎(a) => (b) =>⏎c⏎⏎)()`: another line break follows if there are comments in the parentheses. The empty
|
||||
// text makes it count.
|
||||
is_callee.then_some(
|
||||
if_group_breaks(&format_args!(soft_line_break(), "")).with_group_id(Some(group_id))
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
@@ -345,84 +369,21 @@ fn has_complex_signature(arrow: Func<'_>) -> bool {
|
||||
arrow.return_type().is_some() && arrow.params_with_this().next().is_some()
|
||||
}
|
||||
|
||||
/// Whether a comment, printed or not, is the last thing before `e`. Before the parentheses of a
|
||||
/// callee, it belongs to the call.
|
||||
fn has_leading_comment<'a>(e: Expr<'a>, is_callee: bool, f: &Formatter<'a>) -> bool {
|
||||
let comments = f.comments();
|
||||
let start = e.span().start;
|
||||
comments
|
||||
.comments_before(start)
|
||||
.last()
|
||||
.or_else(|| comments.printed_comments().last())
|
||||
.is_some_and(|comment| {
|
||||
comment.end() <= start
|
||||
&& (!is_callee || comment.start() >= e.outer_span().start)
|
||||
&& f.source_text()
|
||||
.all_bytes(Span::before(comment.end(), e.span()), |b| {
|
||||
b.is_ascii_whitespace() || b == b'('
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether there are comments in the `()` of `arrow`, or before its `=>`.
|
||||
fn has_dangling_comments<'a>(
|
||||
arrow: Func<'a>,
|
||||
cache_mode: FunctionCacheMode,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
if f.is_quiet() && matches!(cache_mode, FunctionCacheMode::NoCache) {
|
||||
return false;
|
||||
}
|
||||
let Some(arrow_token) = arrow.arrow_span() else {
|
||||
return false;
|
||||
};
|
||||
let params = FormatFormalParameters(arrow).span();
|
||||
let signature_end = arrow.return_type().map_or(params.end, |ty| ty.span().end);
|
||||
comments_between(Span::before(signature_end, arrow_token), f)
|
||||
.next()
|
||||
.is_some()
|
||||
|| (arrow.params_with_this().next().is_none()
|
||||
&& comments_between(params, f).next().is_some())
|
||||
}
|
||||
|
||||
/// Prettier's `hasLeadingOwnLineComment` for the body of `arrow`: then the body goes on the next
|
||||
/// line, after the comment.
|
||||
fn has_leading_own_line_comment<'a>(
|
||||
arrow: Func<'a>,
|
||||
cache_mode: FunctionCacheMode,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
if f.is_quiet() && matches!(cache_mode, FunctionCacheMode::NoCache) {
|
||||
return false;
|
||||
}
|
||||
let Some(arrow_token) = arrow.arrow_span() else {
|
||||
return false;
|
||||
};
|
||||
let has_preceding_node = arrow.params_with_this().next().is_some()
|
||||
|| !arrow.type_params().is_empty()
|
||||
|| arrow.return_type().is_some();
|
||||
// If nothing else is between them and the body, the comments before the `(` lead it as well.
|
||||
let start = match has_preceding_node {
|
||||
true => arrow_token.end,
|
||||
false => arrow.span().start,
|
||||
};
|
||||
let is_before_arrow_token = |comment: &Comment| {
|
||||
comment.start() >= FormatFormalParameters(arrow).span().start
|
||||
&& comment.end() <= arrow_token.start
|
||||
};
|
||||
let mut comments =
|
||||
comments_between(Span::before(start, AstNodes::FunctionBody(arrow).span()), f)
|
||||
.filter(|comment| has_preceding_node || !is_before_arrow_token(comment));
|
||||
/// Prettier's `hasLeadingOwnLineComment` for the body of `arrow`: the body goes on the next line, after the comment.
|
||||
fn body_has_leading_own_line_comment<'a>(arrow: Func<'a>, f: &Formatter<'a>) -> bool {
|
||||
match arrow.body() {
|
||||
FnBody::Expr(body) if matches!(body.kind(), ExprKind::Jsx(_)) => {
|
||||
comments.any(|comment| f.comments().is_suppression_comment(comment))
|
||||
_ if f.is_quiet() => false,
|
||||
FnBody::Expr(body) => has_leading_own_line_comment(body, f),
|
||||
_ => {
|
||||
// `() =>⏎/* comment */ {}` stays as it is for oxfmt. See `write_space_before_body`.
|
||||
let keeps_line_break_before = comments_stay_between_head_and_body(f);
|
||||
(f.comments().of_node(AstNodes::FunctionBody(arrow)).leading)
|
||||
.iter()
|
||||
.any(|comment| {
|
||||
comment.has_newline_after()
|
||||
|| (keeps_line_break_before && comment.has_newline_before())
|
||||
})
|
||||
}
|
||||
FnBody::Expr(_) => comments.any(|comment| comment.followed_by_newline()),
|
||||
// A comment at the end of the line of the `=>` is moved into the block, if there is
|
||||
// anything before the `=>` that it could belong to.
|
||||
_ => comments.any(|comment| {
|
||||
comment.followed_by_newline() && (comment.preceded_by_newline() || !has_preceding_node)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,21 +459,114 @@ fn should_add_parens_if_not_break(arrow: Func<'_>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// The comments before `arrow`, which is the body of another arrow function.
|
||||
struct FormatCommentsBeforeArrow<'a>(Func<'a>, FunctionCacheMode);
|
||||
/// Writes `content`, which prints `comments`. An argument of a call can be formatted twice, and a comment is only printed
|
||||
/// once, so what is written is kept under the span of the first comment.
|
||||
fn write_comments_once<'a>(
|
||||
comments: CommentList<'a>,
|
||||
content: impl Format<'a>,
|
||||
cache_mode: FunctionCacheMode,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if let Some(first) = comments.first() {
|
||||
FormatContentWithCacheMode::new(|| first.span(), content, cache_mode).fmt(f);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatCommentsBeforeArrow<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() && matches!(self.1, FunctionCacheMode::NoCache) {
|
||||
return;
|
||||
}
|
||||
let span = self.0.as_ast_nodes().span();
|
||||
FormatContentWithCacheMode::new(
|
||||
|| Span::empty(span.start),
|
||||
format_leading_comments(span),
|
||||
self.1,
|
||||
)
|
||||
.fmt(f);
|
||||
/// For oxfmt the leading comments of a block that are on the line of the `=>` stay there. The block writes the others.
|
||||
/// Returns whether the last of them ends the line.
|
||||
///
|
||||
/// ```js
|
||||
/// a = () => // comment
|
||||
/// {};
|
||||
/// ```
|
||||
fn write_comments_on_line_of_arrow_for_oxfmt<'a>(
|
||||
arrow: Func<'a>,
|
||||
cache_mode: FunctionCacheMode,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
if f.is_quiet()
|
||||
|| !comments_stay_between_head_and_body(f)
|
||||
|| !matches!(arrow.body(), FnBody::Block(_))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let owned = f.comments().of_node(AstNodes::FunctionBody(arrow));
|
||||
let leading_comments = owned.leading;
|
||||
let count = (leading_comments.iter())
|
||||
.take_while(|comment| !comment.has_newline_before())
|
||||
.count();
|
||||
let comments = leading_comments.slice(0, count);
|
||||
let content = FormatTrailingComments::Comments(comments);
|
||||
write_comments_once(comments, content, cache_mode, f);
|
||||
// `a(() => /* prettier-ignore */⏎{ b( ) })` is `a(() => /* prettier-ignore */ { b( ) })`
|
||||
(comments.last()).is_some_and(|comment| {
|
||||
comment.is_line() || (comment.has_newline_after() && !owned.is_ignored)
|
||||
})
|
||||
}
|
||||
|
||||
/// `arrow` is the body of another arrow function: `(a) => /* comment */ (b) => c`
|
||||
fn write_leading_comments_of_arrow<'a>(
|
||||
arrow: Func<'a>,
|
||||
cache_mode: FunctionCacheMode,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
// `() => /** @type {A} */ (/** @type {B} */ ((a) => a))` is `() => /** @type {A} */ /** @type {B} */ (a) => a` for oxfmt,
|
||||
// which leaves out the parentheses of a type cast here.
|
||||
for parenthesis in cast_parentheses_of_arrow(arrow, f) {
|
||||
let comments = f.comments().of_cast_parens(parenthesis.start).leading;
|
||||
let content = FormatLeadingComments::Comments(comments);
|
||||
write_comments_once(comments, content, cache_mode, f);
|
||||
}
|
||||
let comments = f.comments().of_node(arrow.as_ast_nodes()).leading;
|
||||
let content = FormatLeadingComments::Comments(comments);
|
||||
write_comments_once(comments, content, cache_mode, f);
|
||||
}
|
||||
|
||||
/// `arrow` is the body of another arrow function: `(a) => ((b) => c /* comment */)`
|
||||
fn write_trailing_comments_of_arrow<'a>(
|
||||
arrow: Func<'a>,
|
||||
cache_mode: FunctionCacheMode,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let comments = f.comments().of_node(arrow.as_ast_nodes()).trailing;
|
||||
let content = FormatTrailingComments::Comments(comments);
|
||||
write_comments_once(comments, content, cache_mode, f);
|
||||
for parenthesis in cast_parentheses_of_arrow(arrow, f).iter().rev() {
|
||||
let comments = f.comments().of_cast_parens(parenthesis.start).trailing;
|
||||
let content = FormatTrailingComments::Comments(comments);
|
||||
write_comments_once(comments, content, cache_mode, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// The parentheses of the type casts around `arrow`, from the outside in.
|
||||
fn cast_parentheses_of_arrow<'a>(arrow: Func<'a>, f: &Formatter<'a>) -> SmallVec<[Span; 2]> {
|
||||
match arrow.owner().as_expr() {
|
||||
Some(e) if is_cast_target(e, f) => cast_parentheses(e, f),
|
||||
_ => SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The trailing comments of the arrow functions in the chain that starts with `head`, from the innermost to the second.
|
||||
/// Those of `head` are written with the expression that it is.
|
||||
fn write_trailing_comments_of_chain<'a>(
|
||||
head: Func<'a>,
|
||||
cache_mode: FunctionCacheMode,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let mut chain: SmallVec<[Func<'a>; 4]> = SmallVec::new();
|
||||
let mut current = head;
|
||||
while let Some(next) = next_in_chain(current, f) {
|
||||
chain.push(next);
|
||||
current = next;
|
||||
}
|
||||
for &arrow in chain.iter().rev() {
|
||||
write_trailing_comments_of_arrow(arrow, cache_mode, f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,7 +604,6 @@ impl<'a> Format<'a> for FormatSignature<'a> {
|
||||
let params = FormatFormalParameters(arrow);
|
||||
let type_params = type_parameters(arrow.type_params(), Node::Func(arrow));
|
||||
let return_type = arrow.return_type().map(FormatTypeAnnotation);
|
||||
let return_type = return_type.as_ref().map(FormatNodeWithoutTrailingComments);
|
||||
let has_parameters = arrow.params_with_this().next().is_some();
|
||||
|
||||
// What loses its soft line breaks in an expanded argument is cached under the span of the
|
||||
@@ -594,26 +647,30 @@ impl<'a> Format<'a> for FormatSignature<'a> {
|
||||
))
|
||||
);
|
||||
|
||||
if f.is_quiet() && matches!(cache_mode, FunctionCacheMode::NoCache) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let Some(arrow_token) = arrow.arrow_span() else {
|
||||
return;
|
||||
};
|
||||
// If the first is from before the `(`, it leads the body, and takes the others along.
|
||||
let comments = f.comments().comments_before(arrow_token.start);
|
||||
let comments = match comments.first() {
|
||||
Some(first) if first.start() < params.span().end => &[][..],
|
||||
_ => comments,
|
||||
};
|
||||
// `(a) /* comment */ => b`. Prettier breaks the line between two such comments, which is a syntax error before `=>`.
|
||||
let comments =
|
||||
dangling_comments_with_marker(arrow.as_ast_nodes(), Marker::CommentBeforeArrow, f);
|
||||
let content = FormatTrailingComments::Comments(comments);
|
||||
write!(
|
||||
f,
|
||||
FormatContentWithCacheMode::new(|| arrow.as_ast_nodes().span(), content, cache_mode)
|
||||
);
|
||||
write_comments_once(comments, content, cache_mode, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// `a((b) => // comment⏎{ c; })` is `a((b) => // comment⏎{⏎ c;⏎});` for oxfmt. Anywhere else, as in `a((b) => (c) => ..)`,
|
||||
/// the comment and the block are indented, on lines of their own. Prettier moves the comment into the block:
|
||||
/// `a((b) => {⏎ // comment⏎ c;⏎});`
|
||||
fn block_of_grouped_argument_stays_after_arrow(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `a((b) => /* prettier-ignore */ (c) => { d( ) })`: oxfmt formats `(c) => ..`, as both do in a chain that is not the last
|
||||
/// argument of a call.
|
||||
fn arrow_in_arrow_is_never_ignored(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// The body of a function that is not an arrow function.
|
||||
pub(crate) struct FormatMaybeCachedFunctionBody<'a> {
|
||||
pub(crate) func: Func<'a>,
|
||||
@@ -642,107 +699,75 @@ impl<'a> Format<'a> for FormatArrowBody<'a> {
|
||||
|
||||
// In the expanded last argument of a call. Anywhere else it is part of a chain.
|
||||
if let FnBody::Expr(body) = arrow.body()
|
||||
&& let Some(next) = body.arrow_function()
|
||||
&& let Some(next) = next_in_chain(arrow, f)
|
||||
{
|
||||
// It is not written by way of `Expr`.
|
||||
if !f.context_mut().has_stack_left() {
|
||||
return;
|
||||
}
|
||||
write!(f, FormatCommentsBeforeArrow(next, cache_mode));
|
||||
return write_arrow(body, next, self.options, true, f);
|
||||
write_leading_comments_of_arrow(next, cache_mode, f);
|
||||
match !f.is_quiet()
|
||||
&& !arrow_in_arrow_is_never_ignored(f)
|
||||
&& f.comments().is_ignored(next.as_ast_nodes())
|
||||
{
|
||||
true => write!(f, FormatSuppressedNode(body.span())),
|
||||
false => write_arrow(body, next, self.options, true, f),
|
||||
}
|
||||
return write_trailing_comments_of_arrow(next, cache_mode, f);
|
||||
}
|
||||
|
||||
let span = || AstNodes::FunctionBody(arrow).span();
|
||||
let content = format_with(|f| match arrow.body() {
|
||||
FnBody::Expr(body) => write_expression_body(body, self.has_own_line_comment, f),
|
||||
_ => {
|
||||
// Otherwise the block takes the block comments, and the others are moved into it.
|
||||
if self.has_own_line_comment {
|
||||
write!(f, format_leading_comments(span()));
|
||||
}
|
||||
write!(f, FormatFunctionBody(arrow));
|
||||
}
|
||||
FnBody::Expr(body) => write_expression_body(arrow, body, self.has_own_line_comment, f),
|
||||
_ => write!(f, FormatFunctionBody(arrow)),
|
||||
});
|
||||
FormatContentWithCacheMode::new(span, content, cache_mode).fmt(f);
|
||||
}
|
||||
}
|
||||
|
||||
/// `() =>⏎ /* comment */ (a, b)` for oxfmt, `() => /* comment */ (a, b)` for Prettier.
|
||||
/// `() =>⏎ /* comment */ (a, b)` for oxfmt, `() => /* comment */ (a, b)` for Prettier. For oxfmt a comment anywhere in the
|
||||
/// arrow function before the sequence counts: `(/* comment */ a) =>⏎ (b, c)`.
|
||||
fn sequence_behind_comments_is_on_its_own_line(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `const a = (b) => (⏎b && c⏎// comment⏎);`: for oxfmt the comment is behind the statement. For Prettier it trails `b && c`.
|
||||
fn only_the_body_of_an_argument_keeps_comments_before_its_parenthesis(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `is_on_its_own_line`: a comment has moved it off the line of the `=>` already.
|
||||
fn write_expression_body<'a>(body: Expr<'a>, is_on_its_own_line: bool, f: &mut Formatter<'a>) {
|
||||
/// `body`: of `arrow`. `is_on_its_own_line`: a comment has moved it off the line of the `=>` already.
|
||||
fn write_expression_body<'a>(
|
||||
arrow: Func<'a>,
|
||||
body: Expr<'a>,
|
||||
is_on_its_own_line: bool,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let is_sequence = is_sequence(body) && !is_cast_target(body, f);
|
||||
if !is_sequence {
|
||||
return write!(f, body);
|
||||
}
|
||||
if f.is_quiet() {
|
||||
return write!(
|
||||
f,
|
||||
[is_sequence.then_some("("), body, is_sequence.then_some(")")]
|
||||
);
|
||||
return write!(f, ["(", body, ")"]);
|
||||
}
|
||||
if is_sequence {
|
||||
// The comments are outside of the parentheses.
|
||||
let span = body.span();
|
||||
let is_suppressed = f.comments().is_suppressed(span.start);
|
||||
let leading = span_that_comments_lead(body, f);
|
||||
let content = format_with(|f| {
|
||||
match f.comments().comments_before(span.start) {
|
||||
// But for one that is about the parentheses behind it.
|
||||
[others @ .., cast]
|
||||
if sequence_behind_comments_is_on_its_own_line(f)
|
||||
&& f.comments().is_cast_parenthesis(span.start) =>
|
||||
{
|
||||
let cast = FormatLeadingComments::Comments(std::slice::from_ref(cast));
|
||||
write!(f, [FormatLeadingComments::Comments(others), "(", cast]);
|
||||
}
|
||||
_ => write!(f, [format_leading_comments(leading), "("]),
|
||||
}
|
||||
match is_suppressed {
|
||||
true => write!(f, FormatSuppressedNode(span)),
|
||||
false => write_expression(body, ExprOptions::None, f),
|
||||
}
|
||||
write!(f, ")");
|
||||
});
|
||||
return match !is_on_its_own_line
|
||||
&& sequence_behind_comments_is_on_its_own_line(f)
|
||||
&& f.comments().has_comment_before(leading.start)
|
||||
{
|
||||
true => write!(f, group(&indent(&format_args!(hard_line_break(), content)))),
|
||||
false => write!(f, content),
|
||||
};
|
||||
}
|
||||
write!(f, body);
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_in(Span::after(body.span(), body.outer_span().end));
|
||||
let count = match body.kind() {
|
||||
// `(a ? b : c /* comment */)`: in the parentheses that are written if it does not break.
|
||||
ExprKind::Cond { .. } => comments
|
||||
.iter()
|
||||
.take_while(|comment| !comment.preceded_by_newline())
|
||||
.count(),
|
||||
// What is before the `)` of parentheses that go trails the body, with its indentation.
|
||||
ExprKind::Binary { .. }
|
||||
if !comments.is_empty()
|
||||
&& (!only_the_body_of_an_argument_keeps_comments_before_its_parenthesis(f)
|
||||
|| matches!(
|
||||
// The statement that the body is, the body, the function, what that is in.
|
||||
body.ast_parent().parent().parent().parent(),
|
||||
AstNodes::CallExpression(_) | AstNodes::NewExpression(_)
|
||||
)) =>
|
||||
{
|
||||
comments.len()
|
||||
// The comments are outside of the parentheses.
|
||||
let owned = f.comments().of_node(body.as_ast_nodes());
|
||||
let content = format_with(|f| {
|
||||
write!(f, [FormatLeadingComments::Comments(owned.leading), "("]);
|
||||
match owned.is_ignored {
|
||||
true => write!(f, FormatSuppressedNode(body.span())),
|
||||
false => write_expression(body, ExprOptions::None, f),
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments.get(..count).unwrap_or_default())
|
||||
);
|
||||
write!(f, ")");
|
||||
// `((/* prettier-ignore */ a, b) /* comment */)`: the comment trails `b`, which is copied without it.
|
||||
if owned.is_ignored
|
||||
&& let Some(last) = body.sequence().last()
|
||||
{
|
||||
write!(f, trailing_comments(last.as_chain_element()));
|
||||
}
|
||||
write!(f, FormatTrailingComments::Comments(owned.trailing));
|
||||
});
|
||||
match !is_on_its_own_line
|
||||
&& sequence_behind_comments_is_on_its_own_line(f)
|
||||
&& (!owned.leading.is_empty()
|
||||
|| (f.comments()).has_comment_in_span(Span::before(arrow.span().start, body.span())))
|
||||
{
|
||||
true => write!(f, group(&indent(&format_args!(hard_line_break(), content)))),
|
||||
false => write!(f, content),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! `a as T`, `a as const`, `a satisfies T`
|
||||
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::typecast::comments_of_expression;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -17,70 +18,120 @@ fn comments_stay_around_cast_operator(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// See [`comments_stay_around_cast_operator`]. `comments`: those between `expression` and the type.
|
||||
/// `is_union`: the type is a union, which places the comments before it itself.
|
||||
/// See [`comments_stay_around_cast_operator`]. `cast`: the whole expression. `type_annotation`: `None` for `const`.
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn write_with_comments_in_place<'a>(
|
||||
cast: Expr<'a>,
|
||||
expression: Expr<'a>,
|
||||
operation: &'static str,
|
||||
comments: &'a [Comment],
|
||||
is_union: bool,
|
||||
format_type: &dyn Format<'a>,
|
||||
type_annotation: Option<TypeNode<'a>>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
// One that is about the type has to be seen by it: then all are left to the type.
|
||||
let has_suppression_comment = comments
|
||||
.iter()
|
||||
.any(|comment| f.comments().is_suppression_comment(comment));
|
||||
let operator = operation.as_bytes().first().copied().unwrap_or_default();
|
||||
let before_operator_count = f
|
||||
.comments()
|
||||
.comments_before_character(expression.span().end, operator)
|
||||
.len()
|
||||
.min(comments.len());
|
||||
let (before_operator, after_operator) = comments.split_at(before_operator_count);
|
||||
let type_node = type_annotation.map(|ty| ty.as_ast_nodes());
|
||||
let after_expression = comments_of_expression(expression, f).trailing;
|
||||
let before_type = match type_node {
|
||||
Some(ty) => f.comments().of_node(ty).leading,
|
||||
// `const` is not a node. A comment of several lines before it trails the whole expression.
|
||||
None => {
|
||||
let comments = f.comments().of_node(cast.as_ast_nodes()).trailing;
|
||||
let count = (comments.iter())
|
||||
.take_while(|comment| comment.span().end <= cast.span().end)
|
||||
.count();
|
||||
comments.slice(0, count)
|
||||
}
|
||||
};
|
||||
let is_ignored = type_node.is_some_and(|ty| f.comments().is_ignored(ty));
|
||||
// A union places the comments before it itself.
|
||||
let is_union =
|
||||
!is_ignored && type_annotation.is_some_and(|ty| matches!(ty.kind(), TypeKind::Union(_)));
|
||||
|
||||
let expression_end = expression.outer_span().end;
|
||||
let before_operator_count = (after_expression.iter())
|
||||
.take_while(|comment| comment.prev_token_end() <= expression_end)
|
||||
.count();
|
||||
let (before_operator, after_operator) = after_expression.split_at(before_operator_count);
|
||||
// No line break can be before the operator: only block comments on the line of the expression stay
|
||||
// there. A line comment is written behind the operator anyway.
|
||||
// `(a /* prettier-ignore */⏎) as T` is `a as /* prettier-ignore */ T`.
|
||||
let has_ignore_comment =
|
||||
(before_operator.iter()).any(|comment| f.comments().is_suppression_comment(comment));
|
||||
let glued_before_count = before_operator
|
||||
.iter()
|
||||
.take_while(|comment| !comment.preceded_by_newline() && !comment.is_multiline_block())
|
||||
.take_while(|comment| {
|
||||
!has_ignore_comment && !comment.has_newline_before() && !comment.is_multiline_block()
|
||||
})
|
||||
.count();
|
||||
let (glued_before, moved) = before_operator.split_at(glued_before_count);
|
||||
// Those on the line of the operator.
|
||||
let glued_after_count = match is_union || !moved.is_empty() {
|
||||
true => 0,
|
||||
false => after_operator
|
||||
.iter()
|
||||
.take_while(|comment| {
|
||||
!(comment.preceded_by_newline()
|
||||
|| (comment.is_multiline_block() && comment.followed_by_newline()))
|
||||
})
|
||||
.count(),
|
||||
let is_glued_after = |comment: &Comment<'_>| {
|
||||
!(comment.has_newline_before()
|
||||
|| (comment.is_multiline_block() && comment.has_newline_after()))
|
||||
};
|
||||
let can_glue_after = !is_union && moved.is_empty() && !is_ignored;
|
||||
let glued_after_count = match can_glue_after {
|
||||
true => after_operator.iter().take_while(is_glued_after).count(),
|
||||
false => 0,
|
||||
};
|
||||
let (glued_after, leading) = after_operator.split_at(glued_after_count);
|
||||
let is_on_lines_of_its_own = |comment: &Comment| {
|
||||
comment.followed_by_newline()
|
||||
&& (comment.preceded_by_newline() || comment.is_multiline_block())
|
||||
let glued_before_type_count = match can_glue_after && leading.is_empty() {
|
||||
true => before_type.iter().take_while(is_glued_after).count(),
|
||||
false => 0,
|
||||
};
|
||||
let is_type_on_next_line = glued_before
|
||||
.iter()
|
||||
let (glued_before_type, leading_type) = before_type.split_at(glued_before_type_count);
|
||||
let is_on_lines_of_its_own = |comment: Comment<'_>| {
|
||||
comment.has_newline_after()
|
||||
&& (comment.has_newline_before() || comment.is_multiline_block())
|
||||
};
|
||||
let is_type_on_next_line = (glued_before.iter())
|
||||
.chain(glued_after)
|
||||
.chain(glued_before_type)
|
||||
.any(|comment| comment.is_line())
|
||||
|| (!is_union && moved.iter().chain(leading).any(is_on_lines_of_its_own));
|
||||
|| (!is_union
|
||||
&& (moved.iter().chain(leading).chain(leading_type)).any(is_on_lines_of_its_own))
|
||||
// `a as // prettier-ignore⏎T`
|
||||
|| (is_ignored && leading_type.iter().any(|comment| comment.has_newline_after()));
|
||||
|
||||
write!(f, FormatNodeWithoutTrailingComments(&expression));
|
||||
if !has_suppression_comment {
|
||||
write!(f, FormatTrailingComments::Comments(glued_before));
|
||||
}
|
||||
write!(f, [space(), operation]);
|
||||
if !has_suppression_comment {
|
||||
write!(f, FormatTrailingComments::Comments(glued_after));
|
||||
}
|
||||
let format_type = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatLeadingComments::Comments(moved),
|
||||
FormatLeadingComments::Comments(leading),
|
||||
type_annotation
|
||||
.is_none()
|
||||
.then_some(FormatLeadingComments::Comments(leading_type))
|
||||
]
|
||||
);
|
||||
match type_annotation {
|
||||
Some(ty) => write!(f, ty),
|
||||
None => write!(f, "const"),
|
||||
}
|
||||
});
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&expression),
|
||||
FormatTrailingComments::Comments(glued_before),
|
||||
space(),
|
||||
operation,
|
||||
FormatTrailingComments::Comments(glued_after),
|
||||
FormatTrailingComments::Comments(glued_before_type)
|
||||
]
|
||||
);
|
||||
match is_type_on_next_line {
|
||||
true => write!(f, indent(&format_args!(hard_line_break(), format_type))),
|
||||
false => write!(f, [space(), format_type]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Not inlined: `a as b as c` is nested as deep as it is long, and what this needs of the stack would be taken at each level.
|
||||
#[inline(never)]
|
||||
fn ends_with_line_comment<'a>(expression: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
(comments_of_expression(expression, f).trailing.last()).is_some_and(|comment| comment.is_line())
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_as_or_satisfies_expression<'a>(e: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let (expression, type_annotation, operation) = match e.kind() {
|
||||
ExprKind::As { expr, ty } => (expr, Some(ty), "as"),
|
||||
@@ -88,91 +139,21 @@ pub(crate) fn write_as_or_satisfies_expression<'a>(e: Expr<'a>, f: &mut Formatte
|
||||
ExprKind::Satisfies { expr, ty } => (expr, Some(ty), "satisfies"),
|
||||
_ => return,
|
||||
};
|
||||
let type_start = match type_annotation {
|
||||
Some(ty) => ty.span().start,
|
||||
None => e
|
||||
.const_keyword_span()
|
||||
.map_or_else(|| e.span().end, |it| it.start),
|
||||
};
|
||||
|
||||
let format_inner = format_with(|f| {
|
||||
let format_type = format_with(|f| match type_annotation {
|
||||
if !f.is_quiet() && comments_stay_around_cast_operator(f) {
|
||||
return write_with_comments_in_place(e, expression, operation, type_annotation, f);
|
||||
}
|
||||
write!(f, [expression, space(), operation]);
|
||||
// Prettier's space is text. It shows before a line comment that trails the expression if the type starts on the
|
||||
// next line: `a as // comment`.
|
||||
match !f.is_quiet() && ends_with_line_comment(expression, f) {
|
||||
true => write!(f, " "),
|
||||
false => write!(f, space()),
|
||||
}
|
||||
match type_annotation {
|
||||
Some(ty) => write!(f, ty),
|
||||
None => write!(f, "const"),
|
||||
});
|
||||
let comments = match f.is_quiet() {
|
||||
true => &[][..],
|
||||
false => f
|
||||
.comments()
|
||||
.comments_in(Span::after(expression.span(), type_start)),
|
||||
};
|
||||
if !comments.is_empty() && comments_stay_around_cast_operator(f) {
|
||||
let is_union = type_annotation.is_some_and(|ty| {
|
||||
matches!(ty.kind(), TypeKind::Union(_))
|
||||
&& !f.comments().is_suppressed(ty.span().start)
|
||||
});
|
||||
// `const` is no node that would write the comments before it.
|
||||
let format_type = format_with(|f| {
|
||||
if type_annotation.is_none() {
|
||||
write!(
|
||||
f,
|
||||
FormatLeadingComments::Comments(f.comments().comments_before(type_start))
|
||||
);
|
||||
}
|
||||
write!(f, format_type);
|
||||
});
|
||||
return write_with_comments_in_place(
|
||||
expression,
|
||||
operation,
|
||||
comments,
|
||||
is_union,
|
||||
&format_type,
|
||||
f,
|
||||
);
|
||||
}
|
||||
// Those up to the first that spans several lines.
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|c| !c.is_multiline_block())
|
||||
.count();
|
||||
let block_comments = comments.get(..count).unwrap_or_default();
|
||||
|
||||
if !comments.is_empty() && type_annotation.is_none() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&expression),
|
||||
FormatTrailingComments::Comments(block_comments),
|
||||
space(),
|
||||
operation,
|
||||
space(),
|
||||
"const"
|
||||
]
|
||||
);
|
||||
} else if block_comments.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&expression),
|
||||
space(),
|
||||
operation,
|
||||
space(),
|
||||
format_type
|
||||
]
|
||||
);
|
||||
} else {
|
||||
write!(f, [expression, space(), operation]);
|
||||
// Prettier's space is text, which is seen before a line comment that trails the expression if
|
||||
// the type starts on the next line: `a as // comment`.
|
||||
let is_after_line_comment = f
|
||||
.comments()
|
||||
.printed_comments()
|
||||
.last()
|
||||
.is_some_and(|it| it.is_line());
|
||||
match is_after_line_comment {
|
||||
true => write!(f, [" ", format_type]),
|
||||
false => write!(f, [space(), format_type]),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! `a + b`, `a && b`. Prettier's `printBinaryishExpression`.
|
||||
|
||||
use super::expressions::{is_last_binary_operand_comment, unary_argument_has_comments};
|
||||
use crate::js::format::write_trailing_comments_of;
|
||||
use crate::js::utils::typecast::{is_cast_target, is_cast_target_that_hides_its_kind};
|
||||
use crate::js::utils::typecast::{
|
||||
comments_of_expression, has_comments, has_leading_own_line_comment, is_cast_target,
|
||||
is_cast_target_that_hides_its_kind, is_chain_expression_without_comments,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
use smallvec::SmallVec;
|
||||
@@ -176,21 +177,21 @@ impl<'a> BinaryLikeExpression<'a> {
|
||||
|
||||
// A condition has its own indentation and group.
|
||||
if !is_in_type_cast && self.is_inside_condition(parent) {
|
||||
return format_flattened_logical_expression(*self, true, f);
|
||||
return format_flattened_logical_expression(*self, true, false, f);
|
||||
}
|
||||
|
||||
// Where it is in parentheses: `(a + b)()`, `!(a + b)`, `(a + b).c`.
|
||||
let is_inside_parenthesis = match parent {
|
||||
AstNodes::StaticMemberExpression(_) | AstNodes::PrivateFieldExpression(_) => true,
|
||||
// It writes the parentheses and indents an argument with comments.
|
||||
AstNodes::UnaryExpression(unary) => !unary_argument_has_comments(unary, self.expr, f),
|
||||
AstNodes::UnaryExpression(_) => !has_comments(self.expr, f),
|
||||
_ => parent.is_call_like_callee(self.expr),
|
||||
};
|
||||
if is_inside_parenthesis && !is_in_type_cast {
|
||||
return write!(
|
||||
f,
|
||||
group(&soft_block_indent(&format_with(|f| {
|
||||
format_flattened_logical_expression(*self, false, f)
|
||||
format_flattened_logical_expression(*self, false, false, f)
|
||||
})))
|
||||
);
|
||||
}
|
||||
@@ -199,7 +200,7 @@ impl<'a> BinaryLikeExpression<'a> {
|
||||
return write!(
|
||||
f,
|
||||
group(&format_with(|f| format_flattened_logical_expression(
|
||||
*self, false, f
|
||||
*self, false, false, f
|
||||
)))
|
||||
);
|
||||
}
|
||||
@@ -207,7 +208,7 @@ impl<'a> BinaryLikeExpression<'a> {
|
||||
let inline_logical_expression = self.should_inline_logical_expression(f);
|
||||
let should_indent_if_inlines =
|
||||
!is_in_type_cast && should_indent_if_parent_inlines(parent, f);
|
||||
let parts = split_into_left_and_right_sides(*self, false, f);
|
||||
let parts = split_into_left_and_right_sides(*self, false, false, f);
|
||||
let flattened = parts.len() > 2 || self.right_with_same_operator(f).is_some();
|
||||
|
||||
if (inline_logical_expression && !flattened)
|
||||
@@ -241,15 +242,6 @@ impl<'a> BinaryLikeExpression<'a> {
|
||||
|
||||
let group_id = f.group_id("logicalChain");
|
||||
|
||||
// A line comment behind the operator before a JSX element at the end trails what is before
|
||||
// the operator, which is in the chain.
|
||||
let should_expand_chain = !f.is_quiet()
|
||||
&& jsx_element.as_ref().is_some_and(|jsx| {
|
||||
(f.comments()
|
||||
.comments_before_iter(jsx.last_operand(f).span().start))
|
||||
.any(|comment| comment.is_line() && !comment.preceded_by_newline())
|
||||
});
|
||||
|
||||
let format_non_jsx_parts = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
@@ -264,7 +256,6 @@ impl<'a> BinaryLikeExpression<'a> {
|
||||
))
|
||||
))
|
||||
.with_group_id(Some(group_id))
|
||||
.should_expand(should_expand_chain)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -287,13 +278,22 @@ impl<'a> BinaryLikeExpression<'a> {
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum BinaryLeftOrRightSide<'a> {
|
||||
/// The left side of `parent`, which the chain starts with.
|
||||
Left { parent: BinaryLikeExpression<'a> },
|
||||
Left {
|
||||
parent: BinaryLikeExpression<'a>,
|
||||
/// The whole chain.
|
||||
chain: BinaryLikeExpression<'a>,
|
||||
/// See [`BinaryLeftOrRightSide::Right::is_nested`], for `chain`.
|
||||
is_chain_nested: bool,
|
||||
},
|
||||
/// The operator and the right side of `parent`.
|
||||
Right {
|
||||
parent: BinaryLikeExpression<'a>,
|
||||
/// The chain is the condition of an `if`, a `while`, a `do`-`while` or a `switch`.
|
||||
inside_condition: bool,
|
||||
operands: Operands,
|
||||
/// Prettier's `isNested`: `parent` is written as a part of a longer chain and not as a node of its own, so the chain
|
||||
/// writes its comments.
|
||||
is_nested: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -306,34 +306,69 @@ enum Operands {
|
||||
Last,
|
||||
}
|
||||
|
||||
/// Writes all operands of a chain one after the other.
|
||||
/// Writes all operands of a chain one after the other. `is_nested`: see [`BinaryLeftOrRightSide::Right::is_nested`].
|
||||
fn format_flattened_logical_expression<'a>(
|
||||
binary: BinaryLikeExpression<'a>,
|
||||
inside_condition: bool,
|
||||
is_nested: bool,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
// `a && ((b && ((c && ..) && d)) && e)`: it is not written by way of `Expr`.
|
||||
if !f.context_mut().has_stack_left() {
|
||||
return;
|
||||
}
|
||||
let parts = split_into_left_and_right_sides(binary, inside_condition, f);
|
||||
let parts = split_into_left_and_right_sides(binary, inside_condition, is_nested, f);
|
||||
f.join().entries(parts.iter());
|
||||
}
|
||||
|
||||
/// `(/* comment */ a + b) + c`: writes the leading comments of the expressions of `chain` that are not written as nodes of
|
||||
/// their own.
|
||||
fn write_leading_comments_of_nested<'a>(
|
||||
chain: BinaryLikeExpression<'a>,
|
||||
is_chain_nested: bool,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if is_chain_nested {
|
||||
write!(f, leading_comments(chain.expr.as_ast_nodes()));
|
||||
}
|
||||
let mut current = chain;
|
||||
while let Some(left) = current.can_flatten(f) {
|
||||
write!(f, leading_comments(left.expr.as_ast_nodes()));
|
||||
current = left;
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `hasComment(node.left, CommentCheckFlags.Trailing | CommentCheckFlags.Line)`.
|
||||
fn has_trailing_line_comment<'a>(left: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
!f.is_quiet()
|
||||
&& !is_chain_expression_without_comments(left, f)
|
||||
&& (comments_of_expression(left, f).trailing.iter()).any(|comment| comment.is_line())
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for BinaryLeftOrRightSide<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let (mut binary_like_expression, inside_parenthesis, operands) = match *self {
|
||||
Self::Left { parent }
|
||||
let (mut binary_like_expression, inside_parenthesis, operands, is_nested) = match *self {
|
||||
Self::Left { parent, .. }
|
||||
if f.is_quiet() && is_one_text(parent.left) && f.is_at_start_of_group() =>
|
||||
{
|
||||
return write!(f, parent.left);
|
||||
}
|
||||
Self::Left { parent } => return write!(f, group(&parent.left)),
|
||||
Self::Left {
|
||||
parent,
|
||||
chain,
|
||||
is_chain_nested,
|
||||
} => {
|
||||
if !f.is_quiet() {
|
||||
write_leading_comments_of_nested(chain, is_chain_nested, f);
|
||||
}
|
||||
return write!(f, group(&parent.left));
|
||||
}
|
||||
Self::Right {
|
||||
parent,
|
||||
inside_condition,
|
||||
operands,
|
||||
} => (parent, inside_condition, operands),
|
||||
is_nested,
|
||||
} => (parent, inside_condition, operands, is_nested),
|
||||
};
|
||||
let logical_operator = binary_like_expression
|
||||
.is_logical()
|
||||
@@ -346,11 +381,12 @@ impl<'a> Format<'a> for BinaryLeftOrRightSide<'a> {
|
||||
&& let Some(right_logical) = binary_like_expression.right_with_same_operator(f)
|
||||
{
|
||||
if operands != Operands::Last {
|
||||
if !comment_behind_chain_leads_chain_in_parentheses(f) {
|
||||
write_trailing_comments_of_nested(binary_like_expression.left, f);
|
||||
}
|
||||
let right = right_logical.left;
|
||||
write_operator(operator, right, is_inlined_operand(right), f);
|
||||
write_operator(operator, right_logical.expr, is_inlined_operand(right), f);
|
||||
// In the rebalanced tree they lead `b`.
|
||||
if !f.is_quiet() {
|
||||
write!(f, leading_comments(right_logical.expr.as_ast_nodes()));
|
||||
}
|
||||
match BinaryLikeExpression::new(right_logical.left)
|
||||
.filter(|left| left.operator == operator && !is_cast_target(left.expr, f))
|
||||
{
|
||||
@@ -358,6 +394,7 @@ impl<'a> Format<'a> for BinaryLeftOrRightSide<'a> {
|
||||
format_flattened_logical_expression(
|
||||
left_logical_child,
|
||||
inside_parenthesis,
|
||||
true,
|
||||
f,
|
||||
);
|
||||
}
|
||||
@@ -371,7 +408,6 @@ impl<'a> Format<'a> for BinaryLeftOrRightSide<'a> {
|
||||
}
|
||||
|
||||
let (left, right) = (binary_like_expression.left, binary_like_expression.right);
|
||||
let is_jsx = right.tag() == ExprTag::Jsx;
|
||||
|
||||
let operator_and_right_expression = format_with(|f| {
|
||||
if is_angular_pipe(binary_like_expression.operator, f) {
|
||||
@@ -380,34 +416,18 @@ impl<'a> Format<'a> for BinaryLeftOrRightSide<'a> {
|
||||
let is_inlined = binary_like_expression.should_inline_logical_expression(f)
|
||||
|| would_end_type_arguments(binary_like_expression, f);
|
||||
write_operator(binary_like_expression.operator, right, is_inlined, f);
|
||||
if is_inlined
|
||||
&& !is_jsx
|
||||
&& f.comments()
|
||||
.has_leading_own_line_comment(right.span().start)
|
||||
{
|
||||
return write!(f, soft_line_indent_or_space(&right));
|
||||
match is_inlined && operand_has_leading_own_line_comment(right, f) {
|
||||
true => write!(f, soft_line_indent_or_space(&right)),
|
||||
false => write!(f, right),
|
||||
}
|
||||
write!(f, right);
|
||||
// `a && (b && c /* comment */)`: in the tree that Prettier has rebalanced, the comment is in
|
||||
// the expression that `c` is the right side of.
|
||||
if !f.is_quiet() && binary_like_expression.expr != outermost.expr {
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_in(Span::after(right.span(), outermost.expr.span().end));
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
// See `is_last_binary_operand_comment`.
|
||||
if !f.is_quiet()
|
||||
&& let AstNodes::UnaryExpression(unary) = outermost.parent()
|
||||
&& let [comment, ..] = f.comments().unprinted_comments()
|
||||
&& comment.start() >= right.span().end
|
||||
&& comment.end() <= unary.span().end
|
||||
&& is_last_binary_operand_comment(outermost.expr, comment, f)
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(std::slice::from_ref(comment))
|
||||
);
|
||||
// `a && (b && c /* comment */)`: in the rebalanced tree the comment trails `c`.
|
||||
let mut in_parentheses = binary_like_expression.expr;
|
||||
while !f.is_quiet() && in_parentheses != outermost.expr {
|
||||
write!(f, trailing_comments(in_parentheses.as_ast_nodes()));
|
||||
match in_parentheses.parent() {
|
||||
Node::Expr(parent) => in_parentheses = parent,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -420,42 +440,40 @@ impl<'a> Format<'a> for BinaryLeftOrRightSide<'a> {
|
||||
&& is_angular_pipe(operator, f) == is_pipe
|
||||
}
|
||||
};
|
||||
let should_group = !(matches!(binary_like_expression.expr.parent(), Node::Expr(parent) if is_same_kind(parent))
|
||||
// For Prettier what is in the parentheses of a type cast is in a `ParenthesizedExpression`.
|
||||
let is_operand_of_same_kind = |operand: Expr<'a>| {
|
||||
is_same_kind(operand) && !is_cast_target_that_hides_its_kind(operand, f)
|
||||
};
|
||||
let is_in_same_kind = matches!(binary_like_expression.expr.parent(), Node::Expr(parent) if is_same_kind(parent))
|
||||
&& !is_cast_target_that_hides_its_kind(binary_like_expression.expr, f);
|
||||
let should_group = !(is_in_same_kind
|
||||
|| (is_pipe && is_argument_of_angular_pipe(binary_like_expression.expr))
|
||||
|| is_same_kind(left)
|
||||
|| is_same_kind(right)
|
||||
|| is_operand_of_same_kind(left)
|
||||
|| is_operand_of_same_kind(right)
|
||||
|| (inside_parenthesis && logical_operator.is_some()));
|
||||
|
||||
write_trailing_comments_of_nested(left, f);
|
||||
|
||||
if !should_group {
|
||||
return write!(f, operator_and_right_expression);
|
||||
}
|
||||
// A line comment between the left side and the right side, which has been printed by now,
|
||||
// breaks the line:
|
||||
// A line comment after the left side breaks the line after the operator:
|
||||
//
|
||||
// a =
|
||||
// b || // comment
|
||||
// c;
|
||||
let should_break = !f.is_quiet()
|
||||
&& (f.comments().printed_comments().iter().rev())
|
||||
.take_while(|comment| {
|
||||
left.span().end <= comment.start() && right.span().start >= comment.end()
|
||||
})
|
||||
.any(|comment| comment.is_line());
|
||||
write!(
|
||||
f,
|
||||
group(&operator_and_right_expression).should_expand(should_break)
|
||||
);
|
||||
//
|
||||
// Without a group of its own, the line break is in the group that the comment is in.
|
||||
match should_group {
|
||||
true => write!(
|
||||
f,
|
||||
group(&operator_and_right_expression)
|
||||
.should_expand(has_trailing_line_comment(left, f))
|
||||
),
|
||||
false => write!(f, operator_and_right_expression),
|
||||
}
|
||||
if is_nested && !f.is_quiet() {
|
||||
write!(f, trailing_comments(outermost.expr.as_ast_nodes()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `operator`, which is being written, is the `|` before a filter of Vue: a line is broken before it.
|
||||
/// `a || b || // comment⏎(c || d)`: for oxfmt the comment gets a line of its own before `c`.
|
||||
fn comment_behind_chain_leads_chain_in_parentheses(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
fn is_before_vue_filter(operator: BinOp, f: &Formatter<'_>) -> bool {
|
||||
operator == BinOp::BitOr && f.context().is_vue_filter_sequence.get()
|
||||
}
|
||||
@@ -518,48 +536,83 @@ fn would_end_type_arguments<'a>(binary: BinaryLikeExpression<'a>, f: &Formatter<
|
||||
&& f.options().experimental_operator_position.is_end()
|
||||
}
|
||||
|
||||
/// Writes `operator` and what is around it. `right`: the operand after it. `is_inlined`: it stays on the line of
|
||||
/// the operator.
|
||||
/// The leading comments of the operand that `right` starts with. `right` is what follows `operator` in the tree.
|
||||
///
|
||||
/// `a && (b && c)`: in the tree that Prettier has rebalanced, `b` follows the first `&&`, and the comments that lead `b && c`
|
||||
/// here lead `b`.
|
||||
fn leading_comments_of_operand<'a>(
|
||||
operator: BinOp,
|
||||
right: Expr<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> SmallVec<[CommentList<'a>; 2]> {
|
||||
let mut lists = SmallVec::new();
|
||||
let mut operand = right;
|
||||
loop {
|
||||
lists.push(comments_of_expression(operand, f).leading);
|
||||
let starts_with = BinaryLikeExpression::new(operand).filter(|it| {
|
||||
it.is_logical()
|
||||
&& it.operator == operator
|
||||
&& !is_tree_as_it_is_parsed(f)
|
||||
&& !is_cast_target(it.expr, f)
|
||||
});
|
||||
match starts_with {
|
||||
Some(logical) => operand = logical.left,
|
||||
None => return lists,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `hasLeadingOwnLineComment(options.originalText, rightNodeToCheckComments)`.
|
||||
fn operand_has_leading_own_line_comment<'a>(right: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
if right.tag() == ExprTag::Jsx {
|
||||
return has_leading_own_line_comment(right, f);
|
||||
}
|
||||
(comments_of_expression(right, f).leading.iter()).any(|comment| comment.has_newline_after())
|
||||
}
|
||||
|
||||
/// Writes `operator` and what is around it. `right`: what follows it in the tree. `is_inlined`: the operand after it stays
|
||||
/// on the line of the operator.
|
||||
fn write_operator<'a>(operator: BinOp, right: Expr<'a>, is_inlined: bool, f: &mut Formatter<'a>) {
|
||||
if is_inlined {
|
||||
return write!(f, [operator_after_space(operator), space()]);
|
||||
}
|
||||
if is_before_vue_filter(operator, f)
|
||||
&& !f
|
||||
.comments()
|
||||
.has_leading_own_line_comment(right.span().start)
|
||||
{
|
||||
let is_operator_at_end = f.options().experimental_operator_position.is_end();
|
||||
let is_before_vue_filter = is_before_vue_filter(operator, f);
|
||||
// At the end of the line, the operator is written the same with and without comments.
|
||||
let leading_comments = match f.is_quiet() || (is_operator_at_end && !is_before_vue_filter) {
|
||||
true => SmallVec::new(),
|
||||
false => leading_comments_of_operand(operator, right, f),
|
||||
};
|
||||
let has_leading_own_line_comment = match right.tag() == ExprTag::Jsx {
|
||||
true => operand_has_leading_own_line_comment(right, f),
|
||||
false => {
|
||||
(leading_comments.iter().copied().flatten()).any(|comment| comment.has_newline_after())
|
||||
}
|
||||
};
|
||||
if is_before_vue_filter && !has_leading_own_line_comment {
|
||||
return write!(f, [soft_line_break_or_space(), operator.as_str(), space()]);
|
||||
}
|
||||
if f.options().experimental_operator_position.is_end() {
|
||||
if is_operator_at_end {
|
||||
return write!(
|
||||
f,
|
||||
[operator_after_space(operator), soft_line_break_or_space()]
|
||||
);
|
||||
}
|
||||
// A comment that ends its line stays before the operator.
|
||||
let start = right.span().start;
|
||||
let has_comment_before_operator = !f.is_quiet()
|
||||
&& match right.kind() {
|
||||
ExprKind::Jsx(_) => f.comments().is_suppressed(start),
|
||||
_ => f.comments().has_leading_own_line_comment(start),
|
||||
}
|
||||
&& !f
|
||||
.comments()
|
||||
.comments_before_iter(start)
|
||||
.any(|comment| f.comments().is_type_cast_comment(comment));
|
||||
let has_comment_before_operator = has_leading_own_line_comment
|
||||
&& !(leading_comments.iter().copied().flatten())
|
||||
.any(|comment| comment.looks_like_type_cast());
|
||||
if has_comment_before_operator {
|
||||
// Prettier writes a space here. It is seen before a line comment that trails the left side, and it is one more
|
||||
// column that has to fit.
|
||||
let space = writes_space_before_comments_before_operator(f).then_some(" ");
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
space,
|
||||
soft_line_break_or_space(),
|
||||
format_leading_comments(right.span())
|
||||
]
|
||||
);
|
||||
write!(f, [space, soft_line_break_or_space()]);
|
||||
for comments in leading_comments {
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
} else {
|
||||
write!(f, soft_line_break_or_space());
|
||||
}
|
||||
@@ -629,27 +682,11 @@ fn is_one_text(e: Expr<'_>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// The comments after `left`, if it is written as a part of the chain and not as a node of its own.
|
||||
fn write_trailing_comments_of_nested<'a>(left: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let node = left.as_ast_nodes();
|
||||
if matches!(
|
||||
node,
|
||||
AstNodes::LogicalExpression(_)
|
||||
| AstNodes::BinaryExpression(_)
|
||||
| AstNodes::PrivateInExpression(_)
|
||||
) {
|
||||
write_trailing_comments_of(node, f);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BinaryLeftOrRightSide<'a> {
|
||||
/// The operand that is written last.
|
||||
fn last_operand(&self, f: &Formatter<'a>) -> Expr<'a> {
|
||||
match *self {
|
||||
BinaryLeftOrRightSide::Left { parent } => parent.left,
|
||||
BinaryLeftOrRightSide::Left { parent, .. } => parent.left,
|
||||
BinaryLeftOrRightSide::Right { parent, .. } => {
|
||||
let mut last = parent;
|
||||
while let Some(right) = last.right_with_same_operator(f) {
|
||||
@@ -670,11 +707,13 @@ impl<'a> BinaryLeftOrRightSide<'a> {
|
||||
BinaryLeftOrRightSide::Right {
|
||||
parent,
|
||||
inside_condition,
|
||||
is_nested,
|
||||
..
|
||||
} => Some(BinaryLeftOrRightSide::Right {
|
||||
parent,
|
||||
inside_condition,
|
||||
operands,
|
||||
is_nested,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -697,10 +736,11 @@ impl<'a> BinaryLeftOrRightSide<'a> {
|
||||
}
|
||||
|
||||
/// The operands of a chain, in order: the leftmost that is not part of the chain, and then the
|
||||
/// right side of each expression on the way up.
|
||||
/// right side of each expression on the way up. `is_nested`: see [`BinaryLeftOrRightSide::Right::is_nested`], for `binary`.
|
||||
fn split_into_left_and_right_sides<'a>(
|
||||
binary: BinaryLikeExpression<'a>,
|
||||
inside_condition: bool,
|
||||
is_nested: bool,
|
||||
f: &Formatter<'a>,
|
||||
) -> SmallVec<[BinaryLeftOrRightSide<'a>; 4]> {
|
||||
let mut items = SmallVec::new();
|
||||
@@ -710,13 +750,18 @@ fn split_into_left_and_right_sides<'a>(
|
||||
parent: current,
|
||||
inside_condition,
|
||||
operands: Operands::All,
|
||||
is_nested: is_nested || current.expr != binary.expr,
|
||||
});
|
||||
match current.can_flatten(f) {
|
||||
Some(left) => current = left,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
items.push(BinaryLeftOrRightSide::Left { parent: current });
|
||||
items.push(BinaryLeftOrRightSide::Left {
|
||||
parent: current,
|
||||
chain: binary,
|
||||
is_chain_nested: is_nested,
|
||||
});
|
||||
items.reverse();
|
||||
items
|
||||
}
|
||||
|
||||
@@ -1,52 +1,30 @@
|
||||
use super::program::FormatStatements;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
use crate::write;
|
||||
|
||||
/// `{ .. }`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_block_statement<'a>(
|
||||
statement: Stmt<'a>,
|
||||
body: List<'a, Stmt<'a>>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
write!(f, "{");
|
||||
|
||||
// See `write_catch_clause`.
|
||||
let can_be_handler = f.context().has_cached_elements()
|
||||
&& matches!(statement.parent(), Node::Stmt(parent) if parent.tag() == StmtTag::Try);
|
||||
let comments_before_catch_clause = match can_be_handler.then(|| statement.ast_parent()) {
|
||||
Some(parent @ AstNodes::CatchClause(_)) => f.context().get_cached_element(&parent.span()),
|
||||
_ => None,
|
||||
};
|
||||
let formatted_comments_before_catch_clause = format_with(|f| {
|
||||
if let Some(comments) = comments_before_catch_clause {
|
||||
f.write_element(comments);
|
||||
}
|
||||
});
|
||||
|
||||
if is_empty_block(body) {
|
||||
// `try // comment\n{}`: the comments before the block that are not written yet are written
|
||||
// in it.
|
||||
if comments_before_catch_clause.is_some()
|
||||
|| f.comments().has_comment_before(statement.span().end)
|
||||
{
|
||||
let dangling = f
|
||||
.comments()
|
||||
.of_node(AstNodes::BlockStatement(statement))
|
||||
.dangling;
|
||||
if !dangling.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
block_indent(&format_args!(
|
||||
formatted_comments_before_catch_clause,
|
||||
format_dangling_comments(statement.span())
|
||||
))
|
||||
dangling_comments(AstNodes::BlockStatement(statement)).with_block_indent()
|
||||
);
|
||||
} else if is_non_collapsible(statement.ast_parent(), f) {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
block_indent(&format_args!(
|
||||
formatted_comments_before_catch_clause,
|
||||
FormatStatements(body)
|
||||
))
|
||||
);
|
||||
write!(f, block_indent(&FormatStatements(body)));
|
||||
}
|
||||
write!(f, "}");
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ use crate::js::print::program::is_next_line_empty;
|
||||
use crate::js::utils::call_expression::{is_call_expression, strip_chain_element_wrappers};
|
||||
use crate::js::utils::is_long_curried_call;
|
||||
use crate::js::utils::member_chain::simple_argument::SimpleArgument;
|
||||
use crate::js::utils::typecast::is_cast_target;
|
||||
use crate::js::utils::typecast::{
|
||||
comments_of_expression, comments_of_outermost_node, has_comments, has_comments_that_count,
|
||||
is_cast_target,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
use smallvec::SmallVec;
|
||||
@@ -70,13 +73,13 @@ impl<'a> Format<'a> for FormatArguments<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let last_index = match self.len() {
|
||||
0 if f.is_quiet() => return write!(f, "()"),
|
||||
// `call/* comment1 */(/* comment2 */)`
|
||||
// `call(/* comment */)`
|
||||
0 => {
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
"(",
|
||||
format_dangling_comments(self.parent.span()).with_soft_block_indent(),
|
||||
dangling_comments(self.parent).with_soft_block_indent(),
|
||||
")"
|
||||
]
|
||||
);
|
||||
@@ -97,9 +100,7 @@ impl<'a> Format<'a> for FormatArguments<'a> {
|
||||
}
|
||||
let has_function = has & kind(ExprTag::Fn) != 0;
|
||||
|
||||
if has_function
|
||||
&& has & kind(ExprTag::Array) != 0
|
||||
&& is_react_hook_with_deps_array(self, f.comments(), f.options().flavor)
|
||||
if has_function && has & kind(ExprTag::Array) != 0 && is_react_hook_with_deps_array(self, f)
|
||||
{
|
||||
return write!(
|
||||
f,
|
||||
@@ -216,7 +217,13 @@ fn is_empty_line_between<'a>(end: u32, next: Expr<'a>, f: &Formatter<'a>) -> boo
|
||||
/// Whether there is an empty line after the argument at `index`, which has been written.
|
||||
fn is_empty_line_kept_after<'a>(args: List<'a, Expr<'a>>, index: usize, f: &Formatter<'a>) -> bool {
|
||||
match (counts_line_breaks_between_arguments(f), args.get(index + 1)) {
|
||||
(true, Some(next)) => f.lines_before(next.span()) > 1,
|
||||
(true, Some(next)) => {
|
||||
let first_comment = (!f.is_quiet())
|
||||
.then(|| comments_of_expression(next, f).leading.first())
|
||||
.flatten();
|
||||
let first_comment = first_comment.map(|comment| comment.span());
|
||||
(f.source_text()).get_lines_before(next.span(), first_comment, false) > 1
|
||||
}
|
||||
_ => is_followed_by_empty_line(args, index, f),
|
||||
}
|
||||
}
|
||||
@@ -348,7 +355,7 @@ fn arguments_grouped_layout<'a>(
|
||||
has_embed_label_with_hug(only, f)
|
||||
&& (f.is_quiet()
|
||||
|| f.options().flavor.is_oxfmt()
|
||||
|| !f.comments().is_suppressed(only.span().start))
|
||||
|| !f.comments().is_ignored(only.as_ast_nodes()))
|
||||
})
|
||||
{
|
||||
return Some(GroupedCallArgumentLayout::GroupedLastArgument);
|
||||
@@ -391,26 +398,37 @@ fn should_group_first_argument<'a>(first: Expr<'a>, second: Expr<'a>, f: &Format
|
||||
return false;
|
||||
}
|
||||
|
||||
// Not if there are comments around the first argument: before it, between it and the comma, or
|
||||
// behind the comma at the end of the line.
|
||||
if !f.is_quiet() {
|
||||
if f.comments().has_comment_before(first.span().start)
|
||||
|| f.comments()
|
||||
.comments_in(first.span().between(second.span()))
|
||||
.iter()
|
||||
.any(|comment| {
|
||||
comment.followed_by_newline()
|
||||
|| !f
|
||||
.source_text()
|
||||
.contains_byte(first.span().between(comment.span), b',')
|
||||
})
|
||||
{
|
||||
return false;
|
||||
if !f.is_quiet()
|
||||
&& match only_comments_around_first_argument_count(f) {
|
||||
true => has_comment_around_first_argument_for_oxfmt(first, second, f),
|
||||
false => has_comments(first, f),
|
||||
}
|
||||
{
|
||||
return false;
|
||||
}
|
||||
is_hopefully_short_call_argument(second, f)
|
||||
}
|
||||
|
||||
/// `a(function (/* comment */) { c; }, b)` is `a(function (/* comment */) {⏎ c;⏎}, b)` for oxfmt. For Prettier the function
|
||||
/// has a comment, so each argument is on its own line.
|
||||
fn only_comments_around_first_argument_count(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// oxfmt goes by where the comments are: before `first`, between it and the comma, or between it and `second` at the end of a
|
||||
/// line. A comment in `first` does not count: `a(function (/* comment */) {}, b)`.
|
||||
fn has_comment_around_first_argument_for_oxfmt<'a>(
|
||||
first: Expr<'a>,
|
||||
second: Expr<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
let between = Span::new(first.span().end, second.span().start);
|
||||
!comments_of_expression(first, f).leading.is_empty()
|
||||
|| (f.comments().comments_in_span(between).iter()).any(|comment| {
|
||||
comment.has_newline_after() || comment.prev_token_end() <= first.outer_span().end
|
||||
})
|
||||
}
|
||||
|
||||
fn should_group_last_argument_impl<'a>(
|
||||
args: List<'a, Expr<'a>>,
|
||||
penultimate: Option<Expr<'a>>,
|
||||
@@ -418,9 +436,6 @@ fn should_group_last_argument_impl<'a>(
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
let args_len = args.len();
|
||||
let previous_end = args
|
||||
.get(args_len.wrapping_sub(2))
|
||||
.map(|previous| previous.span().end);
|
||||
|
||||
// Not if the one before is of the same kind.
|
||||
if let Some(penultimate) = penultimate
|
||||
@@ -445,33 +460,9 @@ fn should_group_last_argument_impl<'a>(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Not if there are comments around the last argument.
|
||||
if !f.is_quiet() {
|
||||
let last_span = last.span();
|
||||
// A comment at the end of the line of the argument before, or before the comma, trails that.
|
||||
let has_comment_before_last = match previous_end {
|
||||
Some(previous_end) => f
|
||||
.comments()
|
||||
.comments_in(Span::before(previous_end, last_span))
|
||||
.iter()
|
||||
.any(|comment| {
|
||||
comment.preceded_by_newline()
|
||||
|| (!comment.followed_by_newline()
|
||||
&& !f
|
||||
.source_text()
|
||||
.contains_byte(comment.span.between(last_span), b','))
|
||||
}),
|
||||
None => f.comments().has_comment_before(last_span.start),
|
||||
};
|
||||
if has_comment_before_last
|
||||
|| f.comments()
|
||||
.comments_after(last_span.end)
|
||||
.first()
|
||||
.is_some_and(|c| {
|
||||
!f.source_text()
|
||||
.contains_byte(last_span.between(c.span), b')')
|
||||
})
|
||||
{
|
||||
let owned = comments_of_outermost_node(last, f);
|
||||
if !owned.leading.is_empty() || !owned.trailing.is_empty() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -482,7 +473,7 @@ fn should_group_last_argument_impl<'a>(
|
||||
if args_len == 2 && penultimate.is_some_and(|it| it.arrow_function().is_some()) {
|
||||
return false;
|
||||
}
|
||||
!can_concisely_print_array_list(last.span(), elements, f)
|
||||
!can_concisely_print_array_list(elements, f)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
@@ -536,6 +527,15 @@ fn is_hopefully_short_call_argument<'a>(argument: Expr<'a>, f: &Formatter<'a>) -
|
||||
}
|
||||
}
|
||||
|
||||
/// `hasComment(arg)` in Prettier's `couldExpandArg`, for `{}` and `[]`. oxfmt only looks at the comments inside.
|
||||
fn empty_literal_has_comment<'a>(argument: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
!f.is_quiet()
|
||||
&& match f.options().flavor.is_oxfmt() {
|
||||
true => !comments_of_outermost_node(argument, f).dangling.is_empty(),
|
||||
false => has_comments(argument, f),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `couldExpandArg`.
|
||||
fn can_group_expression_argument<'a>(mut argument: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
loop {
|
||||
@@ -544,10 +544,10 @@ fn can_group_expression_argument<'a>(mut argument: Expr<'a>, f: &Formatter<'a>)
|
||||
}
|
||||
argument = match argument.kind() {
|
||||
ExprKind::Object(props) => {
|
||||
return !props.is_empty() || f.comments().has_comment_in_span(argument.span());
|
||||
return !props.is_empty() || empty_literal_has_comment(argument, f);
|
||||
}
|
||||
ExprKind::Array(elements) => {
|
||||
return !elements.is_empty() || f.comments().has_comment_in_span(argument.span());
|
||||
return !elements.is_empty() || empty_literal_has_comment(argument, f);
|
||||
}
|
||||
ExprKind::As { .. } if argument.is_flow_type_cast() => return false,
|
||||
ExprKind::As { expr, .. }
|
||||
@@ -651,7 +651,10 @@ fn write_grouped_arguments<'a>(
|
||||
};
|
||||
has_cached = has_cached || options.is_some();
|
||||
let interned = match options {
|
||||
Some(options) => f.intern(&format_args!(FormatBareFunction(argument, options), comma)),
|
||||
Some(options) => f.intern(&format_args!(
|
||||
FormatExpr::with_options(argument, options),
|
||||
comma
|
||||
)),
|
||||
None => f.intern(&format_args!(argument, comma)),
|
||||
};
|
||||
|
||||
@@ -855,16 +858,6 @@ fn has_signature_without_soft_lines(function: Func<'_>) -> bool {
|
||||
function.is_arrow() || !function.params().is_empty() || function.this_param().is_some()
|
||||
}
|
||||
|
||||
/// A function or an arrow function with options, without the comments and the parentheses around
|
||||
/// it, which an argument has none of.
|
||||
struct FormatBareFunction<'a>(Expr<'a>, ExprOptions);
|
||||
|
||||
impl<'a> Format<'a> for FormatBareFunction<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
crate::js::format::write_expression(self.0, self.1, f);
|
||||
}
|
||||
}
|
||||
|
||||
struct FormatGroupedFirstArgument<'a> {
|
||||
argument: Expr<'a>,
|
||||
}
|
||||
@@ -872,7 +865,7 @@ struct FormatGroupedFirstArgument<'a> {
|
||||
impl<'a> Format<'a> for FormatGroupedFirstArgument<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
match self.argument.arrow_function() {
|
||||
Some(_) => FormatBareFunction(
|
||||
Some(_) => FormatExpr::with_options(
|
||||
self.argument,
|
||||
ExprOptions::Arrow(FormatJsArrowFunctionExpressionOptions {
|
||||
cache_mode: FunctionCacheMode::Cache,
|
||||
@@ -897,7 +890,7 @@ impl<'a> Format<'a> for FormatGroupedLastArgument<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let layout = Some(GroupedCallArgumentLayout::GroupedLastArgument);
|
||||
match self.argument.as_fn() {
|
||||
Some(function) if function.is_arrow() => FormatBareFunction(
|
||||
Some(function) if function.is_arrow() => FormatExpr::with_options(
|
||||
self.argument,
|
||||
ExprOptions::Arrow(FormatJsArrowFunctionExpressionOptions {
|
||||
cache_mode: FunctionCacheMode::Cache,
|
||||
@@ -906,7 +899,7 @@ impl<'a> Format<'a> for FormatGroupedLastArgument<'a> {
|
||||
}),
|
||||
)
|
||||
.fmt(f),
|
||||
Some(_) if self.expands_parameters => FormatBareFunction(
|
||||
Some(_) if self.expands_parameters => FormatExpr::with_options(
|
||||
self.argument,
|
||||
ExprOptions::Function(FormatFunctionOptions {
|
||||
cache_mode: FunctionCacheMode::Cache,
|
||||
@@ -919,11 +912,7 @@ impl<'a> Format<'a> for FormatGroupedLastArgument<'a> {
|
||||
}
|
||||
|
||||
/// `useMemo(() => {}, [a, b])`, `useImperativeHandle(ref, () => {}, [a, b])`
|
||||
fn is_react_hook_with_deps_array<'a>(
|
||||
arguments: &FormatArguments<'a>,
|
||||
comments: &Comments<'a>,
|
||||
flavor: Flavor,
|
||||
) -> bool {
|
||||
fn is_react_hook_with_deps_array<'a>(arguments: &FormatArguments<'a>, f: &Formatter<'a>) -> bool {
|
||||
if arguments.len() > 3 || arguments.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
@@ -947,17 +936,8 @@ fn is_react_hook_with_deps_array<'a>(
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Not if there is a comment that is not in the callback or the array. For Prettier one in an empty
|
||||
// array is a comment of the array.
|
||||
let is_empty = !flavor.is_oxfmt()
|
||||
&& matches!(deps.kind(), ExprKind::Array(elements) if elements.is_empty());
|
||||
!comments
|
||||
.comments_before(arguments.parent.span().end)
|
||||
.iter()
|
||||
.any(|comment| {
|
||||
!callback.span().contains(comment.span)
|
||||
&& (is_empty || !deps.span().contains(comment.span))
|
||||
})
|
||||
// `useEffect(() => {}, [/* comment */])`: for Prettier the array has a comment, so this is a call like any other.
|
||||
f.is_quiet() || !(arguments.iter()).any(|argument| has_comments_that_count(argument, f))
|
||||
}
|
||||
|
||||
/// Prettier's `isDecoratedFunction`:
|
||||
|
||||
@@ -7,13 +7,11 @@ use super::arrow_function_expression::is_multiline_template_starting_on_same_lin
|
||||
use super::type_parameters::type_arguments;
|
||||
use crate::js::parentheses::expression::chain_expression_needs_parentheses;
|
||||
use crate::js::utils::call_expression::{
|
||||
is_call_expression, is_test_call_expression_in_flavor, type_arguments_trailing_comments,
|
||||
write_callee_trailing_comments,
|
||||
FormatCallee, is_call_expression, is_test_call_expression_in_flavor,
|
||||
};
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::member_chain::write_member_chain;
|
||||
use crate::js::utils::tailwindcss::{InTailwindContext, TailwindContext, context_of_function};
|
||||
use crate::js::utils::typecast::is_cast_target;
|
||||
use crate::js::utils::typecast::{comments_of_outermost_node, has_comments, is_cast_target};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -39,20 +37,17 @@ fn write_call<'a>(
|
||||
) {
|
||||
let callee = call.callee();
|
||||
let head = format_args!(
|
||||
FormatCallee(call),
|
||||
FormatCallee {
|
||||
callee,
|
||||
is_in_member_chain: false
|
||||
},
|
||||
boundary_behind_callee(f),
|
||||
call.is_optional().then_some("?."),
|
||||
FormatTypeArguments(e, call),
|
||||
type_arguments(call.type_args(), Node::Expr(e)),
|
||||
boundary_behind_callee(f).filter(|_| !call.type_args().is_empty())
|
||||
);
|
||||
|
||||
if keeps_arguments_on_one_line(e, call, f) {
|
||||
// This is no member chain, so what leads `a.b` is written before the `a`, if it is the next comment.
|
||||
if !f.is_quiet()
|
||||
&& let Some(object) = callee.object()
|
||||
{
|
||||
let comments = f.comments().comments_leading_link(object.span().end);
|
||||
FormatLeadingComments::Comments(comments).fmt(f);
|
||||
}
|
||||
let arguments = InTailwindContext(context, FormatArgumentsOnOneLine(call.args()));
|
||||
return write!(f, [head, arguments]);
|
||||
}
|
||||
@@ -102,7 +97,7 @@ fn keeps_arguments_on_one_line<'a>(e: Expr<'a>, call: Call<'a>, f: &Formatter<'a
|
||||
return false;
|
||||
}
|
||||
is_template_on_its_own_line_only_argument(call.args(), f)
|
||||
|| is_simple_module_import(e, call, f)
|
||||
|| is_simple_module_import(call, f)
|
||||
|| is_commonjs_or_amd_module_definition(e, call, f)
|
||||
|| (is_test_call_expression_in_flavor(e, f)
|
||||
&& !call
|
||||
@@ -111,28 +106,16 @@ fn keeps_arguments_on_one_line<'a>(e: Expr<'a>, call: Call<'a>, f: &Formatter<'a
|
||||
.any(|argument| is_cast_target(argument, f)))
|
||||
}
|
||||
|
||||
/// ```js
|
||||
/// new A( // comment new A(b); // comment
|
||||
/// b
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// For oxfmt it trails `A`, like any comment that ends the line after it.
|
||||
fn comment_behind_parenthesis_of_new_trails_class(call: Call<'_>, f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt() && call.type_args().is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn write_new_expression<'a>(e: Expr<'a>, call: Call<'a>, f: &mut Formatter<'a>) {
|
||||
let head = format_args!(
|
||||
"new",
|
||||
space(),
|
||||
format_with(
|
||||
|f| match comment_behind_parenthesis_of_new_trails_class(call, f) {
|
||||
true => write!(f, call.callee()),
|
||||
false => write!(f, FormatCallee(call)),
|
||||
}
|
||||
),
|
||||
FormatTypeArguments(e, call),
|
||||
FormatCallee {
|
||||
callee: call.callee(),
|
||||
is_in_member_chain: false
|
||||
},
|
||||
boundary_behind_callee(f),
|
||||
type_arguments(call.type_args(), Node::Expr(e)),
|
||||
boundary_behind_callee(f).filter(|_| !call.type_args().is_empty())
|
||||
);
|
||||
if is_template_on_its_own_line_only_argument(call.args(), f) {
|
||||
@@ -145,6 +128,7 @@ pub(crate) fn write_new_expression<'a>(e: Expr<'a>, call: Call<'a>, f: &mut Form
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_import_expression<'a>(
|
||||
e: Expr<'a>,
|
||||
args: List<'a, Expr<'a>>,
|
||||
@@ -153,7 +137,7 @@ pub(crate) fn write_import_expression<'a>(
|
||||
let phase = e.import_call_phase();
|
||||
let head = format_args!("import", phase.map(|_| "."), phase);
|
||||
if is_template_on_its_own_line_only_argument(args, f)
|
||||
|| is_lone_string_without_comments(e, args, f)
|
||||
|| is_lone_string_without_comments(args, f)
|
||||
{
|
||||
return write!(f, [head, FormatArgumentsOnOneLine(args)]);
|
||||
}
|
||||
@@ -166,53 +150,12 @@ pub(crate) fn write_import_expression<'a>(
|
||||
);
|
||||
}
|
||||
|
||||
/// What is called, with the comments between it and the `?.`, the `<` or the `(`. A comment that
|
||||
/// ends the line stays there, and the rest of the call goes on the next line.
|
||||
struct FormatCallee<'a>(Call<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatCallee<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let call = self.0;
|
||||
let callee = call.callee();
|
||||
// `new A` has no `(` to look for.
|
||||
if f.is_quiet() || call.close_paren().is_none() {
|
||||
return write!(f, [callee, boundary_behind_callee(f)]);
|
||||
}
|
||||
write!(f, FormatNodeWithoutTrailingComments(&callee));
|
||||
write_callee_trailing_comments(call, callee.span().end, f);
|
||||
write!(f, boundary_behind_callee(f));
|
||||
}
|
||||
}
|
||||
|
||||
/// A comment at the end of the line of the callee stays there. oxfmt, like Prettier 3.8, moves it behind
|
||||
/// the call: `call // comment\n()` is `call(); // comment`.
|
||||
fn boundary_behind_callee(f: &Formatter<'_>) -> Option<LineSuffixBoundary> {
|
||||
(!f.options().flavor.is_oxfmt()).then_some(line_suffix_boundary())
|
||||
}
|
||||
|
||||
/// The `<T>` of `a<T>()`, with the comments around it.
|
||||
pub(crate) struct FormatTypeArguments<'a>(pub(crate) Expr<'a>, pub(crate) Call<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatTypeArguments<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let FormatTypeArguments(e, call) = *self;
|
||||
if call.type_args().is_empty() {
|
||||
return;
|
||||
}
|
||||
let type_arguments = type_arguments(call.type_args(), Node::Expr(e));
|
||||
if f.is_quiet() || call.close_paren().is_none() {
|
||||
return write!(f, type_arguments);
|
||||
}
|
||||
let span = AstNodes::TSTypeParameterInstantiation(Node::Expr(e)).span();
|
||||
write!(f, format_leading_comments(span));
|
||||
type_arguments.write_without_comments(f);
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(type_arguments_trailing_comments(call, span.end, f))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `(a, b)`, without a way to break between the arguments.
|
||||
struct FormatArgumentsOnOneLine<'a>(List<'a, Expr<'a>>);
|
||||
|
||||
@@ -241,17 +184,12 @@ fn is_template_on_its_own_line_only_argument<'a>(
|
||||
args.len() == 1 && args.first().is_some_and(is_one)
|
||||
}
|
||||
|
||||
/// Whether the only argument of `e` is a string, and there are no comments around it.
|
||||
fn is_lone_string_without_comments<'a>(
|
||||
e: Expr<'a>,
|
||||
args: List<'a, Expr<'a>>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
/// Whether the only argument is a string without comments.
|
||||
fn is_lone_string_without_comments<'a>(args: List<'a, Expr<'a>>, f: &Formatter<'a>) -> bool {
|
||||
args.len() == 1
|
||||
&& args
|
||||
.first()
|
||||
.is_some_and(|first| first.tag() == ExprTag::String)
|
||||
&& (f.is_quiet() || !f.comments().has_comment_before(e.span().end))
|
||||
&& args.first().is_some_and(|first| {
|
||||
first.tag() == ExprTag::String && (f.is_quiet() || !has_comments(first, f))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_identifier(e: Expr<'_>, name: &[u8]) -> bool {
|
||||
@@ -261,7 +199,7 @@ fn is_identifier(e: Expr<'_>, name: &[u8]) -> bool {
|
||||
/// Prettier's `isSimpleModuleImport` for a call: `require("a")`, `require.resolve("a")`,
|
||||
/// `require.resolve.paths("a")`, `import.meta.resolve("a")`. A long name of a module is no reason
|
||||
/// to break.
|
||||
fn is_simple_module_import<'a>(e: Expr<'a>, call: Call<'a>, f: &Formatter<'a>) -> bool {
|
||||
fn is_simple_module_import<'a>(call: Call<'a>, f: &Formatter<'a>) -> bool {
|
||||
if call.args().len() != 1
|
||||
|| call.chain() != Chain::No
|
||||
|| !call
|
||||
@@ -286,7 +224,12 @@ fn is_simple_module_import<'a>(e: Expr<'a>, call: Call<'a>, f: &Formatter<'a>) -
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
is_module_function && is_lone_string_without_comments(e, call.args(), f)
|
||||
is_module_function && is_lone_string_without_comments(call.args(), f)
|
||||
}
|
||||
|
||||
/// `require("a" // comment⏎)` is `require("a"); // comment` for oxfmt.
|
||||
fn only_leading_comment_breaks_require(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier's `isCommonsJsOrAmdModuleDefinition`: `require("a", b)`, and `define` of AMD.
|
||||
@@ -306,9 +249,13 @@ fn is_commonjs_or_amd_module_definition<'a>(
|
||||
// `require(path.join(__dirname, "a"))` can break.
|
||||
b"require" => {
|
||||
(args.len() > 1 || (args.len() == 1 && is_string(args.first())))
|
||||
&& !args
|
||||
.first()
|
||||
.is_some_and(|first| f.comments().has_comment_before(first.span().start))
|
||||
&& (f.is_quiet()
|
||||
|| !args.first().is_some_and(
|
||||
|first| match only_leading_comment_breaks_require(f) {
|
||||
true => !comments_of_outermost_node(first, f).leading.is_empty(),
|
||||
false => has_comments(first, f),
|
||||
},
|
||||
))
|
||||
}
|
||||
b"define" => {
|
||||
matches!(e.as_chain_element().parent(), AstNodes::ExpressionStatement(statement) if !statement.is_arrow_function_body())
|
||||
|
||||
+295
-440
@@ -1,24 +1,25 @@
|
||||
use super::decorators::FormatDecorators;
|
||||
use super::function::{
|
||||
FormatCommentsBehindParenthesis, FormatFunctionBody, should_group_function_parameters,
|
||||
};
|
||||
use super::function::{FormatFunctionBody, should_group_function_parameters};
|
||||
use super::function_type::write_accessor_keyword;
|
||||
use super::parameters::FormatFormalParameters;
|
||||
use super::program::FormatStatements;
|
||||
use super::semicolon::{OptionalSemicolon, start_of_comments_in_dropped_parentheses};
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use super::type_parameters::{FormatTSTypeParametersOptions, type_arguments, type_parameters};
|
||||
use crate::js::attach::Marker;
|
||||
use crate::js::format::{
|
||||
FormatMemberBeforeAnother, FormatTypeAnnotation, format_node, format_node_without_comments,
|
||||
identifier, no_comment_trails_what_is_before_another,
|
||||
terminator_of_what_is_ignored_follows_semi, write_trailing_comments_of,
|
||||
FormatTypeAnnotation, format_name, format_node, identifier,
|
||||
terminator_of_what_is_ignored_follows_semi,
|
||||
};
|
||||
use crate::js::parentheses::expression::needs_parentheses;
|
||||
use crate::js::trivia::{comments_stay_between_head_and_body, write_head_body_separator};
|
||||
use crate::js::utils::assignment_like::AssignmentLike;
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::object::{format_computed_or_property_key, key_requires_quotes};
|
||||
use crate::js::utils::statement_body::write_space_before_body;
|
||||
use crate::js::utils::string::{FormatLiteralStringToken, StringLiteralParentKind};
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::js::utils::typecast::{
|
||||
comments_of_expression, has_comments_that_count, is_chain_expression_without_comments,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -26,8 +27,12 @@ fn has_modifier(member: Member<'_>, flag: Flags) -> bool {
|
||||
member.modifiers().iter().any(|it| it.flag() == flag)
|
||||
}
|
||||
|
||||
/// The `{ .. }` of a class that has members.
|
||||
/// The `{ .. }` of a class. Prettier's `printClassBody`.
|
||||
fn write_class_body<'a>(class: Class<'a>, f: &mut Formatter<'a>) {
|
||||
if class.members().is_empty() {
|
||||
let comments = dangling_comments(AstNodes::ClassBody(class)).with_block_indent();
|
||||
return write!(f, ["{", comments, "}"]);
|
||||
}
|
||||
let is_consistent = f.options().quote_properties.is_consistent();
|
||||
if is_consistent {
|
||||
let quote_needed = class.members().iter().any(|member| {
|
||||
@@ -45,14 +50,9 @@ fn write_class_body<'a>(class: Class<'a>, f: &mut Formatter<'a>) {
|
||||
let mut iter = class.members().iter().peekable();
|
||||
let mut is_first = true;
|
||||
while let Some(element) = iter.next() {
|
||||
let span = element.span();
|
||||
// `a = 1 ⏎ ⏎ ;[b] = 2`: the `;` is the end of the member before, which this one follows on
|
||||
// the same line.
|
||||
let follows_semicolon =
|
||||
span.start != 0 && f.source_text().byte_at(span.start - 1) == Some(b';');
|
||||
match is_first {
|
||||
true => is_first = false,
|
||||
false if !follows_semicolon && f.lines_before(span) > 1 => write!(f, empty_line()),
|
||||
false if has_empty_line_before(element, f) => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
}
|
||||
let next_element = iter.peek().copied();
|
||||
@@ -72,6 +72,29 @@ fn write_class_body<'a>(class: Class<'a>, f: &mut Formatter<'a>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether there is an empty line between `element` and the member before it.
|
||||
fn has_empty_line_before<'a>(element: Member<'a>, f: &Formatter<'a>) -> bool {
|
||||
let span = element.span();
|
||||
// `a() {}⏎// comment⏎;⏎⏎b() {}`: the comment leads `b`, and the empty line is after the comment.
|
||||
let has_comments = !f.is_quiet() && f.comments().is_owner(Node::Member(element));
|
||||
let comment_before_stray_semicolon = match has_comments {
|
||||
true => (f.comments().of_node(element.as_ast_nodes()).leading.first())
|
||||
.filter(|comment| comment.next_token_start() != span.start),
|
||||
false => None,
|
||||
};
|
||||
if let Some(comment) = comment_before_stray_semicolon {
|
||||
return crate::text::is_previous_line_empty(
|
||||
f.source_text().as_bytes(),
|
||||
comment.span().start as usize,
|
||||
);
|
||||
}
|
||||
// `a = 1 ⏎ ⏎ ;[b] = 2`: the `;` is the end of the member before, which this one follows on
|
||||
// the same line.
|
||||
let follows_semicolon =
|
||||
span.start != 0 && f.source_text().byte_at(span.start - 1) == Some(b';');
|
||||
!follows_semicolon && f.lines_before(span) > 1
|
||||
}
|
||||
|
||||
/// A member of a class, an interface or a type literal.
|
||||
pub(crate) fn write_member<'a>(member: Member<'a>, f: &mut Formatter<'a>) {
|
||||
match member.as_ast_nodes() {
|
||||
@@ -117,52 +140,31 @@ fn write_method_definition<'a>(member: Member<'a>, f: &mut Formatter<'a>) {
|
||||
);
|
||||
let node = AstNodes::MethodDefinition(member);
|
||||
match (member.key(), member.constructor_keyword()) {
|
||||
(_, Some(keyword)) => write_constructor_keyword(keyword, node, f),
|
||||
(_, Some(keyword)) => write_constructor_keyword(keyword, f),
|
||||
(Some(key), None) => format_computed_or_property_key(key, node, f),
|
||||
(None, None) => {}
|
||||
}
|
||||
write!(f, member.flags().contains(Flags::OPTIONAL).then_some("?"));
|
||||
|
||||
// `a(/* prettier-ignore */) {}`: typescript-estree has a `FunctionExpression` here, which is copied up to its end.
|
||||
let function = AstNodes::Function(value);
|
||||
if !f.is_quiet() && f.comments().is_ignored(function) {
|
||||
return format_node(function, f, |_| {});
|
||||
}
|
||||
format_grouped_parameters_with_return_type_for_method(value, f);
|
||||
|
||||
if value.has_body() {
|
||||
return write!(f, FormatFunctionBody(value));
|
||||
}
|
||||
if !f.is_quiet() && !all_comments_before_semicolon_go_behind_it(f) {
|
||||
let limit = f
|
||||
.comments()
|
||||
.start_of_comments_before_semicolon(None, member.span());
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(f.comments().comments_before(limit))
|
||||
);
|
||||
}
|
||||
write!(f, OptionalSemicolon);
|
||||
}
|
||||
|
||||
/// `#a = 1 /* comment */;` stays as it is in JavaScript: Babel's `ClassPrivateProperty` is not in the list of Prettier's
|
||||
/// `handleMethodNameComments`, so the comment trails the value.
|
||||
fn is_no_property_to_handle_method_name_comments<'a>(
|
||||
member: Member<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
f.context().has_tree_of_babel()
|
||||
&& !f.options().flavor.is_oxfmt()
|
||||
&& (member.key()).is_some_and(|key| matches!(key.kind(), KeyKind::Private(_)))
|
||||
}
|
||||
|
||||
/// For oxfmt all comments between a member and its `;` are behind the `;`. For Prettier those that end or
|
||||
/// start their line are in the member: see `Comments::start_of_comments_before_semicolon`.
|
||||
fn all_comments_before_semicolon_go_behind_it(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
fn constructor_keeps_override(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `constructor`, which can be written as a string, and is quoted like any other name.
|
||||
fn write_constructor_keyword<'a>(keyword: Ident<'a>, node: AstNodes<'a>, f: &mut Formatter<'a>) {
|
||||
fn write_constructor_keyword<'a>(keyword: Ident<'a>, f: &mut Formatter<'a>) {
|
||||
let span = keyword.span();
|
||||
let source = f.source_text().text_for(&span);
|
||||
if source.starts_with(b"\"") || source.starts_with(b"'") {
|
||||
@@ -174,32 +176,18 @@ fn write_constructor_keyword<'a>(keyword: Ident<'a>, node: AstNodes<'a>, f: &mut
|
||||
QuoteProperties::Preserve => false,
|
||||
QuoteProperties::Consistent => !f.context().is_quote_needed(),
|
||||
};
|
||||
format_node(
|
||||
span,
|
||||
|| node,
|
||||
f,
|
||||
|f| match is_unquoted {
|
||||
true => write!(f, source_text(span.shrink(1, 1))),
|
||||
false => write!(
|
||||
f,
|
||||
FormatLiteralStringToken::new(
|
||||
source,
|
||||
false,
|
||||
StringLiteralParentKind::Expression
|
||||
)
|
||||
),
|
||||
},
|
||||
);
|
||||
format_name(span, f, |f| match is_unquoted {
|
||||
true => write!(f, source_text(span.shrink(1, 1))),
|
||||
false => write!(
|
||||
f,
|
||||
FormatLiteralStringToken::new(source, false, StringLiteralParentKind::Expression)
|
||||
),
|
||||
});
|
||||
} else if f.context().is_quote_needed() {
|
||||
let quote = f.options().quote_style.as_str();
|
||||
format_node(
|
||||
span,
|
||||
|| node,
|
||||
f,
|
||||
|f| write!(f, [quote, source_text(span), quote]),
|
||||
);
|
||||
format_name(span, f, |f| write!(f, [quote, source_text(span), quote]));
|
||||
} else {
|
||||
write!(f, identifier(keyword, node));
|
||||
write!(f, identifier(keyword));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,10 +208,22 @@ fn write_static_block<'a>(member: Member<'a>, f: &mut Formatter<'a>) {
|
||||
.and_then(Func::body_statements)
|
||||
.filter(|body| !is_empty(body))
|
||||
{
|
||||
// `static {⏎;⏎// comment⏎}` is `static {⏎}⏎// comment` for oxfmt.
|
||||
Some(body) if body.iter().all(|it| it.tag() == StmtTag::Empty) => {
|
||||
let comments = f.comments().of_node(AstNodes::StaticBlock(member)).dangling;
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
hard_line_break(),
|
||||
"}",
|
||||
FormatTrailingComments::Comments(comments)
|
||||
]
|
||||
);
|
||||
}
|
||||
Some(body) => write!(f, block_indent(&FormatStatements(body))),
|
||||
None => write!(
|
||||
f,
|
||||
format_dangling_comments(member.span()).with_block_indent()
|
||||
dangling_comments(AstNodes::StaticBlock(member)).with_block_indent()
|
||||
),
|
||||
}
|
||||
write!(f, "}");
|
||||
@@ -281,26 +281,21 @@ impl Spanned for FormatIndexSignatureName<'_> {
|
||||
impl<'a> Format<'a> for FormatIndexSignatureName<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let param = self.0;
|
||||
format_node(
|
||||
param.span(),
|
||||
|| param.as_ast_nodes().parent(),
|
||||
f,
|
||||
|f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
param.is_rest().then_some("..."),
|
||||
source_text(param.pat().span()),
|
||||
param.is_optional().then_some("?"),
|
||||
param.ty().map(FormatTypeAnnotation)
|
||||
]
|
||||
);
|
||||
// An error.
|
||||
if let Some(default) = param.default() {
|
||||
write!(f, [space(), "=", space(), default]);
|
||||
}
|
||||
},
|
||||
);
|
||||
format_node(param.as_ast_nodes(), f, |f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
param.is_rest().then_some("..."),
|
||||
param.pat(),
|
||||
param.is_optional().then_some("?"),
|
||||
param.ty().map(FormatTypeAnnotation)
|
||||
]
|
||||
);
|
||||
// An error.
|
||||
if let Some(default) = param.default() {
|
||||
write!(f, [space(), "=", space(), default]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,22 +309,28 @@ pub(crate) struct FormatClassImplements<'a>(pub(crate) List<'a, TypeNode<'a>>);
|
||||
|
||||
impl<'a> Format<'a> for FormatClassImplements<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let last_index = self.0.len().saturating_sub(1);
|
||||
let mut joiner = f.join_with(soft_line_break_or_space());
|
||||
for (i, heritage) in FormatSeparatedIter::new(self.0.iter(), ",")
|
||||
.with_trailing_separator(TrailingSeparator::Disallowed)
|
||||
.enumerate()
|
||||
{
|
||||
// The comments after the last are written in the body.
|
||||
match i == last_index {
|
||||
true => joiner.entry(&FormatNodeWithoutTrailingComments(&heritage)),
|
||||
false => joiner.entry(&heritage),
|
||||
};
|
||||
}
|
||||
f.join_with(soft_line_break_or_space())
|
||||
.entries_with_trailing_separator(self.0.iter(), ",", TrailingSeparator::Disallowed);
|
||||
}
|
||||
}
|
||||
|
||||
/// The dangling comments of `node` that have `marker`: Prettier's `printDanglingComments(path, options, { marker })`.
|
||||
pub(crate) fn dangling_comments_with_marker<'a>(
|
||||
node: AstNodes<'a>,
|
||||
marker: Marker,
|
||||
f: &Formatter<'a>,
|
||||
) -> CommentList<'a> {
|
||||
let comments = f.comments().of_node(node).dangling;
|
||||
let has_marker = |comment: &Comment<'a>| {
|
||||
(f.comments().attachment(*comment)).is_some_and(|attachment| attachment.marker == marker)
|
||||
};
|
||||
let start = comments.iter().take_while(|it| !has_marker(it)).count();
|
||||
let count = comments.iter().skip(start).take_while(has_marker).count();
|
||||
comments.slice(start, start + count)
|
||||
}
|
||||
|
||||
/// A class declaration or expression.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_class<'a>(class: Class<'a>, f: &mut Formatter<'a>) {
|
||||
match class.owner() {
|
||||
Node::Expr(e) if class.decorators().next().is_some() && needs_parentheses(e, f) => {
|
||||
@@ -369,7 +370,6 @@ impl<'a> Format<'a> for FormatClass<'a> {
|
||||
let is_expression = matches!(class.owner(), Node::Expr(_));
|
||||
let super_class = class.extends();
|
||||
let implements = class.implements();
|
||||
let body_span = class.body_span();
|
||||
|
||||
// Those of an exported class are written with the `export`, which they can be before.
|
||||
if is_expression
|
||||
@@ -387,125 +387,97 @@ impl<'a> Format<'a> for FormatClass<'a> {
|
||||
}
|
||||
write!(f, "class");
|
||||
|
||||
let gaps = HeadGaps::new(class);
|
||||
// The trailing comments of the name and of the type parameters are indented.
|
||||
let head = format_with(|f| {
|
||||
if let Some(id) = class.name() {
|
||||
let comments = f.comments().of_name(id.span().start);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
space(),
|
||||
FormatNodeWithoutTrailingComments(&identifier(id, node))
|
||||
FormatLeadingComments::Comments(comments.leading),
|
||||
source_text(id.span()),
|
||||
indent(&FormatTrailingComments::Comments(comments.trailing))
|
||||
]
|
||||
);
|
||||
write!(f, indent(&FormatCommentsTrailingInHead(gaps.after_name)));
|
||||
}
|
||||
if let Some(span) = class.type_params().angle_brackets_span() {
|
||||
let group_id = Some(f.group_id("type_parameters"));
|
||||
if !class.type_params().is_empty() {
|
||||
let options = FormatTSTypeParametersOptions {
|
||||
group_id,
|
||||
group_id: Some(f.group_id("type_parameters")),
|
||||
is_type_or_interface_decl: false,
|
||||
};
|
||||
write!(f, format_leading_comments(span));
|
||||
type_parameters(class.type_params(), Node::Class(class))
|
||||
.with_options(options)
|
||||
.write_without_comments(f);
|
||||
let type_parameters =
|
||||
type_parameters(class.type_params(), Node::Class(class)).with_options(options);
|
||||
write!(
|
||||
f,
|
||||
indent(&FormatCommentsTrailingInHead(gaps.after_type_parameters))
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&type_parameters),
|
||||
indent(&trailing_comments(AstNodes::TSTypeParameterDeclaration(
|
||||
Node::Class(class)
|
||||
)))
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let group_mode = should_group(class, parent, &gaps, f);
|
||||
let group_mode = should_group(class, parent, f);
|
||||
|
||||
let format_heritage_clauses = format_with(|f| {
|
||||
if let Some(extends) = super_class {
|
||||
let format_super = format_with(|f| {
|
||||
write!(f, format_leading_comments(extends.span()));
|
||||
let format_type_arguments = format_with(|f| {
|
||||
write!(f, FormatCommentsTrailingInHead(gaps.after_super_class));
|
||||
if let Some(span) = class.extends_args().angle_brackets_span() {
|
||||
write!(f, format_leading_comments(span));
|
||||
type_arguments(class.extends_args(), Node::Class(class))
|
||||
.write_without_comments(f);
|
||||
write!(
|
||||
f,
|
||||
FormatCommentsTrailingInHead(gaps.after_super_type_arguments)
|
||||
);
|
||||
}
|
||||
});
|
||||
let are_type_arguments_in_parentheses =
|
||||
type_arguments_of_super_class_are_in_its_parentheses(f);
|
||||
let content = format_with(|f| {
|
||||
write!(f, FormatNodeWithoutTrailingComments(&extends));
|
||||
if are_type_arguments_in_parentheses {
|
||||
write!(f, format_type_arguments);
|
||||
}
|
||||
});
|
||||
|
||||
// Prettier's `printSuperClass`.
|
||||
if matches!(parent, AstNodes::AssignmentExpression(_)) {
|
||||
let content = content.memoized();
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
if_group_breaks(&format_args!(
|
||||
"(",
|
||||
soft_block_indent(&content),
|
||||
")"
|
||||
)),
|
||||
if_group_fits_on_line(&content)
|
||||
))
|
||||
);
|
||||
} else {
|
||||
content.fmt(f);
|
||||
}
|
||||
if !are_type_arguments_in_parentheses {
|
||||
write!(f, format_type_arguments);
|
||||
}
|
||||
});
|
||||
|
||||
let format_extends = format_args!("extends", space(), format_super);
|
||||
let format_extends =
|
||||
format_args!("extends", space(), FormatSuperClass { class, extends });
|
||||
match group_mode {
|
||||
true => write!(f, [soft_line_break_or_space(), group(&format_extends)]),
|
||||
false => write!(f, [space(), format_extends]),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(first) = implements.first() {
|
||||
// Those on a line with code, after the `implements`, lead the first type.
|
||||
let comments = f.comments().comments_before(first.span().start);
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|it| it.preceded_by_newline() || it.followed_by_newline())
|
||||
.count();
|
||||
let leading_comments = comments.get(..count).unwrap_or_default();
|
||||
// Prettier's `printHeritageClauses`
|
||||
if !implements.is_empty() {
|
||||
let comments = match f.is_quiet() {
|
||||
true => CommentList::empty(),
|
||||
false => dangling_comments_with_marker(node, Marker::Implements, f),
|
||||
};
|
||||
let format_comments = FormatDanglingComments::Comments {
|
||||
comments,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
let implements = FormatClassImplements(implements);
|
||||
|
||||
if usize::from(super_class.is_some()) + implements.0.len() > 1 {
|
||||
if has_multiple_heritage(class) {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
soft_line_break_or_space(),
|
||||
FormatLeadingComments::Comments(leading_comments),
|
||||
(!leading_comments.is_empty()).then_some(hard_line_break()),
|
||||
format_with(|f| match keeps_empty_lines_before_implements(f) {
|
||||
true => write_comments_with_empty_lines(comments, f),
|
||||
false => write!(
|
||||
f,
|
||||
[
|
||||
format_comments,
|
||||
(!comments.is_empty()).then_some(hard_line_break())
|
||||
]
|
||||
),
|
||||
}),
|
||||
"implements",
|
||||
group(&soft_line_indent_or_space(&implements))
|
||||
]
|
||||
);
|
||||
} else {
|
||||
let format_comments = FormatDanglingComments::Comments {
|
||||
comments: leading_comments,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
let needs_line_break = leading_comments.last().is_some_and(|it| it.is_line());
|
||||
let format_inner = format_args!(
|
||||
"implements",
|
||||
space(),
|
||||
format_comments,
|
||||
needs_line_break.then_some(hard_line_break()),
|
||||
implements
|
||||
);
|
||||
// `class // comment⏎implements A {}`: Prettier 3.9.9 prints `class implements // commentA {}`.
|
||||
let are_before_keyword = comments_are_before_implements(f)
|
||||
|| comments.iter().any(|comment| comment.is_line());
|
||||
let format_inner = format_with(|f| match comments.last() {
|
||||
Some(last) if are_before_keyword => {
|
||||
write!(f, format_comments);
|
||||
match last.is_line() || last.has_newline_after() {
|
||||
true => write!(f, hard_line_break()),
|
||||
false => write!(f, space()),
|
||||
}
|
||||
write!(f, ["implements", space(), implements]);
|
||||
}
|
||||
_ => write!(f, ["implements", space(), format_comments, implements]),
|
||||
});
|
||||
match group_mode {
|
||||
true => write!(f, [soft_line_break_or_space(), group(&format_inner)]),
|
||||
false => write!(f, [space(), format_inner]),
|
||||
@@ -522,182 +494,140 @@ impl<'a> Format<'a> for FormatClass<'a> {
|
||||
.with_group_id(Some(heritage_id))
|
||||
);
|
||||
// The `{` on a line of its own sets the members apart from a head that is broken.
|
||||
if class.members().is_empty() {
|
||||
write!(f, space());
|
||||
} else {
|
||||
if !class.members().is_empty() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
if_group_breaks(&hard_line_break()).with_group_id(Some(heritage_id)),
|
||||
if_group_fits_on_line(&space()).with_group_id(Some(heritage_id))
|
||||
]
|
||||
if_group_breaks(&hard_line_break()).with_group_id(Some(heritage_id))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
write!(f, [head, format_heritage_clauses, space()]);
|
||||
write!(f, [head, format_heritage_clauses]);
|
||||
}
|
||||
|
||||
// Prettier's `handleClassComments`: a comment before the `{` that starts or ends its line is
|
||||
// moved into the body.
|
||||
if !f.is_quiet() && comments_stay_between_head_and_body(f) {
|
||||
write_head_body_separator(body_span.start, f);
|
||||
} else {
|
||||
let comments = f.comments().comments_before(body_span.start);
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|it| !it.preceded_by_newline() && !it.followed_by_newline())
|
||||
.count();
|
||||
write!(
|
||||
f,
|
||||
FormatLeadingComments::Comments(comments.get(..count).unwrap_or_default())
|
||||
);
|
||||
}
|
||||
|
||||
if class.members().is_empty() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
"{",
|
||||
format_dangling_comments(node.span()).with_block_indent(),
|
||||
"}"
|
||||
]
|
||||
);
|
||||
} else {
|
||||
format_node_without_comments(body_span, || node, f, |f| write_class_body(class, f));
|
||||
}
|
||||
write_space_before_body(AstNodes::ClassBody(class), f);
|
||||
format_node(AstNodes::ClassBody(class), f, |f| {
|
||||
write_class_body(class, f)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// What follows a part of the head of a class.
|
||||
#[derive(Copy, Clone)]
|
||||
enum HeadGap {
|
||||
/// The `<` of type parameters or type arguments.
|
||||
AngleBracket,
|
||||
/// `extends` and the super class.
|
||||
Extends {
|
||||
/// It is an optional chain, which Prettier attaches no comments to. So what follows the
|
||||
/// comment is not the super class, and `handleClassComments` has nothing to say.
|
||||
is_chain: bool,
|
||||
},
|
||||
/// `implements` and the first type.
|
||||
Implements {
|
||||
/// What is before it is the name, the type parameters or the super class, as opposed to its
|
||||
/// type arguments or what is in an optional chain.
|
||||
follows_known_part: bool,
|
||||
},
|
||||
/// `class A extends B /* comment */⏎<C> {}` is `B<C> /* comment */` for Prettier and `B /* comment */<C>` for oxfmt.
|
||||
fn comments_of_super_class_are_around_its_type_arguments(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Between two parts of the head of a class: where the first ends, where the second starts, and
|
||||
/// what the second is. `None` if the `{` is next.
|
||||
type HeadGapAt = Option<(u32, u32, HeadGap)>;
|
||||
|
||||
struct HeadGaps {
|
||||
after_name: HeadGapAt,
|
||||
after_type_parameters: HeadGapAt,
|
||||
after_super_class: HeadGapAt,
|
||||
after_super_type_arguments: HeadGapAt,
|
||||
/// What follows `extends`: the super class with its type arguments. The comments of the super class are around both.
|
||||
struct FormatSuperClass<'a> {
|
||||
class: Class<'a>,
|
||||
extends: Expr<'a>,
|
||||
}
|
||||
|
||||
impl HeadGaps {
|
||||
fn new(class: Class<'_>) -> Self {
|
||||
let implements = |follows_known_part| {
|
||||
class
|
||||
.implements()
|
||||
.first()
|
||||
.map(|it| (it.span().start, HeadGap::Implements { follows_known_part }))
|
||||
};
|
||||
let is_chain = class.extends().is_some_and(Expr::is_chain_root);
|
||||
let heritage = || {
|
||||
class
|
||||
.extends()
|
||||
.map(|it| (it.span().start, HeadGap::Extends { is_chain }))
|
||||
.or_else(|| implements(true))
|
||||
};
|
||||
let angle_bracket = |span: Option<Span>| span.map(|it| (it.start, HeadGap::AngleBracket));
|
||||
let type_parameters = class.type_params().angle_brackets_span();
|
||||
let type_arguments = class.extends_args().angle_brackets_span();
|
||||
let gap = |end: Option<u32>, next: Option<(u32, HeadGap)>| Some((end?, next?.0, next?.1));
|
||||
HeadGaps {
|
||||
after_name: gap(
|
||||
class.name().map(|it| it.span().end),
|
||||
angle_bracket(type_parameters).or_else(heritage),
|
||||
),
|
||||
after_type_parameters: gap(type_parameters.map(|it| it.end), heritage()),
|
||||
after_super_class: gap(
|
||||
class.extends().map(|it| it.outer_span().end),
|
||||
angle_bracket(type_arguments).or_else(|| implements(!is_chain)),
|
||||
),
|
||||
after_super_type_arguments: gap(type_arguments.map(|it| it.end), implements(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How many of `comments`, which are in `gap`, trail what is before them. The others lead what
|
||||
/// follows. Prettier's `handleClassComments`, and what it does with any comment.
|
||||
fn count_comments_trailing_in_head(
|
||||
comments: &[Comment],
|
||||
gap: HeadGap,
|
||||
next_start: u32,
|
||||
f: &Formatter<'_>,
|
||||
) -> usize {
|
||||
let is_before = |comment: &Comment, keyword: &[u8]| {
|
||||
bun_core::strings::contains(
|
||||
f.source_text()
|
||||
.text_for(&Span::new(comment.end(), next_start)),
|
||||
keyword,
|
||||
)
|
||||
};
|
||||
(comments.iter())
|
||||
.take_while(|comment| {
|
||||
let starts_or_ends_line =
|
||||
comment.preceded_by_newline() || comment.followed_by_newline();
|
||||
match gap {
|
||||
HeadGap::AngleBracket => {
|
||||
!comment.preceded_by_newline() && comment.followed_by_newline()
|
||||
}
|
||||
HeadGap::Extends { is_chain } => match starts_or_ends_line {
|
||||
true => !is_chain || !comment.preceded_by_newline(),
|
||||
false => is_before(comment, b"extends"),
|
||||
},
|
||||
HeadGap::Implements { follows_known_part } => match starts_or_ends_line {
|
||||
true => follows_known_part,
|
||||
false => is_before(comment, b"implements"),
|
||||
},
|
||||
}
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
/// The comments in a gap that trail what is before them.
|
||||
struct FormatCommentsTrailingInHead(HeadGapAt);
|
||||
|
||||
impl<'a> Format<'a> for FormatCommentsTrailingInHead {
|
||||
impl<'a> Format<'a> for FormatSuperClass<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if !f.is_quiet()
|
||||
&& let Some((_, next_start, gap)) = self.0
|
||||
{
|
||||
let comments = f.comments().comments_before(next_start);
|
||||
let count = count_comments_trailing_in_head(comments, gap, next_start, f);
|
||||
let FormatSuperClass { class, extends } = *self;
|
||||
// A JSX element writes its own comments, and so does what is in a `ChainExpression`.
|
||||
let comments = (!f.is_quiet()
|
||||
&& comments_of_super_class_are_around_its_type_arguments(f)
|
||||
&& extends.tag() != ExprTag::Jsx
|
||||
&& !is_chain_expression_without_comments(extends, f))
|
||||
.then(|| comments_of_expression(extends, f));
|
||||
let type_arguments = type_arguments(class.extends_args(), Node::Class(class));
|
||||
let are_type_arguments_in_parentheses =
|
||||
type_arguments_of_super_class_are_in_its_parentheses(f);
|
||||
let content = format_with(|f| {
|
||||
match comments {
|
||||
Some(_) => write!(f, FormatNodeWithoutTrailingComments(&extends)),
|
||||
None => write!(f, extends),
|
||||
}
|
||||
if are_type_arguments_in_parentheses {
|
||||
write!(f, type_arguments);
|
||||
}
|
||||
});
|
||||
|
||||
// They are written here, so `extends` does not write them again.
|
||||
if let Some(comments) = comments {
|
||||
// `extends a⏎// prettier-ignore⏎.b`: the comment leads `a.b`, and is copied with it.
|
||||
let is_copied = |comment: &Comment<'a>| {
|
||||
comments.is_ignored && comment.span().start >= extends.span().start
|
||||
};
|
||||
let count = (comments.leading.iter())
|
||||
.take_while(|it| !is_copied(it))
|
||||
.count();
|
||||
let leading_comments = comments.leading.slice(0, count);
|
||||
write!(f, FormatLeadingComments::Comments(leading_comments));
|
||||
}
|
||||
// Prettier's `printSuperClass`.
|
||||
if matches!(
|
||||
AstNodes::Class(class).parent(),
|
||||
AstNodes::AssignmentExpression(_)
|
||||
) {
|
||||
let content = content.memoized();
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments.get(..count).unwrap_or_default())
|
||||
group(&format_args!(
|
||||
if_group_breaks(&format_args!("(", soft_block_indent(&content), ")")),
|
||||
if_group_fits_on_line(&content)
|
||||
))
|
||||
);
|
||||
} else {
|
||||
content.fmt(f);
|
||||
}
|
||||
if !are_type_arguments_in_parentheses {
|
||||
write!(f, type_arguments);
|
||||
}
|
||||
if let Some(comments) = comments {
|
||||
write!(f, FormatTrailingComments::Comments(comments.trailing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `hasMultipleHeritage`.
|
||||
fn has_multiple_heritage(class: Class<'_>) -> bool {
|
||||
usize::from(class.extends().is_some()) + class.implements().len() > 1
|
||||
}
|
||||
|
||||
/// `class /* comment */ implements A {}` is `class implements /* comment */ A {}` for Prettier.
|
||||
fn comments_are_before_implements(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `class A extends B⏎// comment⏎⏎implements C {}`: oxfmt keeps the empty line after the comment.
|
||||
fn keeps_empty_lines_before_implements(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Each of `comments` on its own line, and the empty lines after them.
|
||||
fn write_comments_with_empty_lines<'a>(comments: CommentList<'a>, f: &mut Formatter<'a>) {
|
||||
for index in 0..comments.len() {
|
||||
let comments = comments.slice(index, index + 1);
|
||||
let is_next_line_empty = (comments.first())
|
||||
.is_some_and(|comment| f.source_text().lines_after(comment.span().end) > 1);
|
||||
let indent = DanglingIndentMode::None;
|
||||
write!(f, FormatDanglingComments::Comments { comments, indent });
|
||||
match is_next_line_empty {
|
||||
true => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `shouldPrintClassInGroupMode`: whether the head of the class is a group that can break
|
||||
/// before `extends` and `implements`.
|
||||
fn should_group<'a>(
|
||||
class: Class<'a>,
|
||||
parent: AstNodes<'a>,
|
||||
gaps: &HeadGaps,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
fn should_group<'a>(class: Class<'a>, parent: AstNodes<'a>, f: &Formatter<'a>) -> bool {
|
||||
let (super_class, implements) = (class.extends(), class.implements());
|
||||
if usize::from(super_class.is_some()) + implements.len() > 1 {
|
||||
if has_multiple_heritage(class) {
|
||||
return true;
|
||||
}
|
||||
if !f.is_quiet() {
|
||||
let comments = f.comments();
|
||||
let type_parameters = AstNodes::TSTypeParameterDeclaration(Node::Class(class));
|
||||
if (class.name()).is_some_and(|id| !comments.of_name(id.span().start).trailing.is_empty())
|
||||
|| !comments.of_node(type_parameters).trailing.is_empty()
|
||||
|| super_class.is_some_and(|super_class| has_comments_that_count(super_class, f))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Prettier's `isMemberExpression(stripChainElementWrappers(e))`.
|
||||
let is_member = |mut e: Expr<'a>| {
|
||||
@@ -706,7 +636,7 @@ fn should_group<'a>(
|
||||
}
|
||||
matches!(e.kind(), ExprKind::Dot { .. } | ExprKind::Index { .. })
|
||||
};
|
||||
let is_member_heritage = match (super_class, implements.first()) {
|
||||
match (super_class, implements.first()) {
|
||||
(Some(super_class), _) => {
|
||||
!matches!(parent, AstNodes::AssignmentExpression(_))
|
||||
&& class.extends_args().is_empty()
|
||||
@@ -718,37 +648,7 @@ fn should_group<'a>(
|
||||
_ => false,
|
||||
},
|
||||
(None, None) => false,
|
||||
};
|
||||
if is_member_heritage || f.is_quiet() {
|
||||
return is_member_heritage;
|
||||
}
|
||||
|
||||
// A comment trails the name or the type parameters, or is around the super class.
|
||||
let counts = |gap: HeadGapAt| {
|
||||
gap.map_or((0, 0), |(previous_end, next_start, gap)| {
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_in(Span::new(previous_end, next_start));
|
||||
(
|
||||
comments.len(),
|
||||
count_comments_trailing_in_head(comments, gap, next_start, f),
|
||||
)
|
||||
})
|
||||
};
|
||||
let before_super_class = match (gaps.after_type_parameters, gaps.after_name, super_class) {
|
||||
(Some(gap), ..) | (None, Some(gap), _) if matches!(gap.2, HeadGap::Extends { .. }) => {
|
||||
let (all, trailing) = counts(Some(gap));
|
||||
all - trailing
|
||||
}
|
||||
(None, None, Some(super_class)) => {
|
||||
f.comments().comments_before(super_class.span().start).len()
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
counts(gaps.after_name).1 > 0
|
||||
|| counts(gaps.after_type_parameters).1 > 0
|
||||
|| before_super_class > 0
|
||||
|| counts(gaps.after_super_class).1 > 0
|
||||
}
|
||||
|
||||
/// A member of a class and the `;` after it.
|
||||
@@ -816,23 +716,16 @@ impl FormatClassElementWithSemicolon<'_> {
|
||||
|
||||
impl<'a> FormatClassElementWithSemicolon<'a> {
|
||||
/// See [`terminator_of_what_is_ignored_follows_semi`]. Returns whether the member is not formatted.
|
||||
fn write_ignored_without_terminator(&self, f: &mut Formatter<'a>) -> bool {
|
||||
fn write_ignored_without_terminator(&self, f: &mut Formatter<'a>) {
|
||||
let span = self.element.span();
|
||||
let has_terminator = match self.element.kind() {
|
||||
MemberKind::Property | MemberKind::IndexSignature => true,
|
||||
_ => self.element.func().is_some_and(|func| !func.has_body()),
|
||||
};
|
||||
let content = match has_terminator {
|
||||
true => f.comments().without_semicolon(span),
|
||||
true => f.file().without_semicolon(span),
|
||||
false => span,
|
||||
};
|
||||
let is_suppressed = f.comments().is_suppressed(span.start)
|
||||
|| f.comments().has_trailing_suppression_comment(span.end)
|
||||
|| (content.end < span.end
|
||||
&& f.comments().has_trailing_suppression_comment(content.end));
|
||||
if !is_suppressed {
|
||||
return false;
|
||||
}
|
||||
let terminator = has_terminator
|
||||
&& match f.options().semicolons {
|
||||
Semicolons::Always => true,
|
||||
@@ -840,107 +733,69 @@ impl<'a> FormatClassElementWithSemicolon<'a> {
|
||||
self.element.kind() == MemberKind::Property && self.needs_semicolon()
|
||||
}
|
||||
};
|
||||
let node = self.element.as_ast_nodes();
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
format_leading_comments(span),
|
||||
leading_comments(node),
|
||||
FormatSuppressedNode(content),
|
||||
terminator.then_some(";")
|
||||
terminator.then_some(";"),
|
||||
trailing_comments(node)
|
||||
]
|
||||
);
|
||||
write_trailing_comments_of(self.element.as_ast_nodes(), f);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatClassElementWithSemicolon<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let span = self.element.span();
|
||||
if !f.is_quiet()
|
||||
&& terminator_of_what_is_ignored_follows_semi(f)
|
||||
&& self.write_ignored_without_terminator(f)
|
||||
{
|
||||
return;
|
||||
let node = self.element.as_ast_nodes();
|
||||
let is_ignored = !f.is_quiet() && f.comments().is_ignored(node);
|
||||
if is_ignored && terminator_of_what_is_ignored_follows_semi(f) {
|
||||
return self.write_ignored_without_terminator(f);
|
||||
}
|
||||
let is_suppressed = f.comments().is_suppressed(span.start)
|
||||
|| f.comments().has_trailing_suppression_comment(span.end);
|
||||
let needs_semi = self.element.kind() == MemberKind::Property
|
||||
&& match f.options().semicolons {
|
||||
Semicolons::Always => !is_suppressed,
|
||||
Semicolons::Always => !is_ignored,
|
||||
Semicolons::AsNeeded => self.needs_semicolon(),
|
||||
};
|
||||
|
||||
if f.is_quiet() {
|
||||
let is_added_after_comments =
|
||||
f.options().semicolons.is_as_needed() && needed_semicolon_is_after_comments(f);
|
||||
if !needs_semi || f.is_quiet() || is_added_after_comments {
|
||||
return write!(f, [self.element, needs_semi.then_some(";")]);
|
||||
}
|
||||
if is_suppressed {
|
||||
// The `;` that keeps the next member apart is written in any case.
|
||||
write!(f, [self.element, needs_semi.then_some(";")]);
|
||||
} else if needs_semi {
|
||||
// The comments on the line of the `;` are written behind it. They are in the member:
|
||||
// `a = 1 // prettier-ignore ⏎ ;` is formatted.
|
||||
let limit = match is_no_property_to_handle_method_name_comments(self.element, f) {
|
||||
true => span.end,
|
||||
false => f.comments().start_of_comments_before_semicolon(
|
||||
self.element.init().map(|it| it.span().end),
|
||||
span,
|
||||
),
|
||||
};
|
||||
let limit = match all_comments_before_semicolon_go_behind_it(f) {
|
||||
true => (self.element.init())
|
||||
.and_then(|it| start_of_comments_in_dropped_parentheses(it, None, f))
|
||||
.unwrap_or_else(|| f.comments().without_semicolon(span).end)
|
||||
.min(limit),
|
||||
false => limit,
|
||||
};
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(limit);
|
||||
write!(f, self.element);
|
||||
// Nothing follows them in the member.
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(f.comments().comments_before(span.end))
|
||||
);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
write!(f, ";");
|
||||
if all_comments_before_semicolon_go_behind_it(f) {
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(f.comments().comments_before(span.end))
|
||||
);
|
||||
}
|
||||
if !(self.next_element.is_some() && no_comment_trails_what_is_before_another(f)) {
|
||||
write_trailing_comments_of(self.element.as_ast_nodes(), f);
|
||||
}
|
||||
} else {
|
||||
match self.next_element {
|
||||
Some(_) => write!(f, FormatMemberBeforeAnother(self.element)),
|
||||
None => write!(f, self.element),
|
||||
}
|
||||
// Those before the `;` of the source, which is the end of the member.
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(f.comments().comments_before(span.end))
|
||||
);
|
||||
}
|
||||
// The `;` is part of the member, so the comments that trail the member are behind it.
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&self.element),
|
||||
";",
|
||||
trailing_comments(node)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Without semicolons, Prettier's `printClassBody` adds the `;` that is needed after the member and its comments:
|
||||
/// `static /* comment */;`. oxfmt prints `static; /* comment */`
|
||||
fn needed_semicolon_is_after_comments(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier's `printMethodValue`: the type parameters, the parameters and the return type of a
|
||||
/// method.
|
||||
pub(crate) fn format_grouped_parameters_with_return_type_for_method<'a>(
|
||||
func: Func<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
// typescript-estree has a `FunctionExpression` in a method, which starts here.
|
||||
if !f.is_quiet() {
|
||||
write!(f, leading_comments(AstNodes::Function(func)));
|
||||
}
|
||||
write!(f, type_parameters(func.type_params(), Node::Func(func)));
|
||||
write!(f, FormatCommentsBehindParenthesis(func));
|
||||
|
||||
group(&format_with(|f| {
|
||||
let format_parameters = FormatFormalParameters(func).memoized();
|
||||
let return_type = func.return_type().map(FormatTypeAnnotation);
|
||||
let format_return_type = return_type
|
||||
.as_ref()
|
||||
.map(FormatNodeWithoutTrailingComments)
|
||||
.memoized();
|
||||
let format_return_type = func.return_type().map(FormatTypeAnnotation).memoized();
|
||||
|
||||
// The parameters have to be formatted before the return type, which
|
||||
// `should_group_function_parameters` may do.
|
||||
|
||||
@@ -27,11 +27,16 @@ impl<'a> FormatDecorators<'a> {
|
||||
Self::new(member.decorators(), member.as_ast_nodes())
|
||||
}
|
||||
|
||||
/// `@a export class B {}`, as opposed to `export @a class B {}`.
|
||||
fn are_before_export(&self) -> bool {
|
||||
(self.decorators.first()).is_some_and(|first| first.span().end <= self.parent.span().start)
|
||||
}
|
||||
|
||||
/// Whether one of them is followed by a line break in the source.
|
||||
fn should_expand(&self, f: &Formatter<'a>) -> bool {
|
||||
self.decorators.iter().any(|it| {
|
||||
(self.decorators.iter()).any(|it| {
|
||||
f.source_text()
|
||||
.has_line_terminator_after(FormatDecorator(*it).span().end)
|
||||
.has_line_terminator_after(it.outer_span().end)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -70,6 +75,12 @@ impl<'a> Format<'a> for FormatDecorators<'a> {
|
||||
AstNodes::FormalParameter(_) | AstNodes::FormalParameterRest(_) => {
|
||||
write!(f, self.should_expand(f).then_some(expand_parent()));
|
||||
}
|
||||
// Prettier's `printDecoratorsBeforeExport`
|
||||
AstNodes::ExportNamedDeclaration(_) | AstNodes::ExportDefaultDeclaration(_)
|
||||
if self.are_before_export() =>
|
||||
{
|
||||
write!(f, expand_parent());
|
||||
}
|
||||
AstNodes::ExportNamedDeclaration(_) | AstNodes::ExportDefaultDeclaration(_) => {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
@@ -110,32 +121,27 @@ impl<'a> Format<'a> for FormatDecorator<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let expression = self.0;
|
||||
let node = AstNodes::Decorator(expression);
|
||||
format_node(
|
||||
self.span(),
|
||||
|| node.parent(),
|
||||
f,
|
||||
|f| {
|
||||
// `@a`, `@a.b`, `@a.b()` are written as they are. Anything else is in parentheses.
|
||||
let needs_parentheses = match expression.as_ast_nodes() {
|
||||
AstNodes::IdentifierReference(_) => false,
|
||||
AstNodes::CallExpression(call) => !call
|
||||
.callee()
|
||||
.is_some_and(is_identifier_or_static_member_only),
|
||||
AstNodes::StaticMemberExpression(member) => !member
|
||||
.object()
|
||||
.is_some_and(is_identifier_or_static_member_only),
|
||||
_ => true,
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
"@",
|
||||
needs_parentheses.then_some("("),
|
||||
expression,
|
||||
needs_parentheses.then_some(")")
|
||||
]
|
||||
);
|
||||
},
|
||||
);
|
||||
format_node(node, f, |f| {
|
||||
// `@a`, `@a.b`, `@a.b()` are written as they are. Anything else is in parentheses.
|
||||
let needs_parentheses = match expression.as_ast_nodes() {
|
||||
AstNodes::IdentifierReference(_) => false,
|
||||
AstNodes::CallExpression(call) => !call
|
||||
.callee()
|
||||
.is_some_and(is_identifier_or_static_member_only),
|
||||
AstNodes::StaticMemberExpression(member) => !member
|
||||
.object()
|
||||
.is_some_and(is_identifier_or_static_member_only),
|
||||
_ => true,
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
"@",
|
||||
needs_parentheses.then_some("("),
|
||||
expression,
|
||||
needs_parentheses.then_some(")")
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,45 @@
|
||||
use super::decorators::FormatDecorators;
|
||||
use super::import_declaration::{
|
||||
FormatCommentsInSpecifier, FormatSpecifiers, format_import_and_export_source_with_clause,
|
||||
module_export_name, only_specifier_has_comments,
|
||||
FormatSpecifiers, format_import_and_export_source_with_clause, module_export_name,
|
||||
};
|
||||
use super::semicolon::{OptionalSemicolon, start_of_comments_in_dropped_parentheses};
|
||||
use crate::cursor::{enter_node, extend_node};
|
||||
use crate::js::format::{FormatDeclaration, write_trailing_comments_of};
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use crate::js::ast_nodes::inner_of_stmt;
|
||||
use crate::js::format::{FormatDeclaration, format_ignorable_name, format_node, write_declaration};
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
|
||||
/// `export` or `export default`, and the decorators of the class after it. They can be before the
|
||||
/// `export` or after it, and stay where they are. `keyword` is empty if it is not written.
|
||||
fn format_export_keyword_with_class_decorators<'a>(
|
||||
node: AstNodes<'a>,
|
||||
keyword: &'static str,
|
||||
class: Option<Class<'a>>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let span = node.span();
|
||||
let keyword_and_space = format_with(|f| {
|
||||
if !keyword.is_empty() {
|
||||
write!(f, [keyword, space()]);
|
||||
}
|
||||
});
|
||||
let format_leading_comments = format_with(|f| {
|
||||
let comments = f.comments().comments_before(span.start);
|
||||
FormatLeadingComments::Comments(comments).fmt(f);
|
||||
});
|
||||
|
||||
if let Some(class) = class
|
||||
&& let Some(first_decorator) = class.decorators().next()
|
||||
{
|
||||
let decorators = FormatDecorators::new(class.decorators(), node);
|
||||
if first_decorator.span().end < span.start {
|
||||
enter_node(span, f);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
decorators,
|
||||
hard_line_break(),
|
||||
format_leading_comments,
|
||||
keyword_and_space
|
||||
]
|
||||
);
|
||||
} else if ignored_class_stays_behind_export(f)
|
||||
&& f.comments().is_suppressed(first_decorator.span().start)
|
||||
{
|
||||
// The class is written as it is, with its decorators.
|
||||
write!(f, format_leading_comments);
|
||||
enter_node(span, f);
|
||||
write!(f, keyword_and_space);
|
||||
} else {
|
||||
write!(f, format_leading_comments);
|
||||
enter_node(span, f);
|
||||
write!(
|
||||
f,
|
||||
[keyword, hard_line_break(), decorators, hard_line_break()]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
write!(f, format_leading_comments);
|
||||
enter_node(span, f);
|
||||
write!(f, keyword_and_space);
|
||||
}
|
||||
/// `export // comment⏎@a class B {}`: Prettier writes the comment behind `export `. For oxfmt a line starts after `export`
|
||||
/// as it always does before decorators.
|
||||
fn comments_of_decorated_class_start_a_line(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier writes a class that is not formatted behind `export `, with the comment in between. For
|
||||
/// oxfmt the decorators start a line as they always do after `export`.
|
||||
fn ignored_class_stays_behind_export(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
/// `export /* prettier-ignore */ const a = 1 ;` stays as it is for oxfmt. Prettier writes `const a = 1;`.
|
||||
fn ignored_variable_declaration_is_copied_with_its_semicolon(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `export /* prettier-ignore */ type A = 1 ;`: the `;` is part of a type alias, an `import a = b`, a function without a body
|
||||
/// and a module without a body, so it is copied with them, whatever the options say.
|
||||
///
|
||||
/// Both tools do that after `export`. Where the whole statement is ignored, oxfmt writes the `;` as the options say: see
|
||||
/// [`terminator_of_what_is_ignored_follows_semi`](crate::js::format::terminator_of_what_is_ignored_follows_semi).
|
||||
fn is_ignored_with_its_semicolon<'a>(statement: Stmt<'a>, f: &Formatter<'a>) -> bool {
|
||||
let ends_with_its_semicolon = match statement.kind() {
|
||||
StmtKind::TypeAlias(_) | StmtKind::ImportEquals(_) => true,
|
||||
StmtKind::Fn(func) => !func.has_body(),
|
||||
StmtKind::Module(module) => !module.innermost().has_body(),
|
||||
StmtKind::Var(_) => ignored_variable_declaration_is_copied_with_its_semicolon(f),
|
||||
_ => false,
|
||||
};
|
||||
ends_with_its_semicolon && !f.is_quiet() && f.comments().is_ignored(inner_of_stmt(statement))
|
||||
}
|
||||
|
||||
/// `export` or `export default` and the declaration `statement`.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_exported_declaration<'a>(statement: Stmt<'a>, f: &mut Formatter<'a>) {
|
||||
let node = statement.as_ast_nodes();
|
||||
let class = match statement.kind() {
|
||||
StmtKind::Class(class) => Some(class),
|
||||
_ => None,
|
||||
};
|
||||
let first_modifier = statement
|
||||
.modifiers()
|
||||
.iter()
|
||||
@@ -96,48 +58,106 @@ pub(crate) fn write_exported_declaration<'a>(statement: Stmt<'a>, f: &mut Format
|
||||
_ if statement.is_default_export() => "export default",
|
||||
_ => "export",
|
||||
};
|
||||
format_export_keyword_with_class_decorators(node, keyword, class, f);
|
||||
write!(f, FormatDeclaration(statement));
|
||||
if matches!(statement.kind(), StmtKind::Var(_)) {
|
||||
let keyword_and_space = format_with(|f| {
|
||||
if !keyword.is_empty() {
|
||||
write!(f, [keyword, space()]);
|
||||
}
|
||||
});
|
||||
|
||||
// The decorators of a class can be before the `export` or after it, and stay where they are.
|
||||
let decorated_class = match statement.kind() {
|
||||
StmtKind::Class(class) => Some(class).zip(class.decorators().next()),
|
||||
_ => None,
|
||||
};
|
||||
match decorated_class {
|
||||
None if is_ignored_with_its_semicolon(statement, f) => {
|
||||
let declaration = inner_of_stmt(statement);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
keyword_and_space,
|
||||
leading_comments(declaration),
|
||||
FormatSuppressedNode(statement.span_without_export()),
|
||||
trailing_comments(declaration)
|
||||
]
|
||||
);
|
||||
}
|
||||
None => write!(f, [keyword_and_space, FormatDeclaration(statement)]),
|
||||
// Prettier's `printDecoratorsBeforeExport`
|
||||
Some((class, first_decorator)) if first_decorator.span().end < node.span().start => {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatDecorators::new(class.decorators(), node),
|
||||
hard_line_break(),
|
||||
keyword_and_space
|
||||
]
|
||||
);
|
||||
// `@a export /* prettier-ignore */ class B {}`: the span of the class starts at its decorators, which are written
|
||||
// by now. Only oxfmt attaches the comment to the class.
|
||||
let class_node = AstNodes::Class(class);
|
||||
match !f.is_quiet() && f.comments().is_ignored(class_node) {
|
||||
true => write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(class_node),
|
||||
FormatSuppressedNode(statement.span_without_export()),
|
||||
trailing_comments(class_node)
|
||||
]
|
||||
),
|
||||
false => write!(f, FormatDeclaration(statement)),
|
||||
}
|
||||
}
|
||||
// They are part of the class, so its leading comments are written before them.
|
||||
Some((class, _)) => {
|
||||
let starts_a_line = comments_of_decorated_class_start_a_line(f);
|
||||
match starts_a_line {
|
||||
true => write!(f, [keyword, hard_line_break()]),
|
||||
false => write!(f, keyword_and_space),
|
||||
}
|
||||
// Prettier's `printDecorators` starts with a line break, also behind the one that ends a comment.
|
||||
let class_node = AstNodes::Class(class);
|
||||
let follows_line_break = !f.is_quiet()
|
||||
&& !starts_a_line
|
||||
&& (f.comments().of_node(class_node).leading.last())
|
||||
.is_some_and(|last| last.is_line() || last.has_newline_after());
|
||||
format_node(class_node, f, |f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
follows_line_break.then_some(empty_line()),
|
||||
FormatDecorators::new(
|
||||
class.decorators(),
|
||||
match starts_a_line {
|
||||
true => class_node,
|
||||
false => node,
|
||||
}
|
||||
),
|
||||
hard_line_break()
|
||||
]
|
||||
);
|
||||
write_declaration(statement, f);
|
||||
});
|
||||
}
|
||||
}
|
||||
// An ignored declaration is written with its `;`.
|
||||
if matches!(statement.kind(), StmtKind::Var(_))
|
||||
&& !f
|
||||
.comments()
|
||||
.is_ignored(AstNodes::VariableDeclaration(statement))
|
||||
{
|
||||
write!(f, OptionalSemicolon);
|
||||
}
|
||||
extend_node(node.span(), f);
|
||||
write_trailing_comments_of(node, f);
|
||||
}
|
||||
|
||||
/// `export default a /* comment */;` is `export default a; /* comment */` for oxfmt, as after any other
|
||||
/// statement. Prettier leaves it.
|
||||
fn comments_before_semicolon_of_default_export_go_behind_it(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `export default e`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_export_default_expression<'a>(
|
||||
statement: Stmt<'a>,
|
||||
_statement: Stmt<'a>,
|
||||
expression: Expr<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let node = AstNodes::ExportDefaultDeclaration(statement);
|
||||
format_export_keyword_with_class_decorators(node, "export default", None, f);
|
||||
match comments_before_semicolon_of_default_export_go_behind_it(f) && !f.is_quiet() {
|
||||
true => {
|
||||
let hidden_from = start_of_comments_in_dropped_parentheses(expression, Some(false), f)
|
||||
.unwrap_or_else(|| expression.outer_span().end);
|
||||
let previous = f.comments_mut().hide_comments_from(hidden_from);
|
||||
write!(f, [expression, OptionalSemicolon]);
|
||||
f.comments_mut().restore_hidden_comments(previous);
|
||||
// One on a line of its own leads the next statement.
|
||||
let comments = f.comments().comments_before(statement.span().end);
|
||||
let count = (comments.iter())
|
||||
.take_while(|comment| !comment.preceded_by_newline())
|
||||
.count();
|
||||
let comments = comments.get(..count).unwrap_or_default();
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
false => write!(f, [expression, OptionalSemicolon]),
|
||||
}
|
||||
extend_node(node.span(), f);
|
||||
write_trailing_comments_of(node, f);
|
||||
write!(f, ["export default ", expression, OptionalSemicolon]);
|
||||
}
|
||||
|
||||
/// Flow's `declare export { a }`. Not `export export { a }`, of which TypeScript's parser makes `export { a }`.
|
||||
@@ -146,6 +166,7 @@ fn is_declared(statement: Stmt<'_>) -> bool {
|
||||
}
|
||||
|
||||
/// `export * from "a"`, `export * as a from "a"`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_export_all_declaration<'a>(statement: Stmt<'a>, f: &mut Formatter<'a>) {
|
||||
let StmtKind::ExportStar {
|
||||
alias, type_only, ..
|
||||
@@ -167,132 +188,85 @@ pub(crate) fn write_export_all_declaration<'a>(statement: Stmt<'a>, f: &mut Form
|
||||
format_import_and_export_source_with_clause(statement, f);
|
||||
return write!(f, OptionalSemicolon);
|
||||
};
|
||||
let format_name = module_export_name(name, AstNodes::ExportAllDeclaration(statement));
|
||||
if f.is_quiet() {
|
||||
write!(f, ["*", space(), "as", space(), format_name, space()]);
|
||||
} else {
|
||||
// To Babel `* as a` is a specifier, which the comments before it lead. To TypeScript there
|
||||
// is only the name.
|
||||
if f.file().is_javascript() {
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_before_character(statement.span().start, b'*');
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
write!(f, ["*", space(), "as", space()]);
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(name.span().end);
|
||||
write!(f, format_name);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
let source_start = statement
|
||||
.module_specifier_span()
|
||||
.map_or(0, |source| source.start);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
format_trailing_comments(statement.span(), name.span(), source_start),
|
||||
space()
|
||||
]
|
||||
);
|
||||
}
|
||||
write!(f, ["from", space()]);
|
||||
// For Babel `* as a` is an `ExportNamespaceSpecifier`, which can own comments.
|
||||
let star = statement.export_star_offset().unwrap_or_default();
|
||||
format_ignorable_name(Span::new(star, name.span().end), f, |f| {
|
||||
write!(f, ["*", space(), "as", space(), module_export_name(name)]);
|
||||
});
|
||||
write!(f, [space(), "from", space()]);
|
||||
format_import_and_export_source_with_clause(statement, f);
|
||||
write!(f, OptionalSemicolon);
|
||||
}
|
||||
|
||||
/// `export { a, b as c }`, `export { a } from "a"`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_export_named_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
export: Export<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let span = statement.span();
|
||||
let specifiers = export.items();
|
||||
let export_kind = export.is_type_only().then_some("type ");
|
||||
|
||||
format_leading_comments(span).fmt(f);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
is_declared(statement).then_some("declare "),
|
||||
"export",
|
||||
space()
|
||||
]
|
||||
);
|
||||
|
||||
let needs_space = f.options().bracket_spacing.value();
|
||||
let Some(first) = specifiers.first() else {
|
||||
// The comments in an export of nothing go after the keyword.
|
||||
if !export.has_from() && !f.is_quiet() {
|
||||
let comments = f.comments().comments_before_character(span.start, b'}');
|
||||
if let Some(last) = comments.last() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatDanglingComments::Comments {
|
||||
comments,
|
||||
indent: DanglingIndentMode::None
|
||||
},
|
||||
last.is_line().then_some(hard_line_break()),
|
||||
" "
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
write!(f, [export_kind, "{}"]);
|
||||
return write_export_source(statement, export, f);
|
||||
};
|
||||
if specifiers.len() == 1 && !only_specifier_has_comments(statement, first.span(), f) {
|
||||
write!(f, [is_declared(statement).then_some("declare "), "export"]);
|
||||
// `export { /* comment */ };`
|
||||
let node = AstNodes::ExportNamedDeclaration(statement);
|
||||
if !f.is_quiet()
|
||||
&& let Some(last) = f.comments().of_node(node).dangling.last()
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
export_kind,
|
||||
"{",
|
||||
maybe_space(needs_space),
|
||||
first,
|
||||
maybe_space(needs_space),
|
||||
"}"
|
||||
]
|
||||
);
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
export_kind,
|
||||
"{",
|
||||
FormatSpecifiers(statement, specifiers),
|
||||
"}"
|
||||
space(),
|
||||
dangling_comments(node),
|
||||
last.is_line().then_some(hard_line_break())
|
||||
]
|
||||
);
|
||||
}
|
||||
write_export_source(statement, export, f);
|
||||
}
|
||||
write!(f, [" ", export.is_type_only().then_some("type ")]);
|
||||
|
||||
/// What is after the `}` of `export {}`.
|
||||
fn write_export_source<'a>(statement: Stmt<'a>, export: Export<'a>, f: &mut Formatter<'a>) {
|
||||
let specifiers = export.items();
|
||||
let needs_space = f.options().bracket_spacing.value();
|
||||
match specifiers.first() {
|
||||
None => write!(f, "{}"),
|
||||
Some(only)
|
||||
if specifiers.len() == 1
|
||||
&& (f.is_quiet()
|
||||
|| !f.comments().has_comments(AstNodes::ExportSpecifier(only))) =>
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
"{",
|
||||
maybe_space(needs_space),
|
||||
only,
|
||||
maybe_space(needs_space),
|
||||
"}"
|
||||
]
|
||||
);
|
||||
}
|
||||
Some(_) => write!(
|
||||
f,
|
||||
[
|
||||
"{",
|
||||
FormatSpecifiers(specifiers, AstNodes::ExportSpecifier),
|
||||
"}"
|
||||
]
|
||||
),
|
||||
}
|
||||
if export.has_from() {
|
||||
write!(f, [space(), "from", space()]);
|
||||
format_import_and_export_source_with_clause(statement, f);
|
||||
}
|
||||
write!(f, OptionalSemicolon);
|
||||
write_trailing_comments_of(AstNodes::ExportNamedDeclaration(statement), f);
|
||||
}
|
||||
|
||||
/// `a`, `a as b`, `type a`
|
||||
pub(crate) fn write_export_specifier<'a>(specifier: ExportSpec<'a>, f: &mut Formatter<'a>) {
|
||||
let node = AstNodes::ExportSpecifier(specifier);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatCommentsInSpecifier(specifier.span()),
|
||||
specifier.is_type_only().then_some("type ")
|
||||
]
|
||||
);
|
||||
let exported = module_export_name(specifier.exported(), node);
|
||||
write!(f, specifier.is_type_only().then_some("type "));
|
||||
let exported = module_export_name(specifier.exported());
|
||||
match specifier.is_renamed() {
|
||||
true => write!(
|
||||
f,
|
||||
[
|
||||
module_export_name(specifier.local(), node),
|
||||
module_export_name(specifier.local()),
|
||||
space(),
|
||||
"as",
|
||||
space(),
|
||||
|
||||
@@ -6,13 +6,13 @@ use super::function::FormatFunctionBody;
|
||||
use super::object_like::ObjectLike;
|
||||
use super::object_pattern_like::ObjectPatternLike;
|
||||
use super::return_or_throw_statement::has_argument_leading_comments;
|
||||
use crate::js::format::{FormatExpr, format_node};
|
||||
use crate::js::format::{FormatExpr, format_name, format_node};
|
||||
use crate::js::parentheses::expression::left_edge_end;
|
||||
use crate::js::utils::array::write_array_node;
|
||||
use crate::js::utils::assignment_like::{AssignmentLike, comments_stay_around_operator};
|
||||
use crate::js::utils::assignment_like::AssignmentLike;
|
||||
use crate::js::utils::conditional::ConditionalLike;
|
||||
use crate::js::utils::object::{format_computed_or_property_key, should_preserve_quote};
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::js::utils::typecast::has_comments_that_count;
|
||||
use crate::prelude::*;
|
||||
use crate::{best_fitting, format_args, write};
|
||||
|
||||
@@ -129,12 +129,7 @@ impl<'a> Format<'a> for FormatObjectMember<'a> {
|
||||
};
|
||||
match f.is_quiet() {
|
||||
true => write(f),
|
||||
false => format_node(
|
||||
property.span(),
|
||||
|| property.as_ast_nodes().parent(),
|
||||
f,
|
||||
write,
|
||||
),
|
||||
false => format_node(property.as_ast_nodes(), f, write),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,32 +141,7 @@ impl Spanned for FormatObjectMember<'_> {
|
||||
}
|
||||
|
||||
fn write_object_property<'a>(property: Prop<'a>, f: &mut Formatter<'a>) {
|
||||
if !f.is_quiet()
|
||||
&& f.comments()
|
||||
.has_trailing_suppression_comment(property.span().end)
|
||||
{
|
||||
return write!(f, FormatSuppressedNode(property.span()));
|
||||
}
|
||||
let Some(value) = property.func() else {
|
||||
// Prettier's `handlePropertyComments`: a comment at the end of the line of the key leads
|
||||
// the property.
|
||||
if !f.is_quiet()
|
||||
&& !comments_stay_around_operator(f)
|
||||
&& property.kind() != PropKind::Shorthand
|
||||
&& let (Some(key), Some(value)) = (property.key(), property.value())
|
||||
&& !f.comments().has_comment_in_span(key.span(f.file()))
|
||||
{
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_leading_property(key.span(f.file()).end, value.span().start);
|
||||
if comments
|
||||
.iter()
|
||||
.any(|comment| f.comments().is_suppression_comment(comment))
|
||||
{
|
||||
return write!(f, FormatSuppressedNode(property.span()));
|
||||
}
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
return write!(f, AssignmentLike::ObjectProperty(property));
|
||||
};
|
||||
match property.kind() {
|
||||
@@ -191,40 +161,49 @@ fn write_object_property<'a>(property: Prop<'a>, f: &mut Formatter<'a>) {
|
||||
}
|
||||
// An error.
|
||||
write!(f, property.is_optional().then_some("?"));
|
||||
// `{ a(/* prettier-ignore */) {} }`: with typescript-estree the comment dangles from the `FunctionExpression`.
|
||||
let function = AstNodes::Function(value);
|
||||
if !f.is_quiet() && f.comments().is_ignored(function) {
|
||||
return format_node(function, f, |_| {});
|
||||
}
|
||||
format_grouped_parameters_with_return_type_for_method(value, f);
|
||||
if value.has_body() {
|
||||
write!(f, [space(), FormatFunctionBody(value)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// `import.meta`, `new.target`
|
||||
/// `import.meta`, `new.target`. Both words are `Identifier` nodes for Prettier, so comments can be attached to them.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_meta_property<'a>(e: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
match e.kind() {
|
||||
ExprKind::ImportMeta => write!(f, "import.meta"),
|
||||
_ => {
|
||||
let text = e.text();
|
||||
// With its escapes: `new.t\u0061rget`, `new.t\u{61}rget`.
|
||||
let is_in_name = |byte: &u8| {
|
||||
byte.is_ascii_alphanumeric()
|
||||
|| matches!(byte, b'_' | b'$' | b'\\' | b'{' | b'}' | 0x80..)
|
||||
};
|
||||
let name = text
|
||||
.iter()
|
||||
.rposition(|byte| !is_in_name(byte))
|
||||
.map_or(0, |at| at + 1);
|
||||
match text.get(name..) {
|
||||
Some(b"target") | None => write!(f, "new.target"),
|
||||
// An error: `new.targ`.
|
||||
Some(_) => write!(
|
||||
f,
|
||||
[
|
||||
"new.",
|
||||
source_text(Span::new(e.span().start + name as u32, e.span().end))
|
||||
]
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
let span = e.span();
|
||||
let is_import_meta = matches!(e.kind(), ExprKind::ImportMeta);
|
||||
let meta = if is_import_meta { "import" } else { "new" };
|
||||
let property = Span::new(start_of_meta_property_name(e), span.end);
|
||||
format_name(
|
||||
Span::new(span.start, span.start + meta.len() as u32),
|
||||
f,
|
||||
|f| {
|
||||
write!(f, meta);
|
||||
},
|
||||
);
|
||||
write!(f, ".");
|
||||
format_name(property, f, |f| match is_import_meta {
|
||||
// `import.m\u0065ta`
|
||||
true => write!(f, "meta"),
|
||||
// With its escapes. `new.targ` is an error.
|
||||
false => write!(f, source_text(property)),
|
||||
});
|
||||
}
|
||||
|
||||
/// Where the second word of `import.meta` or `new.target` starts. It can have escapes: `new.t\u0061rget`, `new.t\u{61}rget`.
|
||||
pub(crate) fn start_of_meta_property_name(e: Expr<'_>) -> u32 {
|
||||
let is_in_name = |byte: &u8| {
|
||||
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$' | b'\\' | b'{' | b'}' | 0x80..)
|
||||
};
|
||||
let name = (e.text().iter())
|
||||
.rposition(|byte| !is_in_name(byte))
|
||||
.map_or(0, |at| at + 1);
|
||||
e.span().start + name as u32
|
||||
}
|
||||
|
||||
/// `++a`, `a--`
|
||||
@@ -236,14 +215,9 @@ pub(crate) fn write_update_expression<'a>(op: UnOp, operand: Expr<'a>, f: &mut F
|
||||
}
|
||||
|
||||
/// `!a`, `typeof a`
|
||||
pub(crate) fn write_unary_expression<'a>(
|
||||
e: Expr<'a>,
|
||||
op: UnOp,
|
||||
operand: Expr<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
pub(crate) fn write_unary_expression<'a>(op: UnOp, operand: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
write!(f, [op.as_str(), op.is_keyword().then_some(space())]);
|
||||
if !f.is_quiet() && unary_argument_has_comments(e, operand, f) {
|
||||
if !f.is_quiet() && has_comments_that_count(operand, f) {
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!("(", soft_block_indent(&operand), ")"))
|
||||
@@ -253,78 +227,6 @@ pub(crate) fn write_unary_expression<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a comment leads or trails `argument`, which is the operand of `unary`. The answer does
|
||||
/// not depend on which comments are printed already.
|
||||
pub(crate) fn unary_argument_has_comments<'a>(
|
||||
unary: Expr<'a>,
|
||||
argument: Expr<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
// Prettier attaches no comment to a `ChainExpression`, so what is in it has them.
|
||||
if !f.options().flavor.is_oxfmt()
|
||||
&& !f.context().has_tree_of_babel()
|
||||
&& argument.is_chain_root()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let (outer, inner) = (unary.span(), argument.span());
|
||||
let is_leading =
|
||||
|comment: &Comment| comment.start() >= outer.start && comment.end() <= inner.start;
|
||||
let comments = f.comments();
|
||||
if comments.printed_comments().last().is_some_and(is_leading) {
|
||||
return true;
|
||||
}
|
||||
let unprinted = comments.unprinted_comments();
|
||||
match unprinted.first() {
|
||||
Some(first) if first.start() < outer.end => {
|
||||
if is_leading(first) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
let after = unprinted.partition_point(|comment| comment.start() < inner.end);
|
||||
unprinted
|
||||
.get(after..)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.take_while(|comment| comment.end() <= outer.end)
|
||||
.any(|comment| !is_last_binary_operand_comment(argument, comment, f))
|
||||
}
|
||||
|
||||
/// Prettier's `handleLastBinaryOperatorOperand`: whether `comment`, which is after `argument`, the
|
||||
/// operand of a unary expression, trails the last operand of `argument` and not all of it:
|
||||
///
|
||||
/// ```javascript
|
||||
/// !(
|
||||
/// a || // 1
|
||||
/// b // 2
|
||||
/// );
|
||||
/// ```
|
||||
pub(crate) fn is_last_binary_operand_comment<'a>(
|
||||
argument: Expr<'a>,
|
||||
comment: &Comment,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
let ExprKind::Binary { op, right, .. } = argument.kind() else {
|
||||
return false;
|
||||
};
|
||||
let source_text = f.source_text();
|
||||
op != BinOp::Comma
|
||||
&& comment_can_trail_last_operand(f)
|
||||
&& !comment.preceded_by_newline()
|
||||
&& comment.followed_by_newline()
|
||||
&& !comment.is_multiline_block()
|
||||
&& source_text.contains_newline(Span::before(argument.span().start, right.span()))
|
||||
&& !source_text.contains_newline(Span::before(right.span().start, comment.span))
|
||||
}
|
||||
|
||||
/// For oxfmt a comment before the `)` trails all that is in the parentheses here as everywhere else, and
|
||||
/// breaks no chain of operators.
|
||||
fn comment_can_trail_last_operand(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `a = b`, `a += b`. In the target of a destructuring assignment, a target with its default value.
|
||||
pub(crate) fn write_assignment_expression<'a>(e: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
match (e.as_chain_element(), e.kind()) {
|
||||
@@ -350,7 +252,7 @@ pub(crate) fn write_array_assignment_target<'a>(
|
||||
if elements.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
format_dangling_comments(e.span()).with_soft_block_indent()
|
||||
dangling_comments(e.as_ast_nodes()).with_soft_block_indent()
|
||||
);
|
||||
} else {
|
||||
let rest = elements
|
||||
@@ -387,6 +289,7 @@ pub(crate) fn write_object_assignment_target<'a>(
|
||||
}
|
||||
|
||||
/// `await a`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_await_expression<'a>(e: Expr<'a>, argument: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let format_inner = format_args!("await", space(), argument);
|
||||
let is_callee_or_object = match e.ast_parent() {
|
||||
@@ -410,6 +313,7 @@ pub(crate) fn write_await_expression<'a>(e: Expr<'a>, argument: Expr<'a>, f: &mu
|
||||
}
|
||||
|
||||
/// `yield a`, `yield* a`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_yield_expression<'a>(
|
||||
argument: Option<Expr<'a>>,
|
||||
delegate: bool,
|
||||
@@ -447,6 +351,7 @@ fn yield_writes_its_argument_as_return_does(f: &Formatter<'_>) -> bool {
|
||||
}
|
||||
|
||||
/// `<T>a`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_type_assertion<'a>(e: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let Some(expression) = e.expression() else {
|
||||
return;
|
||||
|
||||
+305
-243
@@ -4,25 +4,24 @@
|
||||
//! The tree has the nodes of TypeScript (`flow.rs` of `bun_sema_parser` has the table), so most of a
|
||||
//! file of Flow is written by the functions for those. They call these where `File::is_flow`.
|
||||
|
||||
use super::class::FormatClassImplements;
|
||||
use super::class::dangling_comments_with_marker;
|
||||
use super::function::FormatFunctionBody;
|
||||
use super::function_type::{write_ts_call_signature_declaration, write_ts_method_signature};
|
||||
use super::import_declaration::FormatStringLiteral;
|
||||
use super::object_like::ObjectLike;
|
||||
use super::parameters::FormatFormalParameters;
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use super::ts_types::{
|
||||
comments_after_property_name, write_ts_interface_signatures, write_ts_signatures,
|
||||
};
|
||||
use super::ts_types::{write_ts_interface_signatures, write_ts_signatures};
|
||||
use super::type_parameters::{type_arguments, type_parameters};
|
||||
use crate::js::attach::Marker;
|
||||
use crate::js::format::{
|
||||
ExprOptions, FormatTypeAnnotation, format_node, identifier, write_expression,
|
||||
ExprOptions, FormatTypeAnnotation, format_name, format_node, identifier, write_expression,
|
||||
};
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::object::{format_computed_or_property_key, key_requires_quotes};
|
||||
use crate::js::utils::typescript::is_simple_type;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
use bun_lint::tokens::skip_trivia;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
// ───────────────────────────── modifiers ─────────────────────────────
|
||||
@@ -34,18 +33,22 @@ impl<'a> Format<'a> for FormatModifiers<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
for modifier in self.0.iter() {
|
||||
let span = modifier.token_span();
|
||||
write!(f, [source_text(span), (span.len() > 1).then_some(space())]);
|
||||
format_name(span, f, |f| write!(f, source_text(span)));
|
||||
write!(f, (span.len() > 1).then_some(space()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `+`, `-`, `in`, `out`: Flow's `Variance`, which is a node for Prettier.
|
||||
pub(crate) fn is_variance(modifier: Modifier<'_>) -> bool {
|
||||
modifier.flag().intersects(Flags::IN | Flags::OUT)
|
||||
}
|
||||
|
||||
/// The `+` or `-` of a property of a class.
|
||||
pub(crate) fn write_variance_sign<'a>(modifiers: List<'a, Modifier<'a>>, f: &mut Formatter<'a>) {
|
||||
for modifier in modifiers
|
||||
.iter()
|
||||
.filter(|it| it.flag().intersects(Flags::IN | Flags::OUT))
|
||||
{
|
||||
write!(f, source_text(modifier.token_span()));
|
||||
for modifier in modifiers.iter().filter(|it| is_variance(*it)) {
|
||||
let span = modifier.token_span();
|
||||
format_name(span, f, |f| write!(f, source_text(span)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +96,7 @@ pub(crate) fn is_no_source_element(node: AstNodes<'_>) -> bool {
|
||||
// ───────────────────────────── expressions ─────────────────────────────
|
||||
|
||||
/// `(e: T)`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_type_cast_expression<'a>(e: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
if let ExprKind::As { expr, ty } = e.kind() {
|
||||
write!(f, ["(", expr, FormatTypeAnnotation(ty), ")"]);
|
||||
@@ -107,7 +111,10 @@ pub(crate) fn is_interface_type(ty: TypeNode<'_>) -> bool {
|
||||
}
|
||||
|
||||
/// `hook` or `component`, if the function type `ty` starts with the word.
|
||||
fn keyword_of_function_type<'a>(ty: TypeNode<'a>, func: Func<'a>) -> Option<&'static str> {
|
||||
pub(crate) fn keyword_of_function_type<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
func: Func<'a>,
|
||||
) -> Option<&'static str> {
|
||||
// In `hook => void` it is the type of the parameter.
|
||||
if func.open_paren() == Some(ty.span().start) {
|
||||
return None;
|
||||
@@ -135,12 +142,23 @@ fn is_optional_indexed_access(mut ty: TypeNode<'_>) -> bool {
|
||||
|
||||
/// A parameter of a function type, of a signature or of a function that is declared: Flow's
|
||||
/// `FunctionTypeParam`, as opposed to a pattern with a type annotation.
|
||||
fn is_function_type_param(param: Param<'_>) -> bool {
|
||||
pub(crate) fn is_function_type_param(param: Param<'_>) -> bool {
|
||||
param
|
||||
.func()
|
||||
.is_some_and(|func| !func.has_body() && !func.is_arrow())
|
||||
}
|
||||
|
||||
/// Whether `ty` is the return type or the type of a parameter of a function type, of a signature or of a function that is
|
||||
/// declared. In Flow's tree it is directly in the function type or the `FunctionTypeParam`: no `TypeAnnotation` starts at the
|
||||
/// `=>` or the `:`.
|
||||
pub(crate) fn is_directly_in_function_type(ty: TypeNode<'_>) -> bool {
|
||||
match ty.parent() {
|
||||
Node::Func(func) => !func.has_body() && !func.is_arrow(),
|
||||
Node::Param(param) => is_function_type_param(param),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `includesFunctionTypeInObjectType`. `is_in_object_type`: `node` is.
|
||||
fn includes_function_type_in_object_type(node: Node<'_>, is_in_object_type: bool) -> bool {
|
||||
// What is in it and has not been looked at.
|
||||
@@ -340,8 +358,20 @@ pub(crate) fn write_indexed_access_type<'a>(
|
||||
/// `: T %checks` as a return type. Returns whether the predicate `ty` is that.
|
||||
pub(crate) fn write_checks_predicate<'a>(ty: TypeNode<'a>, f: &mut Formatter<'a>) -> bool {
|
||||
match ty.kind() {
|
||||
TypeKind::Predicate { param, ty, .. } if param.is("%checks") => {
|
||||
write!(f, [ty, ty.map(|_| space()), "%checks"]);
|
||||
TypeKind::Predicate {
|
||||
param, ty: checked, ..
|
||||
} if param.is("%checks") => {
|
||||
// `% /* comment */ checks`, which Prettier does not parse.
|
||||
let comments = f.comments().of_node(ty.as_ast_nodes()).dangling;
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
checked,
|
||||
checked.map(|_| space()),
|
||||
"%checks",
|
||||
FormatTrailingComments::Comments(comments)
|
||||
]
|
||||
);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@@ -356,24 +386,29 @@ pub(crate) fn predicate_prefix(ty: TypeNode<'_>) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `T` and the `e` of the return type `T %checks(e)`, which is a type query with `T` as its type argument in the tree.
|
||||
pub(crate) fn as_declared_predicate(ty: TypeNode<'_>) -> Option<(Option<TypeNode<'_>>, Expr<'_>)> {
|
||||
match ty.kind() {
|
||||
TypeKind::Typeof { expr, args, .. } if ty.first_token() != b"typeof" => {
|
||||
Some((args.first(), expr))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `: T %checks(e)` as a return type. Returns whether the type query `ty` is that. Of a function
|
||||
/// that is only declared, the `T` is written: the rest is not part of the signature.
|
||||
pub(crate) fn write_declared_predicate<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
expr: Expr<'a>,
|
||||
args: List<'a, TypeNode<'a>>,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
if ty.first_token() == b"typeof" {
|
||||
pub(crate) fn write_declared_predicate<'a>(ty: TypeNode<'a>, f: &mut Formatter<'a>) -> bool {
|
||||
let Some((return_type, expr)) = as_declared_predicate(ty) else {
|
||||
return false;
|
||||
}
|
||||
write!(f, args.first());
|
||||
};
|
||||
write!(f, return_type);
|
||||
let is_of_declared_function = matches!(
|
||||
ty.parent(),
|
||||
Node::Func(func) if !func.has_body() && matches!(func.owner(), Node::Stmt(_))
|
||||
);
|
||||
if !is_of_declared_function {
|
||||
write!(f, [args.first().map(|_| space()), "%checks(", expr, ")"]);
|
||||
write!(f, [return_type.map(|_| space()), "%checks(", expr, ")"]);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -414,13 +449,6 @@ pub(crate) fn write_function_type_keyword<'a>(
|
||||
) -> bool {
|
||||
match keyword_of_function_type(ty, func) {
|
||||
Some("hook") => {
|
||||
// Those between the word and the `(` lead the type.
|
||||
if !f.is_quiet()
|
||||
&& let Some(open) = func.open_paren()
|
||||
{
|
||||
let comments = f.comments().comments_before(open);
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
write!(f, "hook ");
|
||||
false
|
||||
}
|
||||
@@ -472,16 +500,10 @@ pub(crate) fn write_parameter_start<'a>(param: Param<'a>, f: &mut Formatter<'a>)
|
||||
false => None,
|
||||
};
|
||||
if let Some(span) = outer_name {
|
||||
match f.source_text().text_for(&span).first() {
|
||||
Some(b'"' | b'\'') => write!(
|
||||
f,
|
||||
FormatStringLiteral {
|
||||
span,
|
||||
parent: param.as_ast_nodes()
|
||||
}
|
||||
),
|
||||
format_name(span, f, |f| match f.source_text().text_for(&span).first() {
|
||||
Some(b'"' | b'\'') => write!(f, FormatStringLiteral { span }),
|
||||
_ => write!(f, source_text(span)),
|
||||
}
|
||||
});
|
||||
match is_unnamed {
|
||||
true => write!(f, [param.is_optional().then_some("?"), ": "]),
|
||||
false => write!(f, " as "),
|
||||
@@ -605,13 +627,14 @@ pub(crate) fn write_tuple_element<'a>(element: TupleElem<'a>, f: &mut Formatter<
|
||||
write!(f, [source_text(span), (span.len() > 1).then_some(space())]);
|
||||
}
|
||||
if let Some(label) = label {
|
||||
let label = identifier(label, AstNodes::TSNamedTupleMember(element));
|
||||
let label = identifier(label);
|
||||
write!(f, [label, element.is_optional().then_some("?"), ": "]);
|
||||
}
|
||||
write!(f, element.flow_type());
|
||||
}
|
||||
|
||||
/// `[...]`: the comments in it dangle, and are written behind the dots. Returns whether `ty` is that.
|
||||
/// `[...]`. The comments dangle in the tuple type, and Prettier's `printArray` writes them after the dots. Returns whether
|
||||
/// `ty` is that.
|
||||
pub(crate) fn write_tuple_of_unknown_elements<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
elements: List<'a, TupleElem<'a>>,
|
||||
@@ -620,12 +643,19 @@ pub(crate) fn write_tuple_of_unknown_elements<'a>(
|
||||
if elements.len() != 1 || !is_inexact_tuple(elements) {
|
||||
return false;
|
||||
}
|
||||
let comments = f.comments().comments_before(ty.span().end);
|
||||
let has_line_comment = comments.iter().any(|it| it.is_line());
|
||||
let content = format_args!("...", format_dangling_comments(ty.span()));
|
||||
let comments = f.comments().of_node(ty.as_ast_nodes()).dangling;
|
||||
let format_comments = FormatDanglingComments::Comments {
|
||||
comments,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!("[", soft_block_indent(&content), "]")).should_expand(has_line_comment)
|
||||
group(&format_args!(
|
||||
"[",
|
||||
soft_block_indent(&format_args!("...", format_comments)),
|
||||
"]"
|
||||
))
|
||||
.should_expand(comments.iter().any(|comment| comment.is_line()))
|
||||
);
|
||||
true
|
||||
}
|
||||
@@ -644,6 +674,7 @@ pub(crate) fn is_inexact_mark(member: Member<'_>) -> bool {
|
||||
|
||||
/// `{ a: A }`, `{| a: A |}`, `{ a: A, ... }`. Prettier's `printClassBody` for an object type that is
|
||||
/// not the body of an interface or a class.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_object_type<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
members: List<'a, Member<'a>>,
|
||||
@@ -658,15 +689,16 @@ pub(crate) fn write_object_type<'a>(
|
||||
match members.first() {
|
||||
None => write!(
|
||||
f,
|
||||
format_dangling_comments(ty.span()).with_soft_block_indent()
|
||||
dangling_comments(ty.as_ast_nodes()).with_soft_block_indent()
|
||||
),
|
||||
// Prettier has no node for the `...`: the comments dangle.
|
||||
// `{ /* comment */ ... }`
|
||||
Some(first)
|
||||
if is_inexact_mark(first) && f.comments().has_comment_before(first.span().start) =>
|
||||
if is_inexact_mark(first)
|
||||
&& !(f.comments().of_node(ty.as_ast_nodes()).dangling).is_empty() =>
|
||||
{
|
||||
let comments = f.comments().comments_before(first.span().start);
|
||||
let has_line_comment = comments.iter().any(|it| it.is_line());
|
||||
let ends_line = comments.last().is_some_and(|it| it.followed_by_newline());
|
||||
let comments = f.comments().of_node(ty.as_ast_nodes()).dangling;
|
||||
let has_line_comment = comments.iter().any(|comment| comment.is_line());
|
||||
let ends_line = comments.last().is_some_and(|it| it.has_newline_after());
|
||||
let content = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
@@ -723,13 +755,17 @@ pub(crate) fn write_object_type_member<'a>(member: Member<'a>, f: &mut Formatter
|
||||
]
|
||||
);
|
||||
}
|
||||
(MemberKind::CallSignature, Some(func)) => write_ts_call_signature_declaration(func, f),
|
||||
(MemberKind::CallSignature, Some(func)) => {
|
||||
// `static /* comment */ (): void`
|
||||
write!(f, leading_comments(AstNodes::FormalParameters(func)));
|
||||
write_ts_call_signature_declaration(func, f);
|
||||
}
|
||||
(_, Some(func)) => match (
|
||||
member.flow_internal_slot(),
|
||||
member.key().map(|it| it.kind()),
|
||||
) {
|
||||
(Some(slot), _) => {
|
||||
write!(f, ["[[", identifier(slot, node), "]]"]);
|
||||
write!(f, ["[[", identifier(slot), "]]"]);
|
||||
write_ts_call_signature_declaration(func, f);
|
||||
}
|
||||
(None, Some(KeyKind::Ident(name))) if name.bytes().starts_with(b"@@") => {
|
||||
@@ -743,7 +779,7 @@ pub(crate) fn write_object_type_member<'a>(member: Member<'a>, f: &mut Formatter
|
||||
return write_mapped_type_property(mapped, f);
|
||||
}
|
||||
match (member.flow_internal_slot(), member.key()) {
|
||||
(Some(slot), _) => write!(f, ["[[", identifier(slot, node), "]]"]),
|
||||
(Some(slot), _) => write!(f, ["[[", identifier(slot), "]]"]),
|
||||
(None, Some(key)) => match key.kind() {
|
||||
KeyKind::Ident(name) if name.bytes().starts_with(b"@@") => {
|
||||
write!(f, text(name.bytes()));
|
||||
@@ -752,14 +788,6 @@ pub(crate) fn write_object_type_member<'a>(member: Member<'a>, f: &mut Formatter
|
||||
},
|
||||
(None, None) => {}
|
||||
}
|
||||
if !f.is_quiet()
|
||||
&& let Some(ty) = member.ty()
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments_after_property_name(ty, f))
|
||||
);
|
||||
}
|
||||
write!(f, [optional, ": ", member.ty()]);
|
||||
}
|
||||
}
|
||||
@@ -768,7 +796,7 @@ pub(crate) fn write_object_type_member<'a>(member: Member<'a>, f: &mut Formatter
|
||||
/// `[K in T]?: V`. Prettier's `printFlowMappedTypeProperty`.
|
||||
fn write_mapped_type_property<'a>(mapped: Mapped<'a>, f: &mut Formatter<'a>) {
|
||||
let param = mapped.param();
|
||||
let name = identifier(param.name(), AstNodes::TSTypeParameter(param));
|
||||
let name = identifier(param.name());
|
||||
let key = format_args!(name, " in ", param.constraint());
|
||||
let optional = match mapped.optional() {
|
||||
MappedModifier::None => "",
|
||||
@@ -794,19 +822,24 @@ pub(crate) fn is_opaque_type<'a>(statement: Stmt<'a>, f: &Formatter<'a>) -> bool
|
||||
has_word(statement.modifiers(), b"opaque", f)
|
||||
}
|
||||
|
||||
/// The same, for a statement of any file.
|
||||
pub(crate) fn is_flow_opaque_type(statement: Stmt<'_>) -> bool {
|
||||
statement.file().is_flow() && has_word_in(statement.file(), statement.modifiers(), b"opaque")
|
||||
}
|
||||
|
||||
/// `opaque type A<T>: B = C`, `opaque type A super B extends C = D`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_opaque_type<'a>(
|
||||
statement: Stmt<'a>,
|
||||
interface: Interface<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let node = AstNodes::TSInterfaceDeclaration(statement);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
is_declared(statement.modifiers()).then_some("declare "),
|
||||
"opaque type ",
|
||||
identifier(interface.name(), node),
|
||||
identifier(interface.name()),
|
||||
type_parameters(interface.type_params(), Node::Stmt(statement))
|
||||
]
|
||||
);
|
||||
@@ -879,6 +912,7 @@ pub(crate) fn is_declared_class(class: Class<'_>) -> bool {
|
||||
}
|
||||
|
||||
/// Writes `declare class` and `record`. Returns whether `class` is one of them.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_class<'a>(class: Class<'a>, f: &mut Formatter<'a>) -> bool {
|
||||
let is_record = is_record(class, f);
|
||||
if is_record || is_declared_class(class) {
|
||||
@@ -888,6 +922,16 @@ pub(crate) fn write_class<'a>(class: Class<'a>, f: &mut Formatter<'a>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// `A, B`: the types after `mixins` or `implements`.
|
||||
struct FormatHeritageClauses<'a>(List<'a, TypeNode<'a>>);
|
||||
|
||||
impl<'a> Format<'a> for FormatHeritageClauses<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
f.join_with(soft_line_break_or_space())
|
||||
.entries_with_trailing_separator(self.0.iter(), ",", TrailingSeparator::Disallowed);
|
||||
}
|
||||
}
|
||||
|
||||
/// `declare class A<T> extends B<T> mixins C implements D { }`, `record A<T> implements B { }`.
|
||||
/// Prettier's `printClass`.
|
||||
fn write_declared_class_or_record<'a>(class: Class<'a>, is_record: bool, f: &mut Formatter<'a>) {
|
||||
@@ -895,66 +939,75 @@ fn write_declared_class_or_record<'a>(class: Class<'a>, is_record: bool, f: &mut
|
||||
let (mixins, implements) = (class.flow_mixins(), class.implements());
|
||||
let count = usize::from(class.extends().is_some()) + mixins.len() + implements.len();
|
||||
let has_multiple_heritage = count > 1;
|
||||
let name_comments = class
|
||||
.name()
|
||||
.map(|name| f.comments().of_name(name.span().start));
|
||||
let type_parameters_node = AstNodes::TSTypeParameterDeclaration(Node::Class(class));
|
||||
// The first is `A.B` without type arguments.
|
||||
let is_qualified = |ty: TypeNode<'a>| matches!(ty.kind(), TypeKind::Ref { name, args } if name.len() > 1 && args.is_empty());
|
||||
// Prettier's `shouldPrintClassInGroupMode`
|
||||
let group_mode = has_multiple_heritage
|
||||
|| name_comments.is_some_and(|comments| !comments.trailing.is_empty())
|
||||
|| !(f.comments().of_node(type_parameters_node).trailing).is_empty()
|
||||
|| match (class.extends(), mixins.first()) {
|
||||
(Some(extends), _) => extends.tag() == ExprTag::Dot && class.extends_args().is_empty(),
|
||||
(None, Some(first)) => is_qualified(first),
|
||||
(None, None) => false,
|
||||
};
|
||||
// Where what is after `extends`, `mixins` and `implements` starts.
|
||||
let starts = [
|
||||
class.extends().map(|it| it.span().start),
|
||||
mixins.first().map(|it| it.span().start),
|
||||
implements.first().map(|it| it.span().start),
|
||||
];
|
||||
// A comment trails the name or the type parameters.
|
||||
let group_mode = group_mode
|
||||
|| (!f.is_quiet()
|
||||
&& starts
|
||||
.iter()
|
||||
.flatten()
|
||||
.next()
|
||||
.is_some_and(|&start| f.comments().has_comment_before(start)));
|
||||
|
||||
let format_extends = format_with(|f| {
|
||||
let mut names: SmallVec<[Ident<'a>; 4]> = SmallVec::new();
|
||||
// `A.B.C`, from the outside in: each `A.B` and its `B`.
|
||||
let mut qualified_names: SmallVec<[(AstNodes<'a>, Ident<'a>); 4]> = SmallVec::new();
|
||||
let mut leftmost = class.extends();
|
||||
while let Some(ExprKind::Dot { obj, name, .. }) = leftmost.map(|it| it.kind()) {
|
||||
names.push(name);
|
||||
while let Some(e) = leftmost
|
||||
&& let ExprKind::Dot { obj, name, .. } = e.kind()
|
||||
{
|
||||
qualified_names.push((e.as_chain_element(), name));
|
||||
leftmost = Some(obj);
|
||||
}
|
||||
for &(node, _) in qualified_names.iter() {
|
||||
write!(f, leading_comments(node));
|
||||
}
|
||||
write!(f, leftmost);
|
||||
for &name in names.iter().rev() {
|
||||
write!(f, [".", identifier(name, node)]);
|
||||
for &(node, name) in qualified_names.iter().rev() {
|
||||
write!(f, [".", identifier(name), trailing_comments(node)]);
|
||||
}
|
||||
write!(f, type_arguments(class.extends_args(), Node::Class(class)));
|
||||
});
|
||||
let (format_mixins, format_implements) = (
|
||||
FormatClassImplements(mixins),
|
||||
FormatClassImplements(implements),
|
||||
FormatHeritageClauses(mixins),
|
||||
FormatHeritageClauses(implements),
|
||||
);
|
||||
// Prettier's `printHeritageClauses`
|
||||
let clauses = format_with(|f| {
|
||||
let clauses: [(&'static str, &dyn Format<'a>); 3] = [
|
||||
("extends", &format_extends),
|
||||
("mixins", &format_mixins),
|
||||
("implements", &format_implements),
|
||||
let clauses: [(&'static str, Marker, bool, &dyn Format<'a>); 3] = [
|
||||
(
|
||||
"extends",
|
||||
Marker::Extends,
|
||||
class.extends().is_some(),
|
||||
&format_extends,
|
||||
),
|
||||
("mixins", Marker::Mixins, !mixins.is_empty(), &format_mixins),
|
||||
(
|
||||
"implements",
|
||||
Marker::Implements,
|
||||
!implements.is_empty(),
|
||||
&format_implements,
|
||||
),
|
||||
];
|
||||
for ((keyword, list), start) in clauses.into_iter().zip(starts) {
|
||||
let Some(start) = start else {
|
||||
continue;
|
||||
for (keyword, marker, _, list) in clauses.into_iter().filter(|clause| clause.2) {
|
||||
let comments = dangling_comments_with_marker(node, marker, f);
|
||||
let format_comments = FormatDanglingComments::Comments {
|
||||
comments,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
// The comments before a keyword trail what is before it.
|
||||
if !f.is_quiet() {
|
||||
let comments = f.comments().comments_before(start);
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
match (has_multiple_heritage, group_mode) {
|
||||
(true, _) => write!(
|
||||
f,
|
||||
[
|
||||
soft_line_break_or_space(),
|
||||
format_comments,
|
||||
(!comments.is_empty()).then_some(hard_line_break()),
|
||||
keyword,
|
||||
group(&soft_line_indent_or_space(list))
|
||||
]
|
||||
@@ -963,18 +1016,34 @@ fn write_declared_class_or_record<'a>(class: Class<'a>, is_record: bool, f: &mut
|
||||
f,
|
||||
[
|
||||
soft_line_break_or_space(),
|
||||
group(&format_args!(keyword, space(), list))
|
||||
group(&format_args!(keyword, space(), format_comments, list))
|
||||
]
|
||||
),
|
||||
(false, false) => write!(f, [space(), keyword, space(), list]),
|
||||
(false, false) => write!(f, [space(), keyword, space(), format_comments, list]),
|
||||
}
|
||||
}
|
||||
});
|
||||
let head = format_args!(
|
||||
space(),
|
||||
class.name().map(|name| identifier(name, node)),
|
||||
type_parameters(class.type_params(), Node::Class(class))
|
||||
);
|
||||
let head = format_with(|f| {
|
||||
if let (Some(name), Some(comments)) = (class.name(), name_comments) {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
space(),
|
||||
FormatLeadingComments::Comments(comments.leading),
|
||||
source_text(name.span()),
|
||||
indent(&FormatTrailingComments::Comments(comments.trailing))
|
||||
]
|
||||
);
|
||||
}
|
||||
let type_parameters = type_parameters(class.type_params(), Node::Class(class));
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&type_parameters),
|
||||
indent(&trailing_comments(type_parameters_node))
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
let keyword = if is_record { "record" } else { "class" };
|
||||
write!(
|
||||
@@ -1003,23 +1072,9 @@ fn write_declared_class_or_record<'a>(class: Class<'a>, is_record: bool, f: &mut
|
||||
} else {
|
||||
write!(f, [head, clauses, space()]);
|
||||
}
|
||||
// `Class::body_span` knows nothing of `mixins`.
|
||||
let body_span = match (mixins.last(), implements.is_empty()) {
|
||||
(Some(last), true) => Span::new(
|
||||
skip_trivia(f.file().text(), last.span().end),
|
||||
class.span().end,
|
||||
),
|
||||
_ => class.body_span(),
|
||||
};
|
||||
match class.members().is_empty() {
|
||||
true => write!(
|
||||
f,
|
||||
[
|
||||
"{",
|
||||
format_dangling_comments(body_span).with_block_indent(),
|
||||
"}"
|
||||
]
|
||||
),
|
||||
let body = AstNodes::ClassBody(class);
|
||||
format_node(body, f, |f| match class.members().is_empty() {
|
||||
true => write!(f, ["{", dangling_comments(body).with_block_indent(), "}"]),
|
||||
false if is_record => write_record_body(class, f),
|
||||
false => {
|
||||
write!(
|
||||
@@ -1034,7 +1089,7 @@ fn write_declared_class_or_record<'a>(class: Class<'a>, is_record: bool, f: &mut
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Prettier's `shouldOmitSemicolon` does not know records.
|
||||
if is_record && matches!(class.owner(), Node::Stmt(statement) if statement.is_default_export())
|
||||
{
|
||||
@@ -1045,6 +1100,7 @@ fn write_declared_class_or_record<'a>(class: Class<'a>, is_record: bool, f: &mut
|
||||
/// `hook` or `function`: what the declaration of `func` starts with. A component, and a function
|
||||
/// that is only declared, which is written like a function type, are written here: then it is
|
||||
/// `None`.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_component_or_keyword<'a>(
|
||||
func: Func<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
@@ -1062,31 +1118,31 @@ pub(crate) fn write_component_or_keyword<'a>(
|
||||
if func.has_body() {
|
||||
return Some(keyword);
|
||||
}
|
||||
let name = func
|
||||
.name()
|
||||
.map(|name| identifier(name, AstNodes::Function(func)));
|
||||
let name = func.name().map(|name| identifier(name));
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
is_declared(modifiers).then_some("declare "),
|
||||
keyword,
|
||||
space(),
|
||||
name
|
||||
name,
|
||||
// `f/* comment */ (): void`
|
||||
leading_comments(AstNodes::FormalParameters(func))
|
||||
]
|
||||
);
|
||||
write_ts_call_signature_declaration(func, f);
|
||||
if let Some(ty) = func.return_type()
|
||||
&& let TypeKind::Typeof { expr, .. } = ty.kind()
|
||||
&& ty.first_token() != b"typeof"
|
||||
{
|
||||
write!(f, [" %checks(", expr, ")"]);
|
||||
}
|
||||
// Those before the `;` trail the return type.
|
||||
if !f.is_quiet()
|
||||
&& let Some(semicolon) = statement.semicolon()
|
||||
{
|
||||
let comments = f.comments().comments_before(semicolon.start);
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
if let Some((_, expr)) = func.return_type().and_then(as_declared_predicate) {
|
||||
// `%checks(a) /* comment */`
|
||||
let comments = f.comments().of_node(AstNodes::Function(func)).dangling;
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
" %checks(",
|
||||
expr,
|
||||
")",
|
||||
FormatTrailingComments::Comments(comments)
|
||||
]
|
||||
);
|
||||
}
|
||||
write!(f, OptionalSemicolon);
|
||||
return None;
|
||||
@@ -1097,8 +1153,7 @@ pub(crate) fn write_component_or_keyword<'a>(
|
||||
is_declared(modifiers).then_some("declare "),
|
||||
func.is_async().then_some("async "),
|
||||
"component ",
|
||||
func.name()
|
||||
.map(|name| identifier(name, AstNodes::Function(func))),
|
||||
func.name().map(|name| identifier(name)),
|
||||
type_parameters(func.type_params(), Node::Func(func))
|
||||
]
|
||||
);
|
||||
@@ -1138,6 +1193,30 @@ pub(crate) fn import_kind(start: u32, f: &Formatter<'_>) -> &'static str {
|
||||
|
||||
// ───────────────────────────── match ─────────────────────────────
|
||||
|
||||
/// `match (a) { .. }` as an expression is a `new` expression in the tree, which does not start with `new`. Returns the
|
||||
/// `match (a)`, which is a call, and the `{ .. }`, which is an object.
|
||||
pub(crate) fn as_match_expression(e: Expr<'_>) -> Option<(Expr<'_>, Expr<'_>)> {
|
||||
let ExprKind::New(call) = e.kind() else {
|
||||
return None;
|
||||
};
|
||||
let (head, braces) = (call.callee(), call.args().first()?);
|
||||
(head.span().start == e.span().start
|
||||
&& head.tag() == ExprTag::Call
|
||||
&& braces.tag() == ExprTag::Object)
|
||||
.then_some((head, braces))
|
||||
}
|
||||
|
||||
/// `match (a) { .. }` as a statement is a `switch` statement in the tree, which does not start with `switch`. Returns the
|
||||
/// `match (a)`, which is a call.
|
||||
pub(crate) fn head_of_match_statement(statement: Stmt<'_>) -> Option<Expr<'_>> {
|
||||
match statement.kind() {
|
||||
StmtKind::Switch { expr, .. } => Some(expr).filter(|head| {
|
||||
head.tag() == ExprTag::Call && head.span().start == statement.span().start
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `match (a) { }` and `R { a: 1 }` are `new` expressions in the tree, which do not start with
|
||||
/// `new`. Returns whether `e` is one of them, which is written.
|
||||
pub(crate) fn write_expression_with_braces<'a>(
|
||||
@@ -1157,25 +1236,14 @@ pub(crate) fn write_expression_with_braces<'a>(
|
||||
let mut join = f.join_nodes_with_hardline();
|
||||
for case in cases.iter() {
|
||||
let content = format_with(|f| {
|
||||
format_node(
|
||||
case.span(),
|
||||
|| AstNodes::ObjectExpression(braces),
|
||||
f,
|
||||
|f| {
|
||||
write_match_expression_case(case, f);
|
||||
},
|
||||
);
|
||||
format_node(case.as_ast_nodes(), f, |f| {
|
||||
write_match_expression_case(case, f);
|
||||
});
|
||||
});
|
||||
join.entry(case.span(), &content);
|
||||
}
|
||||
});
|
||||
write_match(
|
||||
head,
|
||||
braces.span().start,
|
||||
cases.is_empty(),
|
||||
&format_cases,
|
||||
f,
|
||||
);
|
||||
write_match(head, cases.is_empty(), &format_cases, f);
|
||||
}
|
||||
_ => {
|
||||
let type_args = call.type_args();
|
||||
@@ -1204,10 +1272,9 @@ pub(crate) fn write_match_statement<'a>(
|
||||
cases: List<'a, Case<'a>>,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
let head_end = head.span().end;
|
||||
let Some(head) = head
|
||||
.as_call()
|
||||
.filter(|_| head.span().start == statement.span().start)
|
||||
.filter(|_| head_of_match_statement(statement) == Some(head))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
@@ -1215,48 +1282,31 @@ pub(crate) fn write_match_statement<'a>(
|
||||
let mut join = f.join_nodes_with_hardline();
|
||||
for case in cases.iter() {
|
||||
let content = format_with(|f| {
|
||||
format_node(
|
||||
case.span(),
|
||||
|| AstNodes::SwitchStatement(statement),
|
||||
f,
|
||||
|f| {
|
||||
write_match_pattern_and_guard(case.test(), f);
|
||||
let body = case.body().first();
|
||||
let is_empty = body.is_some_and(|it| {
|
||||
matches!(it.kind(), StmtKind::Block(statements) if statements.is_empty())
|
||||
&& !f.comments().has_comment_in_span(it.span())
|
||||
});
|
||||
match is_empty {
|
||||
true => write!(f, " => {}"),
|
||||
false => write!(f, [" => ", body]),
|
||||
format_node(AstNodes::SwitchCase(case), f, |f| {
|
||||
write_match_pattern_and_guard(case.test(), f);
|
||||
write!(f, " => ");
|
||||
match case.body().first() {
|
||||
// An empty block in a `case` of a `switch` is written differently.
|
||||
Some(block) if block.as_block().is_some_and(|it| it.is_empty()) => {
|
||||
let node = block.as_ast_nodes();
|
||||
format_node(node, f, |f| {
|
||||
write!(f, ["{", dangling_comments(node).with_block_indent(), "}"]);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
body => write!(f, body),
|
||||
}
|
||||
});
|
||||
});
|
||||
join.entry(case.span(), &content);
|
||||
}
|
||||
});
|
||||
let open_brace = skip_trivia(f.file().text(), head_end);
|
||||
write_match(head, open_brace, cases.is_empty(), &format_cases, f);
|
||||
write_match(head, cases.is_empty(), &format_cases, f);
|
||||
true
|
||||
}
|
||||
|
||||
/// Prettier's `printMatch`. `head`: the `match (a)`. `open_brace`: where the `{` is.
|
||||
fn write_match<'a>(
|
||||
head: Call<'a>,
|
||||
open_brace: u32,
|
||||
is_empty: bool,
|
||||
cases: &impl Format<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let argument = format_with(|f| {
|
||||
write!(f, head.args().first());
|
||||
// What is behind the `{` on its line trails the argument.
|
||||
if !f.is_quiet() {
|
||||
let comments = f.comments().end_of_line_comments_after(open_brace + 1);
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
});
|
||||
/// Prettier's `printMatch`. `head`: the `match (a)`.
|
||||
fn write_match<'a>(head: Call<'a>, is_empty: bool, cases: &impl Format<'a>, f: &mut Formatter<'a>) {
|
||||
let argument = head.args().first();
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
@@ -1285,12 +1335,9 @@ fn write_match_pattern_and_guard<'a>(pattern: Option<Expr<'a>>, f: &mut Formatte
|
||||
write_match_pattern(left, PatternParent::Case, f);
|
||||
// It is in no `&&`.
|
||||
let guard = format_with(|f| {
|
||||
format_node(
|
||||
right.span(),
|
||||
|| right.ast_parent(),
|
||||
f,
|
||||
|f| write_expression(right, ExprOptions::None, f),
|
||||
);
|
||||
format_node(right.as_ast_nodes(), f, |f| {
|
||||
write_expression(right, ExprOptions::None, f)
|
||||
});
|
||||
});
|
||||
write!(
|
||||
f,
|
||||
@@ -1371,12 +1418,9 @@ fn write_match_pattern<'a>(pattern: Expr<'a>, parent: PatternParent, f: &mut For
|
||||
if is_leaf {
|
||||
return write!(f, pattern);
|
||||
}
|
||||
format_node(
|
||||
pattern.span(),
|
||||
|| pattern.ast_parent(),
|
||||
f,
|
||||
|f| write_match_pattern_that_has_parts(pattern, parent, f),
|
||||
);
|
||||
format_node(pattern.as_ast_nodes(), f, |f| {
|
||||
write_match_pattern_that_has_parts(pattern, parent, f)
|
||||
});
|
||||
}
|
||||
|
||||
fn write_match_pattern_that_has_parts<'a>(
|
||||
@@ -1429,6 +1473,7 @@ fn write_match_pattern_that_has_parts<'a>(
|
||||
if !has_rest {
|
||||
write!(f, if_group_breaks(&","));
|
||||
}
|
||||
write!(f, dangling_comments(pattern.as_ast_nodes()));
|
||||
});
|
||||
write!(
|
||||
f,
|
||||
@@ -1444,18 +1489,14 @@ fn write_match_pattern_that_has_parts<'a>(
|
||||
if index > 0 {
|
||||
write!(f, [",", soft_line_break_or_space()]);
|
||||
}
|
||||
format_node(
|
||||
property.span(),
|
||||
|| AstNodes::ObjectExpression(pattern),
|
||||
f,
|
||||
|f| {
|
||||
write_match_object_pattern_property(property, f);
|
||||
},
|
||||
);
|
||||
format_node(property.as_ast_nodes(), f, |f| {
|
||||
write_match_object_pattern_property(property, f);
|
||||
});
|
||||
}
|
||||
if !has_rest {
|
||||
write!(f, if_group_breaks(&","));
|
||||
}
|
||||
write!(f, dangling_comments(pattern.as_ast_nodes()));
|
||||
});
|
||||
write!(
|
||||
f,
|
||||
@@ -1482,15 +1523,11 @@ fn write_match_object_pattern_property<'a>(property: Prop<'a>, f: &mut Formatter
|
||||
return write_match_pattern(value, PatternParent::Other, f);
|
||||
};
|
||||
let span = key.span(f.file());
|
||||
let format_key = format_with(|f| match key.kind() {
|
||||
KeyKind::String(_) => write!(
|
||||
f,
|
||||
FormatStringLiteral {
|
||||
span,
|
||||
parent: AstNodes::ObjectProperty(property)
|
||||
}
|
||||
),
|
||||
_ => write!(f, source_text(span)),
|
||||
let format_key = format_with(|f| {
|
||||
format_name(span, f, |f| match key.kind() {
|
||||
KeyKind::String(_) => write!(f, FormatStringLiteral { span }),
|
||||
_ => write!(f, source_text(span)),
|
||||
});
|
||||
});
|
||||
let format_value = format_with(|f| write_match_pattern(value, PatternParent::Property, f));
|
||||
write!(
|
||||
@@ -1518,6 +1555,21 @@ fn is_simple_match_pattern(pattern: Expr<'_>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// One of the patterns of `a | b`.
|
||||
struct FormatAlternative<'a>(Expr<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatAlternative<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
write_match_pattern(self.0, PatternParent::Other, f);
|
||||
}
|
||||
}
|
||||
|
||||
impl Spanned for FormatAlternative<'_> {
|
||||
fn span(&self) -> Span {
|
||||
self.0.span()
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `printMatchOrPattern`.
|
||||
fn write_match_or_pattern<'a>(pattern: Expr<'a>, parent: PatternParent, f: &mut Formatter<'a>) {
|
||||
let patterns = alternatives(pattern);
|
||||
@@ -1552,12 +1604,22 @@ fn write_match_or_pattern<'a>(pattern: Expr<'a>, parent: PatternParent, f: &mut
|
||||
if index > 0 {
|
||||
write!(f, [soft_line_break_or_space(), "| "]);
|
||||
}
|
||||
// The pattern is aligned without its comments:
|
||||
//
|
||||
// | a
|
||||
// // comment
|
||||
// | b
|
||||
let node = it.as_chain_element();
|
||||
write!(
|
||||
f,
|
||||
align(
|
||||
2,
|
||||
&format_with(|f| write_match_pattern(it, PatternParent::Other, f))
|
||||
)
|
||||
[
|
||||
leading_comments(node),
|
||||
align(
|
||||
2,
|
||||
&FormatNodeWithoutTrailingComments(&FormatAlternative(it))
|
||||
),
|
||||
trailing_comments(node)
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1597,6 +1659,11 @@ fn is_record<'a>(class: Class<'a>, f: &Formatter<'a>) -> bool {
|
||||
has_word(class.modifiers(), b"record", f)
|
||||
}
|
||||
|
||||
/// The same, for a class of any file.
|
||||
pub(crate) fn is_flow_record(class: Class<'_>) -> bool {
|
||||
class.file().is_flow() && has_word_in(class.file(), class.modifiers(), b"record")
|
||||
}
|
||||
|
||||
/// The `{ }` of a record that has members. Prettier's `printClassBody`.
|
||||
fn write_record_body<'a>(class: Class<'a>, f: &mut Formatter<'a>) {
|
||||
let members = class.members();
|
||||
@@ -1634,21 +1701,16 @@ impl<'a> Format<'a> for FormatRecordProperty<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let member = self.0;
|
||||
let node = member.as_ast_nodes();
|
||||
format_node(
|
||||
member.span(),
|
||||
|| node.parent(),
|
||||
f,
|
||||
|f| {
|
||||
write!(f, member.is_static().then_some("static "));
|
||||
if let Some(key) = member.key() {
|
||||
format_computed_or_property_key(key, node, f);
|
||||
}
|
||||
write!(f, member.ty().map(FormatTypeAnnotation));
|
||||
if let Some(value) = member.init() {
|
||||
write!(f, [" = ", value]);
|
||||
}
|
||||
},
|
||||
);
|
||||
format_node(node, f, |f| {
|
||||
write!(f, member.is_static().then_some("static "));
|
||||
if let Some(key) = member.key() {
|
||||
format_computed_or_property_key(key, node, f);
|
||||
}
|
||||
write!(f, member.ty().map(FormatTypeAnnotation));
|
||||
if let Some(value) = member.init() {
|
||||
write!(f, [" = ", value]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+62
-156
@@ -1,18 +1,17 @@
|
||||
use super::arrow_function_expression::{FormatMaybeCachedFunctionBody, FunctionCacheMode};
|
||||
use super::block_statement::is_empty_block;
|
||||
use super::parameters::{FormatFormalParameters, follows_name_or_type_parameters};
|
||||
use super::parameters::FormatFormalParameters;
|
||||
use super::program::FormatStatements;
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use super::type_parameters::type_parameters;
|
||||
use crate::js::format::{
|
||||
ExprOptions, FormatTypeAnnotation, format_node_without_comments, identifier, write_expression,
|
||||
ExprOptions, FormatTypeAnnotation, format_node, identifier, write_expression,
|
||||
};
|
||||
use crate::js::trivia::{comments_stay_between_head_and_body, write_head_body_separator};
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::statement_body::write_space_before_body;
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::js::utils::typescript::end_of_line_comments;
|
||||
use crate::js::utils::typecast::is_cast_target;
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
use crate::{format_args, write};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub(crate) struct FormatFunctionOptions {
|
||||
@@ -20,7 +19,14 @@ pub(crate) struct FormatFunctionOptions {
|
||||
pub(crate) cache_mode: FunctionCacheMode,
|
||||
}
|
||||
|
||||
/// `a = function /* comment */⏎<T>() {}`: for Prettier the line break after the comment breaks with what the function is in. For
|
||||
/// oxfmt it goes if the type parameters fit behind the comment.
|
||||
fn comments_of_type_parameters_are_in_a_group_with_them(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// A function declaration or expression. Not a method: see `class.rs`.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_function<'a>(
|
||||
func: Func<'a>,
|
||||
options: FormatFunctionOptions,
|
||||
@@ -47,11 +53,14 @@ pub(crate) fn write_function<'a>(
|
||||
(!is_only_head).then_some(keyword),
|
||||
func.is_generator().then_some("*"),
|
||||
(!is_only_head).then_some(space()),
|
||||
func.name().map(|name| identifier(name, node)),
|
||||
group(&type_parameters(func.type_params(), Node::Func(func))),
|
||||
FormatCommentsBehindParenthesis(func),
|
||||
func.name().map(|name| identifier(name)),
|
||||
]
|
||||
);
|
||||
let type_parameters = type_parameters(func.type_params(), Node::Func(func));
|
||||
match comments_of_type_parameters_are_in_a_group_with_them(f) {
|
||||
true => write!(f, group(&type_parameters)),
|
||||
false => write!(f, type_parameters),
|
||||
}
|
||||
});
|
||||
FormatContentWithCacheMode::new(|| node.span(), head, options.cache_mode).fmt(f);
|
||||
|
||||
@@ -63,21 +72,7 @@ pub(crate) fn write_function<'a>(
|
||||
|
||||
let format_return_type = func.return_type().map(|return_type| {
|
||||
let return_type = FormatTypeAnnotation(return_type);
|
||||
let content = format_with(move |f: &mut Formatter<'a>| {
|
||||
let needs_space = f.comments().has_comment_before(return_type.span().start);
|
||||
// The comments before the `{` are written with the body.
|
||||
match func.has_body() {
|
||||
true => write!(
|
||||
f,
|
||||
[
|
||||
maybe_space(needs_space),
|
||||
FormatNodeWithoutTrailingComments(&return_type)
|
||||
]
|
||||
),
|
||||
false => write!(f, [maybe_space(needs_space), return_type]),
|
||||
}
|
||||
});
|
||||
FormatContentWithCacheMode::new(|| return_type.span(), content, options.cache_mode)
|
||||
FormatContentWithCacheMode::new(|| return_type.span(), return_type, options.cache_mode)
|
||||
});
|
||||
|
||||
write!(
|
||||
@@ -121,29 +116,6 @@ pub(crate) fn write_function<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments behind the `(` of the parameters on its line, if nothing else follows on that line.
|
||||
/// They trail what is before the `(`, a name or type parameters, if there is such a node:
|
||||
///
|
||||
/// ```js
|
||||
/// function f( // comment
|
||||
/// a) {}
|
||||
/// ```
|
||||
pub(crate) struct FormatCommentsBehindParenthesis<'a>(pub(crate) Func<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatCommentsBehindParenthesis<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
// For oxfmt the parentheses are a node, and the comments in it lead the first parameter.
|
||||
if !f.is_quiet()
|
||||
&& !f.options().flavor.is_oxfmt()
|
||||
&& let Some(first) = self.0.params_with_this().next()
|
||||
&& follows_name_or_type_parameters(self.0)
|
||||
{
|
||||
let comments = end_of_line_comments(f.comments().comments_before(first.span().start));
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `isIifeCalleeOrTaggedTemplateExpressionTag`.
|
||||
fn is_iife_callee_or_tagged_template_tag(e: Expr<'_>) -> bool {
|
||||
match e.ast_parent() {
|
||||
@@ -175,56 +147,31 @@ pub(crate) fn write_called_function_with_comments<'a>(
|
||||
options: ExprOptions,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
// In the parentheses of a type cast it is not what is called.
|
||||
if e.as_fn().is_none()
|
||||
|| !is_iife_callee_or_tagged_template_tag(e)
|
||||
|| comments_of_called_function_are_outside_of_its_parentheses(f)
|
||||
|| is_cast_target(e, f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let (span, end) = (e.span(), e.outer_span().end);
|
||||
// Whoever writes the callee may have hidden the comments after it.
|
||||
let view_limit = f.comments_mut().limit_comments_up_to(0);
|
||||
f.comments_mut().restore_view_limit(None);
|
||||
f.comments_mut().limit_comments_up_to(end);
|
||||
|
||||
// On a line of its own, a comment leads what follows: the first argument or the template.
|
||||
let is_followed = match e.ast_parent() {
|
||||
AstNodes::CallExpression(call) => {
|
||||
call.as_call_like().is_some_and(|it| !it.args().is_empty())
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
let count_trailing = |f: &Formatter<'a>| {
|
||||
let comments = f.comments().comments_in(Span::after(span, end));
|
||||
comments
|
||||
.iter()
|
||||
.take_while(|it| !(is_followed && it.preceded_by_newline()))
|
||||
.count()
|
||||
};
|
||||
let has_comments = f.comments().has_comment_before(span.start) || count_trailing(f) > 0;
|
||||
if has_comments {
|
||||
let is_suppressed = f.comments().is_suppressed(span.start);
|
||||
let content = format_with(|f| {
|
||||
write!(f, format_leading_comments(span));
|
||||
match is_suppressed {
|
||||
true => write!(f, FormatSuppressedNode(span)),
|
||||
false => write_expression(e, options, f),
|
||||
}
|
||||
let count = count_trailing(f);
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(
|
||||
f.comments()
|
||||
.comments_before(end)
|
||||
.get(..count)
|
||||
.unwrap_or_default()
|
||||
)
|
||||
);
|
||||
});
|
||||
write!(f, ["(", soft_block_indent(&content), ")"]);
|
||||
let owned = f.comments().of_node(e.as_chain_element());
|
||||
if owned.leading.is_empty() && owned.trailing.is_empty() {
|
||||
return false;
|
||||
}
|
||||
f.comments_mut().restore_view_limit(view_limit);
|
||||
has_comments
|
||||
let skip_trailing = f.context_mut().take_skip_trailing_comments();
|
||||
let content = format_with(|f| {
|
||||
write!(f, FormatLeadingComments::Comments(owned.leading));
|
||||
match owned.is_ignored {
|
||||
true => write!(f, FormatSuppressedNode(e.span())),
|
||||
false => write_expression(e, options, f),
|
||||
}
|
||||
if !skip_trailing {
|
||||
write!(f, FormatTrailingComments::Comments(owned.trailing));
|
||||
}
|
||||
});
|
||||
write!(f, ["(", soft_block_indent(&content), ")"]);
|
||||
true
|
||||
}
|
||||
|
||||
/// The `{ .. }` of a function.
|
||||
@@ -251,78 +198,37 @@ impl<'a> Format<'a> for FormatFunctionBody<'a> {
|
||||
),
|
||||
};
|
||||
}
|
||||
let write = |f: &mut Formatter<'a>| {
|
||||
match comments_stay_between_head_and_body(f) && !func.is_arrow() {
|
||||
true => write_head_body_separator(self.span().start, f),
|
||||
false => write!(f, [FormatCommentsBeforeBody(func), space()]),
|
||||
let node = AstNodes::FunctionBody(func);
|
||||
let owned = f.comments().of_node(node);
|
||||
if !(owned.is_ignored && space_is_part_of_the_body(f)) {
|
||||
write_space_before_body(node, f);
|
||||
}
|
||||
format_node(node, f, |f| match is_empty_block(statements) {
|
||||
true => write!(f, ["{", dangling_comments(node).with_block_indent(), "}"]),
|
||||
false if owned.dangling.is_empty() => {
|
||||
write!(f, ["{", block_indent(&FormatStatements(statements)), "}"])
|
||||
}
|
||||
if is_empty_block(statements) {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
"{",
|
||||
format_dangling_comments(self.span()).with_block_indent(),
|
||||
"}"
|
||||
]
|
||||
// `function a() // comment⏎{ "use strict" }`: Babel has no statement in the body that the comment could lead.
|
||||
// Prettier's `printBlockBody` writes it after the directives.
|
||||
false => {
|
||||
let is_next_line_empty = (statements.last())
|
||||
.is_some_and(|last| f.source_text().lines_after(last.span().end) > 1);
|
||||
let content = format_args!(
|
||||
FormatStatements(statements),
|
||||
is_next_line_empty.then_some(empty_line()),
|
||||
(!is_next_line_empty).then_some(hard_line_break()),
|
||||
dangling_comments(node)
|
||||
);
|
||||
} else {
|
||||
write!(f, ["{", block_indent(&FormatStatements(statements)), "}"]);
|
||||
write!(f, ["{", block_indent(&content), "}"]);
|
||||
}
|
||||
};
|
||||
format_node_without_comments(self.span(), || func.as_ast_nodes(), f, write);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments before the `{` of a function that stay there. A line comment right before the `{`
|
||||
/// does not: it is moved into the block. Prettier's `handleLastFunctionParameterComments`.
|
||||
struct FormatCommentsBeforeBody<'a>(Func<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatCommentsBeforeBody<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_before(FormatFunctionBody(self.0).span().start);
|
||||
if comments.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Those from before the `(` lead the block, whatever they are.
|
||||
let parameters_start = FormatFormalParameters(self.0).span().start;
|
||||
// After a `=>`, a comment at the end of the line is moved into the block as well:
|
||||
// `handleArrowExpressionComments`.
|
||||
let is_arrow = self.0.is_arrow();
|
||||
let count = (comments.iter())
|
||||
.take_while(|it| {
|
||||
(it.is_block() && !(is_arrow && it.followed_by_newline()))
|
||||
|| it.end() <= parameters_start
|
||||
})
|
||||
.count();
|
||||
let comments = comments.get(..count).unwrap_or_default();
|
||||
let (moved, comments) = comments.split_at(
|
||||
comments
|
||||
.iter()
|
||||
.take_while(|it| it.end() <= parameters_start)
|
||||
.count(),
|
||||
);
|
||||
let (trailing, leading) = comments.split_at(
|
||||
comments
|
||||
.iter()
|
||||
.take_while(|it| !it.preceded_by_newline())
|
||||
.count(),
|
||||
);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
space(),
|
||||
FormatLeadingComments::Comments(moved),
|
||||
FormatTrailingComments::Comments(trailing),
|
||||
space(),
|
||||
FormatLeadingComments::Comments(leading)
|
||||
]
|
||||
);
|
||||
}
|
||||
/// `class A { b() /* prettier-ignore */ { } }` is `b()/* prettier-ignore */ { }` for oxfmt: what is copied is written
|
||||
/// without the space before it. A function and a method of an object have a space of their own there.
|
||||
fn space_is_part_of_the_body(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
impl Spanned for FormatFunctionBody<'_> {
|
||||
|
||||
@@ -4,9 +4,7 @@ use super::function::should_group_function_parameters;
|
||||
use super::parameters::FormatFormalParameters;
|
||||
use super::type_parameters::type_parameters;
|
||||
use crate::js::format::{FormatReturnTypeOfFunctionType, FormatTypeAnnotation};
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::object::format_computed_or_property_key;
|
||||
use crate::js::utils::typescript::end_of_line_comments;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -63,12 +61,10 @@ pub(crate) fn write_ts_method_signature<'a>(
|
||||
if let Some(key) = member.key() {
|
||||
format_computed_or_property_key(key, AstNodes::TSMethodSignature(member), f);
|
||||
}
|
||||
if func.type_params().is_empty() {
|
||||
write!(f, FormatCommentsAroundParenthesis(func));
|
||||
}
|
||||
write!(f, member.flags().contains(Flags::OPTIONAL).then_some("?"));
|
||||
|
||||
let format_type_parameters = FormatTypeParameters(func).memoized();
|
||||
let format_type_parameters =
|
||||
type_parameters(func.type_params(), Node::Func(func)).memoized();
|
||||
let format_parameters = FormatFormalParameters(func).memoized();
|
||||
format_type_parameters.inspect(f);
|
||||
format_parameters.inspect(f);
|
||||
@@ -94,13 +90,10 @@ fn format_grouped_parameters_with_return_type<'a>(
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
group(&format_with(|f| {
|
||||
let format_type_parameters = FormatTypeParameters(func).memoized();
|
||||
let format_type_parameters =
|
||||
type_parameters(func.type_params(), Node::Func(func)).memoized();
|
||||
let format_parameters = FormatFormalParameters(func).memoized();
|
||||
let return_type = func.return_type().map(|_| FormatReturnType(func));
|
||||
let format_return_type = return_type
|
||||
.as_ref()
|
||||
.map(FormatNodeWithoutTrailingComments)
|
||||
.memoized();
|
||||
let format_return_type = FormatReturnType(func).memoized();
|
||||
|
||||
format_type_parameters.inspect(f);
|
||||
format_parameters.inspect(f);
|
||||
@@ -123,67 +116,14 @@ fn format_grouped_parameters_with_return_type<'a>(
|
||||
.fmt(f);
|
||||
}
|
||||
|
||||
/// The type parameters of a signature, with the comments after them.
|
||||
struct FormatTypeParameters<'a>(Func<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatTypeParameters<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let params = self.0.type_params();
|
||||
if !params.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
type_parameters(params, Node::Func(self.0)),
|
||||
FormatCommentsAroundParenthesis(self.0)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments that trail what is before the `(` of a signature: a name or type parameters.
|
||||
///
|
||||
/// - If there are no parameters, those before the `(`, which there is nothing for to lead.
|
||||
/// - Those after the `(` on the same line, if nothing else follows on that line.
|
||||
struct FormatCommentsAroundParenthesis<'a>(Func<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatCommentsAroundParenthesis<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let Some(span) = self.0.params_span().filter(|_| !f.is_quiet()) else {
|
||||
return;
|
||||
};
|
||||
let comments = match self.0.this_param().or_else(|| self.0.params().first()) {
|
||||
// For oxfmt the parentheses are a node, and the comments in it lead the first parameter.
|
||||
Some(_) if f.options().flavor.is_oxfmt() => return,
|
||||
Some(first) => end_of_line_comments(f.comments().comments_before(first.span().start)),
|
||||
None => f.comments().comments_before(span.start),
|
||||
};
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
|
||||
/// `: R`, `=> R`. Prettier's `printTypeAnnotationProperty`: there is a space between the `)` and the
|
||||
/// comments before a `:`.
|
||||
/// `: R`, `=> R`
|
||||
struct FormatReturnType<'a>(Func<'a>);
|
||||
|
||||
impl Spanned for FormatReturnType<'_> {
|
||||
fn span(&self) -> Span {
|
||||
self.0
|
||||
.return_type()
|
||||
.map(TypeNode::annotation_span)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatReturnType<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let Some(return_type) = self.0.return_type() else {
|
||||
return;
|
||||
};
|
||||
let has_comment = !f.is_quiet()
|
||||
&& f.comments()
|
||||
.has_comment_before(return_type.annotation_span().start);
|
||||
write!(f, has_comment.then_some(space()));
|
||||
match self.0.owner() {
|
||||
Node::Type(_) => write!(f, FormatReturnTypeOfFunctionType(return_type)),
|
||||
_ => write!(f, FormatTypeAnnotation(return_type)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::program::is_next_line_empty;
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use crate::js::format::{format_node, identifier};
|
||||
use crate::js::format::{format_ignorable_name, format_name, format_node, identifier};
|
||||
use crate::js::utils::object::FormatKey;
|
||||
use crate::js::utils::string::{
|
||||
FormatLiteralStringToken, StringLiteralParentKind, is_name_without_quotes,
|
||||
@@ -9,41 +9,27 @@ use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
/// A string literal that is not an expression here: a module specifier.
|
||||
pub(crate) struct FormatStringLiteral<'a> {
|
||||
pub(crate) struct FormatStringLiteral {
|
||||
pub(crate) span: Span,
|
||||
pub(crate) parent: AstNodes<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatStringLiteral<'a> {
|
||||
impl<'a> Format<'a> for FormatStringLiteral {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
format_node(
|
||||
self.span,
|
||||
|| self.parent,
|
||||
f,
|
||||
|f| {
|
||||
let string = f.source_text().text_for(&self.span);
|
||||
write!(
|
||||
f,
|
||||
FormatLiteralStringToken::new(
|
||||
string,
|
||||
false,
|
||||
StringLiteralParentKind::Expression
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
format_ignorable_name(self.span, f, |f| {
|
||||
let string = f.source_text().text_for(&self.span);
|
||||
write!(
|
||||
f,
|
||||
FormatLiteralStringToken::new(string, false, StringLiteralParentKind::Expression)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// ESTree's `ModuleExportName`: `a`, or `"a"`.
|
||||
pub(crate) fn module_export_name<'a>(name: Ident<'a>, parent: AstNodes<'a>) -> impl Format<'a> {
|
||||
pub(crate) fn module_export_name<'a>(name: Ident<'a>) -> impl Format<'a> {
|
||||
format_with(move |f: &mut Formatter<'a>| match name.is_string() {
|
||||
true => FormatStringLiteral {
|
||||
span: name.span(),
|
||||
parent,
|
||||
}
|
||||
.fmt(f),
|
||||
false => identifier(name, parent).fmt(f),
|
||||
true => FormatStringLiteral { span: name.span() }.fmt(f),
|
||||
false => identifier(name).fmt(f),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,47 +38,23 @@ pub(crate) fn format_import_and_export_source_with_clause<'a>(
|
||||
statement: Stmt<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let parent = statement.as_ast_nodes();
|
||||
let Some(span) = statement.module_specifier_span() else {
|
||||
return;
|
||||
};
|
||||
let source = FormatStringLiteral { span, parent };
|
||||
write!(f, FormatStringLiteral { span });
|
||||
let Some(with_clause) = statement.import_attributes() else {
|
||||
return write!(f, source);
|
||||
return;
|
||||
};
|
||||
if f.is_quiet() {
|
||||
write!(f, source);
|
||||
return write_with_clause(with_clause, f);
|
||||
// `import "a" with { /* comment */ }`: Prettier has no node for the braces, so the comment trails the source.
|
||||
if !f.is_quiet() && with_clause.entries().is_empty() {
|
||||
let braces = with_clause.object().as_ast_nodes();
|
||||
let comments = f.comments().of_node(braces).dangling;
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(span.end);
|
||||
write!(f, source);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
|
||||
// Without attributes all comments trail the source. Otherwise, of the comments before the
|
||||
// first attribute that do not start their line, those before the `{` and those that end their
|
||||
// line. The others lead the attribute.
|
||||
let braces = with_clause.braces_span();
|
||||
let comments = match with_clause.entries().first() {
|
||||
None => f.comments().comments_before(braces.end),
|
||||
Some(first) => {
|
||||
let comments = f.comments().comments_before(first.span().start);
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|comment| !comment.preceded_by_newline())
|
||||
.enumerate()
|
||||
.filter(|(_, comment)| {
|
||||
comment.span.end <= braces.start || comment.followed_by_newline()
|
||||
})
|
||||
.last()
|
||||
.map_or(0, |(index, _)| index + 1);
|
||||
comments.get(..count).unwrap_or_default()
|
||||
}
|
||||
};
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
write_with_clause(with_clause, f);
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_import_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
import: Import<'a>,
|
||||
@@ -105,7 +67,7 @@ pub(crate) fn write_import_declaration<'a>(
|
||||
write!(f, super::flow::import_kind(import.clause_span().start, f));
|
||||
}
|
||||
if !import.is_side_effect() {
|
||||
write_import_specifiers(statement, import, f);
|
||||
write_import_specifiers(import, f);
|
||||
write!(f, " from ");
|
||||
}
|
||||
format_import_and_export_source_with_clause(statement, f);
|
||||
@@ -113,32 +75,23 @@ pub(crate) fn write_import_declaration<'a>(
|
||||
}
|
||||
|
||||
/// `a, * as b`, `a, { b, c }`
|
||||
fn write_import_specifiers<'a>(statement: Stmt<'a>, import: Import<'a>, f: &mut Formatter<'a>) {
|
||||
let node = AstNodes::ImportDeclaration(statement);
|
||||
fn write_import_specifiers<'a>(import: Import<'a>, f: &mut Formatter<'a>) {
|
||||
let named = import.named();
|
||||
// `import a, {} from "a"` is `import a from "a"`.
|
||||
let has_braces = import.has_named_imports()
|
||||
&& !(named.is_empty() && (import.default().is_some() || import.namespace().is_some()));
|
||||
|
||||
if let Some(default) = import.default() {
|
||||
write!(f, identifier(default, node));
|
||||
write!(f, identifier(default));
|
||||
if import.namespace().is_none() && !has_braces {
|
||||
return;
|
||||
}
|
||||
write!(f, [",", space()]);
|
||||
}
|
||||
if let (Some(namespace), Some(span)) = (import.namespace(), import.namespace_span()) {
|
||||
format_node(
|
||||
span,
|
||||
|| node,
|
||||
f,
|
||||
|f| {
|
||||
write!(
|
||||
f,
|
||||
["*", space(), "as", space(), identifier(namespace, node)]
|
||||
)
|
||||
},
|
||||
);
|
||||
format_ignorable_name(span, f, |f| {
|
||||
write!(f, ["*", space(), "as", space(), identifier(namespace)])
|
||||
});
|
||||
if !has_braces {
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +105,9 @@ fn write_import_specifiers<'a>(statement: Stmt<'a>, import: Import<'a>, f: &mut
|
||||
match named.first() {
|
||||
None => write!(f, "{}"),
|
||||
Some(only)
|
||||
if is_only_specifier && !only_specifier_has_comments(statement, only.span(), f) =>
|
||||
if is_only_specifier
|
||||
&& (f.is_quiet()
|
||||
|| !f.comments().has_comments(AstNodes::ImportSpecifier(only))) =>
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
@@ -165,75 +120,62 @@ fn write_import_specifiers<'a>(statement: Stmt<'a>, import: Import<'a>, f: &mut
|
||||
]
|
||||
)
|
||||
}
|
||||
Some(_) => write!(f, ["{", FormatSpecifiers(statement, named), "}"]),
|
||||
Some(_) => write!(
|
||||
f,
|
||||
["{", FormatSpecifiers(named, AstNodes::ImportSpecifier), "}"]
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a comment belongs to `specifier`, the only one of the import or export `statement`. One
|
||||
/// inside of it does if it starts or ends its line, otherwise it belongs to a name.
|
||||
pub(crate) fn only_specifier_has_comments<'a>(
|
||||
statement: Stmt<'a>,
|
||||
specifier: Span,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
!f.is_quiet()
|
||||
&& (f
|
||||
.comments()
|
||||
.comments_before_character(statement.span().start, b'}')
|
||||
.iter()
|
||||
.any(|comment| {
|
||||
!specifier.contains(comment.span)
|
||||
|| comment.preceded_by_newline()
|
||||
|| comment.followed_by_newline()
|
||||
})
|
||||
|| !comments_before_from(specifier.end, statement, f).is_empty())
|
||||
/// `import {⏎⏎// comment⏎a } from "b"`: oxfmt keeps the empty line, also behind the `{`.
|
||||
fn empty_line_before_comments_of_specifier_stays(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `{ a } /* comment */ from "a"`: of the comments that are not printed yet between `position`,
|
||||
/// where the last specifier of `statement` ends, and the source, those that trail the specifier:
|
||||
/// the ones that end their line, and the ones before the `from` that do not start their line.
|
||||
fn comments_before_from<'a>(
|
||||
mut position: u32,
|
||||
statement: Stmt<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> &'a [Comment] {
|
||||
let Some(source) = statement.module_specifier_span() else {
|
||||
return &[];
|
||||
};
|
||||
let comments = f.comments().comments_in(Span::before(position, source));
|
||||
let mut is_before_from = true;
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|comment| {
|
||||
let gap = f
|
||||
.source_text()
|
||||
.text_for(&Span::before(position, comment.span));
|
||||
position = comment.span.end;
|
||||
is_before_from &= !bun_core::strings::contains(gap, b"from");
|
||||
comment.followed_by_newline() || (is_before_from && !comment.preceded_by_newline())
|
||||
})
|
||||
.count();
|
||||
comments.get(..count).unwrap_or_default()
|
||||
/// A specifier of an import or an export.
|
||||
struct FormatSpecifier<'a, T> {
|
||||
specifier: T,
|
||||
node: AstNodes<'a>,
|
||||
}
|
||||
|
||||
/// The specifiers between the braces of the import or export in the first field.
|
||||
pub(crate) struct FormatSpecifiers<'a, T>(pub(crate) Stmt<'a>, pub(crate) List<'a, T>);
|
||||
impl<T: Spanned> Spanned for FormatSpecifier<'_, T> {
|
||||
fn span(&self) -> Span {
|
||||
self.specifier.span()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Format<'a>> Format<'a> for FormatSpecifier<'a, T> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if !f.is_quiet()
|
||||
&& empty_line_before_comments_of_specifier_stays(f)
|
||||
&& let Some(first) = f.comments().of_node(self.node).leading.first()
|
||||
&& first.span().start < self.node.span().start
|
||||
&& crate::text::is_previous_line_empty(
|
||||
f.source_text().as_bytes(),
|
||||
first.span().start as usize,
|
||||
)
|
||||
{
|
||||
write!(f, empty_line());
|
||||
}
|
||||
write!(f, self.specifier);
|
||||
}
|
||||
}
|
||||
|
||||
/// The specifiers between the braces of an import or an export, and the variant of [`AstNodes`] that they are.
|
||||
pub(crate) struct FormatSpecifiers<'a, T>(pub(crate) List<'a, T>, pub(crate) fn(T) -> AstNodes<'a>);
|
||||
|
||||
impl<'a, T: Handle<'a> + Format<'a> + Spanned> Format<'a> for FormatSpecifiers<'a, T> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let FormatSpecifiers(statement, specifiers) = *self;
|
||||
let FormatSpecifiers(specifiers, as_node) = *self;
|
||||
let format_specifiers = format_with(|f| {
|
||||
let trailing_separator = FormatTrailingCommas::ES5.trailing_separator(f.options());
|
||||
let iter = specifiers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, specifier)| FormatSpecifier {
|
||||
specifier,
|
||||
statement,
|
||||
next_start: specifiers.get(index + 1).map(|next| next.span().start),
|
||||
});
|
||||
let specifiers = specifiers.iter().map(|specifier| FormatSpecifier {
|
||||
specifier,
|
||||
node: as_node(specifier),
|
||||
});
|
||||
f.join_with(soft_line_break_or_space()).entries(
|
||||
FormatSeparatedIter::new(iter, ",").with_trailing_separator(trailing_separator),
|
||||
FormatSeparatedIter::new(specifiers, ",")
|
||||
.with_trailing_separator(trailing_separator),
|
||||
);
|
||||
});
|
||||
let needs_space = f.options().bracket_spacing.value();
|
||||
@@ -247,109 +189,18 @@ impl<'a, T: Handle<'a> + Format<'a> + Spanned> Format<'a> for FormatSpecifiers<'
|
||||
}
|
||||
}
|
||||
|
||||
/// `import {⏎⏎// comment⏎a } from "b"`: oxfmt keeps the empty line, also behind the `{`.
|
||||
fn empty_line_before_comments_of_specifier_stays(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
struct FormatSpecifier<'a, T> {
|
||||
specifier: T,
|
||||
/// The import or export.
|
||||
statement: Stmt<'a>,
|
||||
/// Where the next specifier starts.
|
||||
next_start: Option<u32>,
|
||||
}
|
||||
|
||||
impl<T: Spanned> Spanned for FormatSpecifier<'_, T> {
|
||||
fn span(&self) -> Span {
|
||||
self.specifier.span()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Format<'a> + Spanned> Format<'a> for FormatSpecifier<'a, T> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return write!(f, self.specifier);
|
||||
}
|
||||
// What leads the source is hidden from the last specifier.
|
||||
let end = self.specifier.span().end;
|
||||
let source = self
|
||||
.statement
|
||||
.module_specifier_span()
|
||||
.filter(|_| self.next_start.is_none());
|
||||
let limit = source.and_then(|source| {
|
||||
let is_leading = |it: &&Comment| it.preceded_by_newline() && !it.followed_by_newline();
|
||||
Some(
|
||||
f.comments()
|
||||
.comments_in(self.specifier.span().between(source))
|
||||
.iter()
|
||||
.find(is_leading)?
|
||||
.span
|
||||
.start,
|
||||
)
|
||||
});
|
||||
let previous_limit = limit.map(|limit| f.comments_mut().limit_comments_up_to(limit));
|
||||
if empty_line_before_comments_of_specifier_stays(f)
|
||||
&& f.comments().has_comment_before(self.specifier.span().start)
|
||||
&& f.lines_before(self.specifier.span()) > 1
|
||||
{
|
||||
write!(f, empty_line());
|
||||
}
|
||||
write!(f, self.specifier);
|
||||
if let Some(previous_limit) = previous_limit {
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
}
|
||||
|
||||
// Prettier's `handleModuleSpecifiersComments`: a comment after a specifier that ends its
|
||||
// line trails the specifier, also if it is on a line of its own.
|
||||
let comments = match self.next_start {
|
||||
Some(next_start) => {
|
||||
let comments = f.comments().comments_before(next_start);
|
||||
let count = comments
|
||||
.iter()
|
||||
.rposition(|it| it.followed_by_newline())
|
||||
.map_or(0, |last| last + 1);
|
||||
comments.get(..count).unwrap_or_default()
|
||||
}
|
||||
None => comments_before_from(end, self.statement, f),
|
||||
};
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `handleModuleSpecifiersComments`: a comment in the specifier at this span that starts
|
||||
/// or ends its line goes before the specifier.
|
||||
pub(crate) struct FormatCommentsInSpecifier(pub(crate) Span);
|
||||
|
||||
impl<'a> Format<'a> for FormatCommentsInSpecifier {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let comments = f.comments().comments_before(self.0.end);
|
||||
let count = comments
|
||||
.iter()
|
||||
.rposition(|it| it.preceded_by_newline() || it.followed_by_newline());
|
||||
let comments = comments
|
||||
.get(..count.map_or(0, |last| last + 1))
|
||||
.unwrap_or_default();
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
|
||||
/// `a`, `a as b`, `type a`
|
||||
pub(crate) fn write_import_specifier<'a>(specifier: ImportSpec<'a>, f: &mut Formatter<'a>) {
|
||||
let node = AstNodes::ImportSpecifier(specifier);
|
||||
let kind = specifier
|
||||
.is_type_only()
|
||||
.then(|| super::flow::import_kind(specifier.span().start, f));
|
||||
write!(f, [FormatCommentsInSpecifier(specifier.span()), kind]);
|
||||
let local = identifier(specifier.local(), node);
|
||||
write!(f, kind);
|
||||
let local = identifier(specifier.local());
|
||||
match specifier.is_renamed() {
|
||||
true => write!(
|
||||
f,
|
||||
[
|
||||
module_export_name(specifier.imported(), node),
|
||||
module_export_name(specifier.imported()),
|
||||
space(),
|
||||
"as",
|
||||
space(),
|
||||
@@ -393,25 +244,14 @@ fn write_import_attributes<'a>(entries: List<'a, Prop<'a>>, braces: Span, f: &mu
|
||||
let Some(first) = entries.first() else {
|
||||
return write!(f, "{}");
|
||||
};
|
||||
let attribute_at = |index: usize, attribute: Prop<'a>| FormatImportAttribute {
|
||||
attribute,
|
||||
braces,
|
||||
next_start: entries.get(index + 1).map_or(0, |next| next.span().start),
|
||||
};
|
||||
let needs_space = f.options().bracket_spacing.value();
|
||||
|
||||
// Prettier's `isSingleTypeImportAttributes`
|
||||
let is_type = first.key().is_some_and(|key| key.is("type"))
|
||||
&& first
|
||||
.value()
|
||||
.is_some_and(|value| value.tag() == ExprTag::String);
|
||||
if entries.len() == 1 && is_type && !f.comments().has_comment_before(braces.end) {
|
||||
if entries.len() == 1 && is_type_attribute_without_comments(first, f) {
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
"{",
|
||||
maybe_space(needs_space),
|
||||
attribute_at(0, first),
|
||||
FormatImportAttribute(first),
|
||||
maybe_space(needs_space),
|
||||
"}"
|
||||
]
|
||||
@@ -420,10 +260,7 @@ fn write_import_attributes<'a>(entries: List<'a, Prop<'a>>, braces: Span, f: &mu
|
||||
|
||||
let format_attributes = format_with(|f| {
|
||||
let trailing_separator = FormatTrailingCommas::ES5.trailing_separator(f.options());
|
||||
let attributes = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, attribute)| attribute_at(index, attribute));
|
||||
let attributes = entries.iter().map(FormatImportAttribute);
|
||||
let mut previous_end = None;
|
||||
for attribute in
|
||||
FormatSeparatedIter::new(attributes, ",").with_trailing_separator(trailing_separator)
|
||||
@@ -451,60 +288,75 @@ fn write_import_attributes<'a>(entries: List<'a, Prop<'a>>, braces: Span, f: &mu
|
||||
);
|
||||
}
|
||||
|
||||
/// `type: "json"`
|
||||
struct FormatImportAttribute<'a> {
|
||||
attribute: Prop<'a>,
|
||||
/// The `{ .. }` that it is in.
|
||||
braces: Span,
|
||||
/// Where the next attribute starts, or 0.
|
||||
next_start: u32,
|
||||
/// Prettier's `isSingleTypeImportAttributes`: `type: "json"`
|
||||
fn is_type_attribute_without_comments<'a>(attribute: Prop<'a>, f: &Formatter<'a>) -> bool {
|
||||
let (Some(key), Some(value)) = (attribute.key(), attribute.value()) else {
|
||||
return false;
|
||||
};
|
||||
if !key.is("type") || value.tag() != ExprTag::String {
|
||||
return false;
|
||||
}
|
||||
if f.is_quiet() {
|
||||
return true;
|
||||
}
|
||||
let key_comments = f.comments().of_name(key.span(f.file()).start);
|
||||
key_comments.leading.is_empty()
|
||||
&& key_comments.trailing.is_empty()
|
||||
&& !f
|
||||
.comments()
|
||||
.has_comments(AstNodes::ObjectProperty(attribute))
|
||||
&& !f.comments().has_comments(value.as_ast_nodes())
|
||||
}
|
||||
|
||||
/// `type: "json"`
|
||||
#[derive(Copy, Clone)]
|
||||
struct FormatImportAttribute<'a>(Prop<'a>);
|
||||
|
||||
impl Spanned for FormatImportAttribute<'_> {
|
||||
fn span(&self) -> Span {
|
||||
self.attribute.span()
|
||||
self.0.span()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatImportAttribute<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let attribute = self.attribute;
|
||||
let (span, node) = (attribute.span(), AstNodes::ObjectProperty(attribute));
|
||||
write!(f, format_leading_comments(span));
|
||||
match attribute.key() {
|
||||
Some(key) if matches!(key.kind(), KeyKind::String(_)) => {
|
||||
let span = key.span(f.file());
|
||||
let string = f.source_text().text_for(&span);
|
||||
let format = FormatLiteralStringToken::new(
|
||||
string,
|
||||
false,
|
||||
StringLiteralParentKind::ImportAttribute,
|
||||
)
|
||||
.clean_text(f);
|
||||
format_node(span, || node, f, |f| write!(f, format));
|
||||
}
|
||||
Some(key) => write!(f, FormatKey::new(key, node)),
|
||||
None => {}
|
||||
let attribute = self.0;
|
||||
format_node(AstNodes::ObjectProperty(attribute), f, |f| {
|
||||
write_import_attribute(attribute, f)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn write_import_attribute<'a>(attribute: Prop<'a>, f: &mut Formatter<'a>) {
|
||||
match attribute.key() {
|
||||
Some(key) if matches!(key.kind(), KeyKind::String(_)) => {
|
||||
let span = key.span(f.file());
|
||||
let string = f.source_text().text_for(&span);
|
||||
let format = FormatLiteralStringToken::new(
|
||||
string,
|
||||
false,
|
||||
StringLiteralParentKind::ImportAttribute,
|
||||
)
|
||||
.clean_text(f);
|
||||
format_name(span, f, |f| write!(f, format));
|
||||
}
|
||||
write!(f, ":");
|
||||
Some(key) => write!(f, FormatKey::new(key, AstNodes::ObjectProperty(attribute))),
|
||||
None => {}
|
||||
}
|
||||
write!(f, ":");
|
||||
|
||||
if let Some(value) = attribute.value() {
|
||||
let breaks_after_colon = !f.is_quiet()
|
||||
&& (f.comments().comments_before_iter(value.span().start))
|
||||
.any(|comment| comment.followed_by_newline() || comment.is_indentable_block());
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(span.end);
|
||||
match breaks_after_colon {
|
||||
true => write!(
|
||||
f,
|
||||
group(&indent(&format_args!(soft_line_break_or_space(), value)))
|
||||
),
|
||||
false => write!(f, [space(), value]),
|
||||
}
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
}
|
||||
write!(
|
||||
let Some(value) = attribute.value() else {
|
||||
return;
|
||||
};
|
||||
// Prettier's `chooseLayout`: `break-after-operator`, or else `never-break-after-operator`.
|
||||
let breaks_after_colon = !f.is_quiet()
|
||||
&& (f.comments().of_node(value.as_ast_nodes()).leading.iter())
|
||||
.any(|comment| comment.has_newline_after() || comment.is_indentable_block());
|
||||
match breaks_after_colon {
|
||||
true => write!(
|
||||
f,
|
||||
format_trailing_comments(self.braces, span, self.next_start)
|
||||
);
|
||||
group(&indent(&format_args!(soft_line_break_or_space(), value)))
|
||||
),
|
||||
false => write!(f, [space(), value]),
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::js::utils::typescript::{is_object_like_type, union_leading_comments};
|
||||
use crate::js::utils::typescript::{
|
||||
has_leading_own_line_comment, is_object_like_type, without_lone_operator,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
/// `A & B`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_intersection_type<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
types: List<'a, TypeNode<'a>>,
|
||||
@@ -54,58 +57,44 @@ fn format_intersection_types<'a>(types: List<'a, TypeNode<'a>>, f: &mut Formatte
|
||||
|
||||
for (index, item) in types.iter().enumerate() {
|
||||
let is_object_like = is_object_like_type(item);
|
||||
let content = format_with(|f| {
|
||||
// The comments before a union are outside of its parentheses.
|
||||
// Not if the union is not formatted: then they are written with it.
|
||||
if matches!(item.kind(), TypeKind::Union(members) if members.len() > 1) {
|
||||
let comments = union_leading_comments(item, f).0;
|
||||
let is_suppressed = comments.iter().any(|comment| {
|
||||
f.comments().is_suppression_comment(comment) && !comment.preceded_by_newline()
|
||||
});
|
||||
if !is_suppressed {
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
write!(f, item);
|
||||
});
|
||||
let has_leading_own_line_comment =
|
||||
has_leading_own_line_comment(without_lone_operator(item).as_ast_nodes(), f);
|
||||
|
||||
if index == 0 {
|
||||
write!(f, content);
|
||||
write!(f, item);
|
||||
} else if is_prev_object_like
|
||||
&& is_object_like
|
||||
&& !(comment_above_an_object_type_moves_it_off_the_line(f)
|
||||
&& f.comments().has_leading_own_line_comment(item.span().start))
|
||||
&& has_leading_own_line_comment)
|
||||
{
|
||||
match is_chain_indented {
|
||||
true => write!(f, [" & ", indent(&content)]),
|
||||
false => write!(f, [" & ", content]),
|
||||
true => write!(f, [" & ", indent(&item)]),
|
||||
false => write!(f, [" & ", item]),
|
||||
}
|
||||
} else if !(is_prev_object_like || is_object_like)
|
||||
|| f.comments().has_leading_own_line_comment(item.span().start)
|
||||
{
|
||||
} else if !(is_prev_object_like || is_object_like) || has_leading_own_line_comment {
|
||||
match is_operator_at_start {
|
||||
true => {
|
||||
let comments_above = (own_line_comments_stay_above_operator(f)
|
||||
&& f.comments().has_leading_own_line_comment(item.span().start))
|
||||
.then(|| format_leading_comments(item.span()));
|
||||
&& has_leading_own_line_comment)
|
||||
.then(|| leading_comments(without_lone_operator(item).as_ast_nodes()));
|
||||
write!(
|
||||
f,
|
||||
indent(&format_args!(
|
||||
soft_line_break_or_space(),
|
||||
comments_above, "& ", content
|
||||
comments_above, "& ", item
|
||||
))
|
||||
)
|
||||
}
|
||||
false => write!(
|
||||
f,
|
||||
indent(&format_args!(" &", soft_line_break_or_space(), content))
|
||||
indent(&format_args!(" &", soft_line_break_or_space(), item))
|
||||
),
|
||||
}
|
||||
} else if index > 1 {
|
||||
is_chain_indented = true;
|
||||
write!(f, [" & ", indent(&content)]);
|
||||
write!(f, [" & ", indent(&item)]);
|
||||
} else {
|
||||
write!(f, [" & ", content]);
|
||||
write!(f, [" & ", item]);
|
||||
}
|
||||
is_prev_object_like = is_object_like;
|
||||
}
|
||||
|
||||
@@ -358,6 +358,27 @@ impl<'a> Children<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `{/* prettier-ignore */}`. `empty_expression`: what is in the braces.
|
||||
fn has_ignore_comment<'a>(empty_expression: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let comments = f.comments();
|
||||
(comments
|
||||
.of_node(AstNodes::JSXEmptyExpression(empty_expression))
|
||||
.dangling
|
||||
.iter())
|
||||
.any(|comment| comments.is_suppression_comment(comment))
|
||||
}
|
||||
|
||||
/// ```jsx
|
||||
/// {/* prettier-ignore */}
|
||||
///
|
||||
/// <b c />
|
||||
/// ```
|
||||
///
|
||||
/// Prettier copies the element. oxfmt formats it.
|
||||
fn empty_line_ends_ignore_comment(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Formats the children of `jsx`. `forced_break`: it is known that they are on lines of their own.
|
||||
pub(super) fn format_children<'a>(
|
||||
jsx: Jsx<'a>,
|
||||
@@ -407,8 +428,8 @@ pub(super) fn format_children<'a>(
|
||||
// For Prettier it is about an element or a fragment. For oxfmt about `{e}` too.
|
||||
let is_suppressed = is_after_ignore_comment
|
||||
&& (matches!(node.0.kind(), ExprKind::Jsx(_)) || children.is_oxfmt);
|
||||
is_after_ignore_comment = matches!(node.0.kind(), ExprKind::Missing)
|
||||
&& f.comments().is_suppressed(node.span().end);
|
||||
is_after_ignore_comment =
|
||||
matches!(node.0.kind(), ExprKind::Missing) && has_ignore_comment(node.0, f);
|
||||
let format_node = format_with(|f| match is_suppressed {
|
||||
true => FormatSuppressedNode(node.span()).fmt(f),
|
||||
false => node.fmt(f),
|
||||
@@ -457,7 +478,10 @@ pub(super) fn format_children<'a>(
|
||||
}
|
||||
match separator == Separator::HardLine && is_after_line_break {
|
||||
// The printer makes one line break of two in a row.
|
||||
true => multiline.write_separator(&empty_line(), f),
|
||||
true => {
|
||||
is_after_ignore_comment &= !empty_line_ends_ignore_comment(f);
|
||||
multiline.write_separator(&empty_line(), f);
|
||||
}
|
||||
false if is_mdx_block && separator == Separator::JsxWhitespace => {
|
||||
multiline.write_separator(&Separator::Line, f)
|
||||
}
|
||||
|
||||
+123
-124
@@ -1,8 +1,10 @@
|
||||
use super::child_list::{FormatChildrenResult, format_children};
|
||||
use super::opening_element::FormatOpeningElement;
|
||||
use super::{FormatJsxChild, write_jsx_closing_element, write_jsx_fragment_tag};
|
||||
use crate::js::format::write_trailing_comments_of;
|
||||
use crate::js::attach::Marker;
|
||||
use crate::js::format::format_node;
|
||||
use crate::js::parentheses::expression::needs_parentheses;
|
||||
use crate::js::print::class::dangling_comments_with_marker;
|
||||
use crate::js::utils::jsx::WrapState;
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::prelude::*;
|
||||
@@ -24,40 +26,22 @@ impl<'a> AnyJsxTagWithChildren<'a> {
|
||||
self.expr.ast_parent()
|
||||
}
|
||||
|
||||
fn format_trailing_comments(&self, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
/// The comments inside `<>`, which have no marker, or inside `</>`.
|
||||
fn comments_in_fragment_tag(&self, marker: Marker, f: &Formatter<'a>) -> CommentList<'a> {
|
||||
match f.is_quiet() {
|
||||
true => CommentList::empty(),
|
||||
false => dangling_comments_with_marker(self.expr.as_ast_nodes(), marker, f),
|
||||
}
|
||||
let parent = self.parent();
|
||||
let trailing_comments = if let AstNodes::ExpressionStatement(statement) = parent
|
||||
&& statement.is_arrow_function_body()
|
||||
{
|
||||
// All up to the end of the arrow function
|
||||
f.comments()
|
||||
.comments_before(parent.parent().parent().span().end)
|
||||
} else if let AstNodes::ConditionalExpression(conditional) = parent {
|
||||
match conditional.alternate() == Some(self.expr) {
|
||||
true => f.comments().comments_before(conditional.span().end),
|
||||
false => f.comments().end_of_line_comments_after(self.span().end),
|
||||
}
|
||||
} else if matches!(
|
||||
parent,
|
||||
AstNodes::TemplateLiteral(_) | AstNodes::TSTemplateLiteralType(_)
|
||||
) {
|
||||
f.comments()
|
||||
.comments_before_character(self.span().end, b'}')
|
||||
} else {
|
||||
return write_trailing_comments_of(self.expr.as_ast_nodes(), f);
|
||||
};
|
||||
FormatTrailingComments::Comments(trailing_comments).fmt(f);
|
||||
}
|
||||
|
||||
/// Prettier's `maybeWrapJsxElementInParens`.
|
||||
fn get_wrap_state(&self) -> WrapState {
|
||||
match self.parent() {
|
||||
/// Prettier's `isNoWrapParent`.
|
||||
fn get_wrap_state(&self, parent: AstNodes<'a>) -> WrapState {
|
||||
match parent {
|
||||
AstNodes::ArrayExpression(_)
|
||||
| AstNodes::JSXAttribute(_)
|
||||
| AstNodes::JSXElement(_)
|
||||
| AstNodes::JSXExpressionContainer(_)
|
||||
| AstNodes::JSXFragment(_)
|
||||
| AstNodes::ConditionalExpression(_)
|
||||
| AstNodes::CallExpression(_)
|
||||
| AstNodes::NewExpression(_)
|
||||
@@ -69,9 +53,32 @@ impl<'a> AnyJsxTagWithChildren<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `maybeWrapJsxElementInParens`.
|
||||
fn fmt_in_parentheses(&self, element: &impl Format<'a>, f: &mut Formatter<'a>) {
|
||||
let parent = self.parent();
|
||||
match self.get_wrap_state(parent) {
|
||||
WrapState::NoWrap => write!(f, element),
|
||||
WrapState::WrapOnBreak => {
|
||||
let needs_parentheses = needs_parentheses(self.expr, f);
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
(!needs_parentheses).then_some(if_group_breaks(&"(")),
|
||||
soft_block_indent(element),
|
||||
(!needs_parentheses).then_some(if_group_breaks(&")"))
|
||||
))
|
||||
.should_expand(should_expand(parent))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_opening(&self, f: &mut Formatter<'a>) {
|
||||
match self.jsx.is_fragment() {
|
||||
true => write_jsx_fragment_tag(self.jsx.opening_span(), false, f),
|
||||
true => {
|
||||
let comments = self.comments_in_fragment_tag(Marker::None, f);
|
||||
write_jsx_fragment_tag(self.jsx.opening_span(), comments, false, f);
|
||||
}
|
||||
false => FormatOpeningElement {
|
||||
expr: self.expr,
|
||||
jsx: self.jsx,
|
||||
@@ -82,8 +89,11 @@ impl<'a> AnyJsxTagWithChildren<'a> {
|
||||
|
||||
fn fmt_closing(&self, f: &mut Formatter<'a>) {
|
||||
match (self.jsx.close_tag(), self.jsx.closing_span()) {
|
||||
(Some(name), _) => write_jsx_closing_element(self.expr, name, f),
|
||||
(None, Some(span)) if self.jsx.is_fragment() => write_jsx_fragment_tag(span, true, f),
|
||||
(Some(name), _) => write_jsx_closing_element(name, f),
|
||||
(None, Some(tag)) if self.jsx.is_fragment() => {
|
||||
let comments = self.comments_in_fragment_tag(Marker::ClosingFragment, f);
|
||||
write_jsx_fragment_tag(tag, comments, true, f);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -109,108 +119,97 @@ impl<'a> AnyJsxTagWithChildren<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for AnyJsxTagWithChildren<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let is_suppressed = f.comments().is_suppressed(self.span().start);
|
||||
impl<'a> AnyJsxTagWithChildren<'a> {
|
||||
/// Prettier's `printJsxElementInternal`.
|
||||
fn fmt_tags_and_children(&self, f: &mut Formatter<'a>) {
|
||||
let format_opening = format_with(|f| self.fmt_opening(f));
|
||||
let format_closing = format_with(|f| self.fmt_closing(f));
|
||||
|
||||
let format_tag = format_with(|f| {
|
||||
if is_suppressed {
|
||||
return FormatSuppressedNode(self.span()).fmt(f);
|
||||
match self.layout() {
|
||||
ElementLayout::NoChildren => write!(f, [format_opening, format_closing]),
|
||||
ElementLayout::Template(expression) => {
|
||||
write!(
|
||||
f,
|
||||
[format_opening, FormatJsxChild(expression), format_closing]
|
||||
);
|
||||
}
|
||||
let format_opening = format_with(|f| self.fmt_opening(f));
|
||||
let format_closing = format_with(|f| self.fmt_closing(f));
|
||||
ElementLayout::Default => {
|
||||
let format_opening = format_opening.memoized();
|
||||
let opening_breaks = format_opening.inspect(f).will_break();
|
||||
let multiple_attributes = self.jsx.attrs().len() > 1;
|
||||
|
||||
match self.layout() {
|
||||
ElementLayout::NoChildren => write!(f, [format_opening, format_closing]),
|
||||
ElementLayout::Template(expression) => {
|
||||
write!(
|
||||
f,
|
||||
[format_opening, FormatJsxChild(expression), format_closing]
|
||||
);
|
||||
}
|
||||
ElementLayout::Default => {
|
||||
let format_opening = format_opening.memoized();
|
||||
let opening_breaks = format_opening.inspect(f).will_break();
|
||||
let multiple_attributes = self.jsx.attrs().len() > 1;
|
||||
|
||||
// Prettier's `isMdxBlock`
|
||||
let is_mdx_block = f.options().is_mdx_jsx && self.span().start == 0;
|
||||
match format_children(
|
||||
self.jsx,
|
||||
multiple_attributes || opening_breaks,
|
||||
is_mdx_block,
|
||||
f,
|
||||
) {
|
||||
FormatChildrenResult::ForceMultiline(children)
|
||||
| FormatChildrenResult::BestFitting {
|
||||
expanded_children: children,
|
||||
..
|
||||
} if is_mdx_block => children.fmt_content(f),
|
||||
FormatChildrenResult::SingleChild(child) => {
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(format_opening, child, format_closing))
|
||||
);
|
||||
}
|
||||
FormatChildrenResult::ForceMultiline(multiline) => {
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(format_opening, multiline, format_closing))
|
||||
);
|
||||
}
|
||||
FormatChildrenResult::BestFitting {
|
||||
flat_children,
|
||||
expanded_children,
|
||||
} => {
|
||||
let format_closing = format_closing.memoized();
|
||||
write!(
|
||||
f,
|
||||
best_fitting![
|
||||
group(&format_args!(
|
||||
format_opening,
|
||||
flat_children, format_closing
|
||||
)),
|
||||
group(&format_args!(
|
||||
format_opening,
|
||||
expanded_children, format_closing
|
||||
))
|
||||
]
|
||||
);
|
||||
}
|
||||
// Prettier's `isMdxBlock`
|
||||
let is_mdx_block = f.options().is_mdx_jsx && self.span().start == 0;
|
||||
match format_children(
|
||||
self.jsx,
|
||||
multiple_attributes || opening_breaks,
|
||||
is_mdx_block,
|
||||
f,
|
||||
) {
|
||||
FormatChildrenResult::ForceMultiline(children)
|
||||
| FormatChildrenResult::BestFitting {
|
||||
expanded_children: children,
|
||||
..
|
||||
} if is_mdx_block => children.fmt_content(f),
|
||||
FormatChildrenResult::SingleChild(child) => {
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(format_opening, child, format_closing))
|
||||
);
|
||||
}
|
||||
FormatChildrenResult::ForceMultiline(multiline) => {
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(format_opening, multiline, format_closing))
|
||||
);
|
||||
}
|
||||
FormatChildrenResult::BestFitting {
|
||||
flat_children,
|
||||
expanded_children,
|
||||
} => {
|
||||
let format_closing = format_closing.memoized();
|
||||
write!(
|
||||
f,
|
||||
best_fitting![
|
||||
group(&format_args!(format_opening, flat_children, format_closing)),
|
||||
group(&format_args!(
|
||||
format_opening,
|
||||
expanded_children, format_closing
|
||||
))
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let parent = self.parent();
|
||||
if matches!(parent, AstNodes::JSXElement(_) | AstNodes::JSXFragment(_)) {
|
||||
return write!(f, format_tag);
|
||||
}
|
||||
}
|
||||
|
||||
let format_with_comments = format_args!(
|
||||
format_leading_comments(self.span()),
|
||||
format_tag,
|
||||
format_with(|f| self.format_trailing_comments(f))
|
||||
/// The source text of an element that has a `prettier-ignore` comment, with the comments of the element.
|
||||
fn fmt_ignored(&self, f: &mut Formatter<'a>) {
|
||||
let skip_trailing_comments = f.context_mut().take_skip_trailing_comments();
|
||||
let comments = f.comments().of_node(self.expr.as_ast_nodes());
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatLeadingComments::Comments(comments.leading),
|
||||
FormatSuppressedNode(self.span()),
|
||||
(!skip_trailing_comments)
|
||||
.then_some(FormatTrailingComments::Comments(comments.trailing))
|
||||
]
|
||||
);
|
||||
match self.get_wrap_state() {
|
||||
// Prettier's `printIgnored` comes before `maybeWrapJsxElementInParens`.
|
||||
_ if is_suppressed && ignored_jsx_gets_no_parentheses(f) => {
|
||||
write!(f, format_with_comments);
|
||||
}
|
||||
WrapState::NoWrap => write!(f, format_with_comments),
|
||||
WrapState::WrapOnBreak => {
|
||||
let needs_parentheses = needs_parentheses(self.expr, f);
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
(!needs_parentheses).then_some(if_group_breaks(&"(")),
|
||||
soft_block_indent(&format_with_comments),
|
||||
(!needs_parentheses).then_some(if_group_breaks(&")"))
|
||||
))
|
||||
.should_expand(should_expand(parent))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for AnyJsxTagWithChildren<'a> {
|
||||
/// Prettier's `printJsxElement`: the comments of the element are inside the parentheses.
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let node = self.expr.as_ast_nodes();
|
||||
if !f.is_quiet() && f.comments().is_ignored(node) {
|
||||
return self.fmt_in_parentheses(&format_with(|f| self.fmt_ignored(f)), f);
|
||||
}
|
||||
let format_with_comments =
|
||||
format_with(|f| format_node(node, f, |f| self.fmt_tags_and_children(f)));
|
||||
self.fmt_in_parentheses(&format_with_comments, f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+175
-153
@@ -6,8 +6,10 @@ mod opening_element;
|
||||
|
||||
use self::element::AnyJsxTagWithChildren;
|
||||
pub(crate) use self::element::ignored_jsx_gets_no_parentheses;
|
||||
use crate::js::format::format_node;
|
||||
use crate::js::format::{ExprOptions, format_name, format_node, identifier, write_expression};
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::js::utils::tailwindcss::{InTailwindContext, context_of_attribute};
|
||||
use crate::js::utils::typecast::{comments_of_expression, has_comments_that_count, is_cast_target};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -16,107 +18,90 @@ pub(crate) fn write_jsx_element<'a>(e: Expr<'a>, jsx: Jsx<'a>, f: &mut Formatter
|
||||
AnyJsxTagWithChildren { expr: e, jsx }.fmt(f);
|
||||
}
|
||||
|
||||
/// The name in a tag: `a`, `a.b`, `a-b`, `a:b`. `parent`: the element.
|
||||
struct FormatJsxName<'a> {
|
||||
name: Expr<'a>,
|
||||
parent: AstNodes<'a>,
|
||||
}
|
||||
/// The name in a tag: `a`, `a.b`, `a-b`, `a:b`.
|
||||
struct FormatJsxName<'a>(Expr<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatJsxName<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let name = self.name;
|
||||
format_node(
|
||||
name.span(),
|
||||
|| self.parent,
|
||||
f,
|
||||
|f| match name.kind() {
|
||||
ExprKind::Dot { .. } if !f.context_mut().has_stack_left() => {}
|
||||
ExprKind::Dot {
|
||||
obj,
|
||||
name: property,
|
||||
..
|
||||
} => write!(
|
||||
f,
|
||||
[
|
||||
FormatJsxName {
|
||||
name: obj,
|
||||
parent: self.parent
|
||||
},
|
||||
".",
|
||||
source_text(property.span())
|
||||
]
|
||||
),
|
||||
_ => write_jsx_identifier(name.span(), f),
|
||||
},
|
||||
);
|
||||
let name = self.0;
|
||||
format_node(name.as_ast_nodes(), f, |f| match name.kind() {
|
||||
// The object is not written by way of `Expr`.
|
||||
ExprKind::Dot { .. } if !f.context_mut().has_stack_left() => {}
|
||||
ExprKind::Dot {
|
||||
obj,
|
||||
name: property,
|
||||
..
|
||||
} => write!(f, [FormatJsxName(obj), ".", identifier(property)]),
|
||||
_ => write_jsx_identifier(name.span(), f),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// `a`, `a-b`, `a:b`
|
||||
/// `a`, `a-b`, `a:b`, with the comments of the names.
|
||||
fn write_jsx_identifier<'a>(span: Span, f: &mut Formatter<'a>) {
|
||||
let text = f.source_text().text_for(&span);
|
||||
let is_part =
|
||||
|b: &&u8| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'$' | b'-') || **b >= 0x80;
|
||||
let namespace_len = text.iter().take_while(is_part).count() as u32;
|
||||
if namespace_len == span.len() {
|
||||
return write!(f, text_without_whitespace(text));
|
||||
return format_name(span, f, |f| write!(f, text_without_whitespace(text)));
|
||||
}
|
||||
let namespace = Span::new(span.start, span.start + namespace_len);
|
||||
let name = Span::new(
|
||||
span.end - text.iter().rev().take_while(is_part).count() as u32,
|
||||
span.end,
|
||||
);
|
||||
let comments_before_colon = f.comments().comments_before_character(namespace.end, b':');
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
source_text(namespace),
|
||||
FormatTrailingComments::Comments(comments_before_colon),
|
||||
":"
|
||||
]
|
||||
);
|
||||
let comments_after_colon = f.comments().comments_before(name.start);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatLeadingComments::Comments(comments_after_colon),
|
||||
source_text(name)
|
||||
]
|
||||
);
|
||||
format_name(namespace, f, |f| write!(f, source_text(namespace)));
|
||||
write!(f, ":");
|
||||
format_name(name, f, |f| write!(f, source_text(name)));
|
||||
}
|
||||
|
||||
/// `</a>`
|
||||
fn write_jsx_closing_element<'a>(e: Expr<'a>, name: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let leading_comments = f.comments().comments_before(name.span().start);
|
||||
let name_has_leading_comment = !leading_comments.is_empty();
|
||||
let name_has_own_line_leading_comment = leading_comments.iter().any(|it| it.is_line());
|
||||
let name = FormatJsxName {
|
||||
name,
|
||||
parent: AstNodes::JSXElement(e),
|
||||
};
|
||||
|
||||
/// `</a>`. Prettier's `printJsxClosingElement`.
|
||||
fn write_jsx_closing_element<'a>(name: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return write!(f, ["</", FormatJsxName(name), ">"]);
|
||||
}
|
||||
let leading_comments = f.comments().of_node(name.as_ast_nodes()).leading;
|
||||
let name = FormatJsxName(name);
|
||||
write!(f, "</");
|
||||
if name_has_own_line_leading_comment {
|
||||
if leading_comments.iter().any(|comment| comment.is_line()) {
|
||||
write!(
|
||||
f,
|
||||
[hard_line_break(), block_indent(&name), hard_line_break()]
|
||||
[
|
||||
indent(&format_args!(hard_line_break(), name)),
|
||||
hard_line_break()
|
||||
]
|
||||
);
|
||||
} else {
|
||||
write!(f, [name_has_leading_comment.then_some(space()), name]);
|
||||
write!(f, [(!leading_comments.is_empty()).then_some(space()), name]);
|
||||
}
|
||||
write!(f, ">");
|
||||
}
|
||||
|
||||
/// `<>` and `</>`. `span`: of the tag.
|
||||
fn write_jsx_fragment_tag<'a>(span: Span, is_closing: bool, f: &mut Formatter<'a>) {
|
||||
/// `< /* prettier-ignore */ >` stays as it is for Prettier. oxfmt prints `</* prettier-ignore */>`.
|
||||
fn ignore_comment_applies_to_fragment_tag(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `<>` and `</>`, which is at `tag`, with the `comments` inside the tag. Prettier's `printJsxOpeningClosingFragment`.
|
||||
fn write_jsx_fragment_tag<'a>(
|
||||
tag: Span,
|
||||
comments: CommentList<'a>,
|
||||
is_closing: bool,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let open = if is_closing { "</" } else { "<" };
|
||||
let comments = f.comments().comments_before(span.end);
|
||||
if comments.is_empty() {
|
||||
return write!(f, [open, ">"]);
|
||||
}
|
||||
let has_own_line_comment = comments.iter().any(|c| c.is_line());
|
||||
if ignore_comment_applies_to_fragment_tag(f)
|
||||
&& (comments.iter()).any(|comment| f.comments().is_suppression_comment(comment))
|
||||
{
|
||||
return write!(f, FormatSuppressedNode(tag));
|
||||
}
|
||||
let has_line_comment = comments.iter().any(|comment| comment.is_line());
|
||||
let format_comments = format_with(|f| {
|
||||
if has_own_line_comment {
|
||||
if has_line_comment {
|
||||
write!(f, hard_line_break());
|
||||
} else if is_closing {
|
||||
write!(f, space());
|
||||
@@ -134,7 +119,7 @@ fn write_jsx_fragment_tag<'a>(span: Span, is_closing: bool, f: &mut Formatter<'a
|
||||
[
|
||||
open,
|
||||
indent(&format_comments),
|
||||
has_own_line_comment.then_some(hard_line_break()),
|
||||
has_line_comment.then_some(hard_line_break()),
|
||||
">"
|
||||
]
|
||||
);
|
||||
@@ -147,26 +132,20 @@ pub(crate) struct FormatJsxChild<'a>(pub(crate) Expr<'a>);
|
||||
impl<'a> Format<'a> for FormatJsxChild<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let e = self.0;
|
||||
let Some(span) = e.jsx_container_span() else {
|
||||
if e.jsx_container_span().is_none() {
|
||||
return e.fmt(f);
|
||||
};
|
||||
}
|
||||
match e.kind() {
|
||||
ExprKind::Spread(argument) => {
|
||||
format_node(
|
||||
span,
|
||||
|| AstNodes::JSXSpreadChild(e).parent(),
|
||||
f,
|
||||
|f| write_jsx_spread(argument, span, f),
|
||||
);
|
||||
format_node(AstNodes::JSXSpreadChild(e), f, |f| {
|
||||
write_jsx_spread(argument, f)
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
let node = AstNodes::JSXExpressionContainer(e);
|
||||
format_node(
|
||||
span,
|
||||
|| node.parent(),
|
||||
f,
|
||||
|f| write_jsx_expression_container(e, span, node.parent(), f),
|
||||
);
|
||||
format_node(node, f, |f| {
|
||||
write_jsx_expression_container(e, node.parent(), f)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,73 +157,64 @@ impl Spanned for FormatJsxChild<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `{e}`. `span`: of the braces.
|
||||
/// `{e}`. Prettier's `printJsxExpressionContainer`.
|
||||
fn write_jsx_expression_container<'a>(
|
||||
expression: Expr<'a>,
|
||||
span: Span,
|
||||
parent: AstNodes<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if matches!(expression.kind(), ExprKind::Missing) {
|
||||
// `{/* comment */}`
|
||||
let comments = f.comments().comments_before(span.end);
|
||||
let has_line_comment = comments.iter().any(|c| c.is_line());
|
||||
let indent = if has_line_comment {
|
||||
DanglingIndentMode::Block
|
||||
} else {
|
||||
DanglingIndentMode::None
|
||||
};
|
||||
return write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
"{",
|
||||
FormatDanglingComments::Comments { comments, indent },
|
||||
FormatJsxEmptyExpression(expression),
|
||||
line_suffix_boundary(),
|
||||
"}"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
let has_comment = !f.is_quiet()
|
||||
&& (f.comments().has_comment_before(expression.span().start)
|
||||
|| f.comments()
|
||||
.has_comment_in_range(expression.span().end, span.end)
|
||||
|| (has_only_comments(expression, f) && comments_alone_in_braces_are_not_hugged(f)));
|
||||
let is_child = matches!(parent, AstNodes::JSXElement(_) | AstNodes::JSXFragment(_));
|
||||
if !has_comment && should_inline_jsx_expression(expression, is_child) {
|
||||
return write!(
|
||||
match should_inline_jsx_expression(expression, is_child, f) {
|
||||
true => write!(
|
||||
f,
|
||||
group(&format_args!("{", expression, line_suffix_boundary(), "}"))
|
||||
);
|
||||
),
|
||||
false => write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
"{",
|
||||
soft_block_indent(&expression),
|
||||
line_suffix_boundary(),
|
||||
"}"
|
||||
))
|
||||
),
|
||||
}
|
||||
|
||||
let format_with_comments = format_with(|f| {
|
||||
write!(f, expression);
|
||||
let comments = f.comments().comments_before(span.end);
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
});
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
"{",
|
||||
soft_block_indent(&format_with_comments),
|
||||
line_suffix_boundary(),
|
||||
"}"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/// `{}`, `[]` or the `()` of a call with nothing but comments in it, which are comments of `expression` for Prettier.
|
||||
fn has_only_comments<'a>(expression: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let empty = match expression.kind() {
|
||||
ExprKind::Object(properties) if properties.is_empty() => expression.span(),
|
||||
ExprKind::Array(elements) if elements.is_empty() => expression.span(),
|
||||
ExprKind::Call(call) | ExprKind::New(call) if call.args().is_empty() => {
|
||||
Span::after(call.callee().outer_span(), expression.span().end)
|
||||
/// What is between the braces of `{/* comment */}`. Prettier's `printJsxEmptyExpression`.
|
||||
struct FormatJsxEmptyExpression<'a>(Expr<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatJsxEmptyExpression<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let comments = (f.comments()).of_node(AstNodes::JSXEmptyExpression(self.0));
|
||||
// `{ /* prettier-ignore */ }`: for Prettier the comment also applies to the `JSXEmptyExpression` that owns it, which
|
||||
// is everything between the braces, so the blanks stay.
|
||||
if comments.is_ignored
|
||||
&& let Some(braces) = self.0.jsx_container_span()
|
||||
{
|
||||
return write!(
|
||||
f,
|
||||
FormatSuppressedNode(Span::new(braces.start + 1, braces.end - 1))
|
||||
);
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
f.comments().has_comment_in_span(empty)
|
||||
let comments = comments.dangling;
|
||||
let indent = match comments.iter().any(|comment| comment.is_line()) {
|
||||
true => DanglingIndentMode::Block,
|
||||
false => DanglingIndentMode::None,
|
||||
};
|
||||
write!(f, FormatDanglingComments::Comments { comments, indent });
|
||||
}
|
||||
}
|
||||
|
||||
/// ```jsx
|
||||
@@ -257,16 +227,23 @@ fn has_only_comments<'a>(expression: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
/// />
|
||||
/// ```
|
||||
///
|
||||
/// Prettier on the left, oxfmt on the right.
|
||||
fn comments_alone_in_braces_are_not_hugged(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
/// Prettier on the left, oxfmt on the right: see [`has_comments_that_count`].
|
||||
fn has_comments_in_braces<'a>(expression: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
!f.is_quiet() && has_comments_that_count(expression, f)
|
||||
}
|
||||
|
||||
/// Prettier's `shouldInline` in `printJsxExpressionContainer`: it starts right after the `{` and
|
||||
/// ends right before the `}`, even if it breaks. `is_child`: the braces are among the children of
|
||||
/// an element.
|
||||
fn should_inline_jsx_expression(mut expression: Expr<'_>, mut is_child: bool) -> bool {
|
||||
fn should_inline_jsx_expression<'a>(
|
||||
mut expression: Expr<'a>,
|
||||
mut is_child: bool,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
loop {
|
||||
if has_comments_in_braces(expression, f) || is_cast_target(expression, f) {
|
||||
return false;
|
||||
}
|
||||
match expression.as_ast_nodes() {
|
||||
AstNodes::ArrayExpression(_)
|
||||
| AstNodes::ObjectExpression(_)
|
||||
@@ -301,46 +278,91 @@ fn should_inline_jsx_expression(mut expression: Expr<'_>, mut is_child: bool) ->
|
||||
pub(crate) fn write_jsx_attribute<'a>(attribute: Prop<'a>, f: &mut Formatter<'a>) {
|
||||
if attribute.kind() == PropKind::Spread {
|
||||
if let Some(argument) = attribute.value() {
|
||||
write_jsx_spread(argument, attribute.span(), f);
|
||||
write_jsx_spread(argument, f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(key) = attribute.key() {
|
||||
let span = key.span(f.file());
|
||||
format_node(
|
||||
span,
|
||||
|| AstNodes::JSXAttribute(attribute),
|
||||
f,
|
||||
|f| write_jsx_identifier(span, f),
|
||||
);
|
||||
write_jsx_identifier(span, f);
|
||||
}
|
||||
if let Some(value) = attribute.value() {
|
||||
let context = attribute
|
||||
.key()
|
||||
.and_then(|key| context_of_attribute(f.source_text().text_for(&key.span(f.file())), f));
|
||||
write!(f, ["=", InTailwindContext(context, FormatJsxChild(value))]);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
"=",
|
||||
InTailwindContext(context, FormatJsxAttributeValue(value))
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `{...argument}`. `span`: of the braces.
|
||||
fn write_jsx_spread<'a>(argument: Expr<'a>, span: Span, f: &mut Formatter<'a>) {
|
||||
let has_comment = !f.is_quiet()
|
||||
&& (f.comments().has_comment_before(argument.span().start)
|
||||
|| f.comments()
|
||||
.has_comment_in_range(argument.span().end, span.end));
|
||||
let format_inner = format_args!(format_leading_comments(argument.span()), "...", argument);
|
||||
write!(f, "{");
|
||||
match has_comment {
|
||||
true => write!(f, soft_block_indent(&format_inner)),
|
||||
false => write!(f, format_inner),
|
||||
/// `a=/* prettier-ignore */ 'b'` is `a=/* prettier-ignore */ "b"` for Prettier: `printJsxAttribute` writes a string itself, ignored or
|
||||
/// not. oxfmt copies it.
|
||||
fn ignored_string_of_attribute_is_formatted(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `"b"`, `{b}` or `<b />` after the `=` of an attribute.
|
||||
struct FormatJsxAttributeValue<'a>(Expr<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatJsxAttributeValue<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let value = self.0;
|
||||
if f.is_quiet() || value.jsx_container_span().is_some() {
|
||||
return FormatJsxChild(value).fmt(f);
|
||||
}
|
||||
let node = value.as_ast_nodes();
|
||||
if !matches!(node, AstNodes::StringLiteral(_))
|
||||
|| !f.comments().is_ignored(node)
|
||||
|| !ignored_string_of_attribute_is_formatted(f)
|
||||
{
|
||||
return FormatJsxChild(value).fmt(f);
|
||||
}
|
||||
write!(f, leading_comments(node));
|
||||
write_expression(value, ExprOptions::None, f);
|
||||
write!(f, trailing_comments(node));
|
||||
}
|
||||
write!(f, "}");
|
||||
}
|
||||
|
||||
/// `{...argument}`. Prettier's `printJsxSpreadAttributeOrChild`: the comments of the argument are written before the dots.
|
||||
fn write_jsx_spread<'a>(argument: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
if !has_comments_in_braces(argument, f) {
|
||||
return write!(f, ["{...", argument, "}"]);
|
||||
}
|
||||
let comments = comments_of_expression(argument, f);
|
||||
// `{⏎// prettier-ignore⏎...a⏎// comment⏎.b}`: a leading comment can be inside the argument. An ignored argument is copied
|
||||
// with it.
|
||||
let leading_comments = match comments.is_ignored {
|
||||
true => {
|
||||
let count_before_argument = (comments.leading.iter())
|
||||
.take_while(|comment| comment.span().start < argument.span().start)
|
||||
.count();
|
||||
comments.leading.slice(0, count_before_argument)
|
||||
}
|
||||
false => comments.leading,
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
"{",
|
||||
soft_block_indent(&format_args!(
|
||||
FormatLeadingComments::Comments(leading_comments),
|
||||
"...", argument
|
||||
)),
|
||||
"}"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// `{...e}` among the children of an element. `e`: the `Spread`.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_jsx_spread_child<'a>(e: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
if let ExprKind::Spread(argument) = e.kind() {
|
||||
write_jsx_spread(argument, AstNodes::JSXSpreadChild(e).span(), f);
|
||||
write_jsx_spread(argument, f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,30 +25,57 @@ fn has_line_break<'a>(string: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
&& !loses_line_breaks(string, f)
|
||||
}
|
||||
|
||||
/// ```jsx
|
||||
/// <// comment <// comment
|
||||
/// a a />
|
||||
/// />
|
||||
/// ```
|
||||
///
|
||||
/// Prettier on the left: any comment of the name or the type arguments counts. oxfmt on the right.
|
||||
fn only_comment_after_name_counts(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
impl<'a> FormatOpeningElement<'a> {
|
||||
/// `<a b /⏎// comment⏎>`. Only for oxfmt: see `handle_comment_after_slash_of_jsx_tag_for_oxfmt`.
|
||||
fn comments_after_slash(&self, f: &Formatter<'a>) -> CommentList<'a> {
|
||||
(f.comments())
|
||||
.of_node(AstNodes::JSXOpeningElement(self.expr))
|
||||
.dangling
|
||||
}
|
||||
|
||||
fn compute_layout(&self, f: &Formatter<'a>) -> OpeningElementLayout {
|
||||
let attributes = self.jsx.attrs();
|
||||
let span = self.jsx.opening_span();
|
||||
let comments = f.comments();
|
||||
let (mut name_has_comments, mut last_attribute_has_trailing_comments) = (false, false);
|
||||
let mut only_attribute_has_comments = false;
|
||||
if !f.is_quiet() {
|
||||
let type_arguments = AstNodes::TSTypeParameterInstantiation(Node::Expr(self.expr));
|
||||
let name = self.jsx.tag().map(|name| name.as_ast_nodes());
|
||||
name_has_comments = match only_comment_after_name_counts(f) {
|
||||
true => (!self.jsx.type_args().is_empty())
|
||||
.then_some(type_arguments)
|
||||
.or(name)
|
||||
.is_some_and(|last| !comments.of_node(last).trailing.is_empty()),
|
||||
false => {
|
||||
name.is_some_and(|name| comments.has_comments(name))
|
||||
|| comments.has_comments(type_arguments)
|
||||
}
|
||||
};
|
||||
if let Some(last) = attributes.last() {
|
||||
let comments = comments.of_node(last.as_ast_nodes());
|
||||
let has_comments_after_slash = !self.comments_after_slash(f).is_empty();
|
||||
last_attribute_has_trailing_comments =
|
||||
!comments.trailing.is_empty() || has_comments_after_slash;
|
||||
only_attribute_has_comments = !comments.is_empty() || has_comments_after_slash;
|
||||
}
|
||||
}
|
||||
|
||||
let last_attribute_has_comment = attributes
|
||||
.last()
|
||||
.is_some_and(|a| comments.has_comment_in_range(a.span().end, span.end));
|
||||
let type_arguments_or_name_end = (self.jsx.type_args().angle_brackets_span())
|
||||
.or_else(|| self.jsx.tag().map(|it| it.span()))
|
||||
.map_or(span.start, |it| it.end);
|
||||
let first_attribute_start_or_element_end =
|
||||
attributes.first().map_or(span.end, |a| a.span().start);
|
||||
let name_has_comment = comments.has_comment_in_range(
|
||||
type_arguments_or_name_end,
|
||||
first_attribute_start_or_element_end,
|
||||
);
|
||||
|
||||
if self.jsx.is_self_closing() && attributes.is_empty() && !name_has_comment {
|
||||
if self.jsx.is_self_closing() && attributes.is_empty() && !name_has_comments {
|
||||
OpeningElementLayout::Inline
|
||||
} else if attributes.len() == 1
|
||||
&& !name_has_comment
|
||||
&& !last_attribute_has_comment
|
||||
&& !name_has_comments
|
||||
&& !only_attribute_has_comments
|
||||
&& attributes
|
||||
.first()
|
||||
.and_then(as_string_literal_attribute_value)
|
||||
@@ -57,8 +84,8 @@ impl<'a> FormatOpeningElement<'a> {
|
||||
OpeningElementLayout::SingleStringAttribute
|
||||
} else {
|
||||
OpeningElementLayout::IndentAttributes {
|
||||
name_has_comment,
|
||||
last_attribute_has_comment,
|
||||
name_has_comments,
|
||||
last_attribute_has_trailing_comments,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,10 +99,7 @@ impl<'a> Format<'a> for FormatOpeningElement<'a> {
|
||||
|
||||
let format_open = format_args!(
|
||||
"<",
|
||||
jsx.tag().map(|name| FormatJsxName {
|
||||
name,
|
||||
parent: AstNodes::JSXOpeningElement(e)
|
||||
}),
|
||||
jsx.tag().map(FormatJsxName),
|
||||
type_arguments(jsx.type_args(), Node::Expr(e))
|
||||
);
|
||||
let format_close = format_args!(is_self_closing.then_some("/"), ">");
|
||||
@@ -119,27 +143,31 @@ impl<'a> Format<'a> for FormatOpeningElement<'a> {
|
||||
);
|
||||
}
|
||||
OpeningElementLayout::IndentAttributes {
|
||||
name_has_comment,
|
||||
last_attribute_has_comment,
|
||||
name_has_comments,
|
||||
last_attribute_has_trailing_comments,
|
||||
} => {
|
||||
let format_inner = format_with(|f| {
|
||||
write!(f, format_open);
|
||||
if !attributes.is_empty() {
|
||||
write!(f, soft_line_indent_or_space(&format_attributes));
|
||||
}
|
||||
let comments = f.comments().comments_before(jsx.opening_span().end);
|
||||
FormatTrailingComments::Comments(comments).fmt(f);
|
||||
|
||||
let force_bracket_same_line = f.options().bracket_same_line.value();
|
||||
let wants_bracket_same_line = attributes.is_empty() && !name_has_comment;
|
||||
|
||||
if !f.is_quiet() {
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(self.comments_after_slash(f))
|
||||
);
|
||||
}
|
||||
// Prettier's `shouldPrintBracketSameLine`
|
||||
let bracket_same_line = match attributes.is_empty() {
|
||||
true => !name_has_comments,
|
||||
false => {
|
||||
f.options().bracket_same_line.value()
|
||||
&& !last_attribute_has_trailing_comments
|
||||
}
|
||||
};
|
||||
if is_self_closing {
|
||||
write!(f, [soft_line_break_or_space(), format_close]);
|
||||
} else if last_attribute_has_comment {
|
||||
write!(f, [soft_line_break(), format_close]);
|
||||
} else if (force_bracket_same_line && !attributes.is_empty())
|
||||
|| wants_bracket_same_line
|
||||
{
|
||||
} else if bracket_same_line {
|
||||
write!(f, format_close);
|
||||
} else {
|
||||
write!(f, [soft_line_break(), format_close]);
|
||||
@@ -167,7 +195,7 @@ enum OpeningElementLayout {
|
||||
SingleStringAttribute,
|
||||
/// Each attribute is on its own line if they do not fit on one.
|
||||
IndentAttributes {
|
||||
name_has_comment: bool,
|
||||
last_attribute_has_comment: bool,
|
||||
name_has_comments: bool,
|
||||
last_attribute_has_trailing_comments: bool,
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use crate::js::format::identifier;
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -17,9 +16,6 @@ pub(crate) fn write_ts_mapped_type<'a>(
|
||||
) {
|
||||
let param = mapped.param();
|
||||
let key = param.name();
|
||||
if f.comments().is_suppressed(key.start()) {
|
||||
return write!(f, FormatSuppressedNode(ty.span()));
|
||||
}
|
||||
// One that has a line break after its `{` in the source stays broken.
|
||||
let should_expand = f.options().expand == Expand::Auto
|
||||
&& f.source_text()
|
||||
@@ -27,11 +23,8 @@ pub(crate) fn write_ts_mapped_type<'a>(
|
||||
|
||||
let format_inner = format_with(|f| {
|
||||
if !f.is_quiet() {
|
||||
write_comments_before_bracket(
|
||||
f.comments()
|
||||
.comments_before_character(ty.span().start, b'['),
|
||||
f,
|
||||
);
|
||||
let dangling_comments = f.comments().of_node(AstNodes::TSMappedType(ty)).dangling;
|
||||
write_comments_before_bracket(dangling_comments, f);
|
||||
}
|
||||
match mapped.readonly() {
|
||||
MappedModifier::None => {}
|
||||
@@ -50,17 +43,14 @@ pub(crate) fn write_ts_mapped_type<'a>(
|
||||
// The blank after `in` stays if a comment that trails the key ends up behind it.
|
||||
let comment_brings_its_blank = one_blank_before_line_comment_behind_in(f)
|
||||
&& !f.is_quiet()
|
||||
&& param.constraint().is_some_and(|constraint| {
|
||||
matches!(constraint.kind(), TypeKind::Union(_))
|
||||
&& f.comments()
|
||||
.comments_before(constraint.span().start)
|
||||
.iter()
|
||||
.any(|comment| comment.followed_by_newline())
|
||||
});
|
||||
&& (param.constraint())
|
||||
.is_some_and(|constraint| matches!(constraint.kind(), TypeKind::Union(_)))
|
||||
&& (f.comments().of_name(key.span().start).trailing.iter())
|
||||
.any(|comment| comment.has_newline_after());
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
identifier(key, AstNodes::TSMappedType(ty)),
|
||||
identifier(key),
|
||||
match comment_brings_its_blank {
|
||||
true => " in",
|
||||
false => " in ",
|
||||
@@ -108,17 +98,22 @@ pub(crate) fn write_ts_mapped_type<'a>(
|
||||
|
||||
/// The comments between the `{` and the `[`, each on its own line. The last one stays on the line
|
||||
/// of the `[` if it is there in the source and fits.
|
||||
fn write_comments_before_bracket<'a>(comments: &'a [Comment], f: &mut Formatter<'a>) {
|
||||
let Some((last, others)) = comments.split_last() else {
|
||||
return;
|
||||
};
|
||||
for comment in others {
|
||||
f.comments_mut().increment_printed_count();
|
||||
write!(f, [comment, hard_line_break()]);
|
||||
}
|
||||
f.comments_mut().increment_printed_count();
|
||||
match last.followed_by_newline() {
|
||||
true => write!(f, [last, hard_line_break()]),
|
||||
false => write!(f, group(&format_args!(last, soft_line_break_or_space()))),
|
||||
fn write_comments_before_bracket<'a>(comments: CommentList<'a>, f: &mut Formatter<'a>) {
|
||||
let mut comments = comments.iter().peekable();
|
||||
while let Some(comment) = comments.next() {
|
||||
f.comments_mut().mark_printed(comment);
|
||||
match comments.peek() {
|
||||
// Prettier's `mergeNestledJsdocComments`: `/** .. *//** .. */` is one comment.
|
||||
Some(next)
|
||||
if next.span().start == comment.span().end
|
||||
&& comment.is_indentable_block()
|
||||
&& next.is_indentable_block() =>
|
||||
{
|
||||
write!(f, comment)
|
||||
}
|
||||
Some(_) => write!(f, [comment, hard_line_break()]),
|
||||
None if comment.has_newline_after() => write!(f, [comment, hard_line_break()]),
|
||||
None => write!(f, group(&format_args!(comment, soft_line_break_or_space()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,48 +32,85 @@ fn boundary_behind_object(f: &Formatter<'_>) -> Option<LineSuffixBoundary> {
|
||||
fn write_static_member_expression<'a>(e: Expr<'a>, object: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let start = f.elements().len();
|
||||
write!(f, [object, boundary_behind_object(f)]);
|
||||
write_property_after_object(e, object, start, f);
|
||||
}
|
||||
|
||||
if f.is_quiet() {
|
||||
let lookup = format_with(|f| write_lookup_without_comments(e, f));
|
||||
return match should_inline(e, object, start, f) {
|
||||
true => write!(f, lookup),
|
||||
false => write!(f, group(&indent(&format_args!(soft_line_break(), lookup)))),
|
||||
};
|
||||
}
|
||||
|
||||
let Some(property) = e.member_name() else {
|
||||
return;
|
||||
};
|
||||
let operator = if e.is_optional() { "?." } else { "." };
|
||||
let property_start = property.start();
|
||||
let has_own_line_comment = f.comments().has_leading_own_line_comment(property_start);
|
||||
let is_inline = !has_own_line_comment && should_inline(e, object, start, f);
|
||||
let property = identifier(property, e.as_chain_element());
|
||||
|
||||
if is_inline {
|
||||
return write!(f, [operator, property]);
|
||||
/// Not inlined: `a.b.c.d` is nested as deep as it is long, and what this needs of the stack would be taken at each level.
|
||||
#[inline(never)]
|
||||
fn write_property_after_object<'a>(
|
||||
e: Expr<'a>,
|
||||
object: Expr<'a>,
|
||||
start: usize,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let comments = comments_on_lines_before_property(e, f);
|
||||
let lookup = format_with(|f| match comments.is_empty() {
|
||||
true => write_lookup(e, f),
|
||||
false => write_lookup_after_its_comments(e, f),
|
||||
});
|
||||
if comments.is_empty() && should_inline(e, object, start, f) {
|
||||
return write!(f, lookup);
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
group(&indent(&format_args!(
|
||||
soft_line_break(),
|
||||
format_with(|f| {
|
||||
if has_own_line_comment {
|
||||
let comments = f.comments().comments_before(property_start);
|
||||
write!(
|
||||
f,
|
||||
[FormatLeadingComments::Comments(comments), soft_line_break()]
|
||||
);
|
||||
}
|
||||
}),
|
||||
operator,
|
||||
property,
|
||||
FormatLeadingComments::Comments(comments),
|
||||
(!comments.is_empty()).then_some(soft_line_break()),
|
||||
lookup
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
/// Prettier attaches a comment on its own line in `a.b` or `a[b]` to the member access, so it is printed before `a`. oxfmt
|
||||
/// leaves it where it is.
|
||||
///
|
||||
/// ```js
|
||||
/// a // comment
|
||||
/// // comment a.b;
|
||||
/// .b;
|
||||
/// ```
|
||||
pub(crate) fn comments_in_member_access_stay(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// See [`comments_in_member_access_stay`]: the comments of the property of `member`, if one of them ends its line. They are
|
||||
/// printed before the `.`. Not for `a.#b`, which oxfmt never breaks.
|
||||
fn comments_on_lines_before_property<'a>(member: Expr<'a>, f: &Formatter<'a>) -> CommentList<'a> {
|
||||
if f.is_quiet() || !comments_in_member_access_stay(f) || member.is_private_member() {
|
||||
return CommentList::empty();
|
||||
}
|
||||
(member.member_name_start())
|
||||
.map(|start| f.comments().of_name(start).leading)
|
||||
.filter(|comments| comments.iter().any(|comment| comment.has_newline_after()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The `.b` or `?.b` of `member`. Prettier's `printMemberLookup`.
|
||||
pub(crate) fn write_lookup<'a>(member: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return write_lookup_without_comments(member, f);
|
||||
}
|
||||
let Some(property) = member.member_name() else {
|
||||
return;
|
||||
};
|
||||
let operator = if member.is_optional() { "?." } else { "." };
|
||||
write!(f, [operator, identifier(property)]);
|
||||
}
|
||||
|
||||
/// [`write_lookup`] after the leading comments of the name have been written before the `.`, as oxfmt does. There a
|
||||
/// `prettier-ignore` comment among them is not about the name any more.
|
||||
pub(crate) fn write_lookup_after_its_comments<'a>(member: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
write_lookup_without_comments(member, f);
|
||||
if !f.is_quiet()
|
||||
&& let Some(start) = member.member_name_start()
|
||||
{
|
||||
FormatTrailingComments::Comments(f.comments().of_name(start).trailing).fmt(f);
|
||||
}
|
||||
}
|
||||
|
||||
/// The `.b` or `?.b` of `member`, where there are no comments.
|
||||
pub(crate) fn write_lookup_without_comments<'a>(member: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
fn write_lookup_without_comments<'a>(member: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let Some(start) = member.member_name_start() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,13 @@ impl<'a> ObjectLike<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn node(&self) -> AstNodes<'a> {
|
||||
match self {
|
||||
ObjectLike::ObjectExpression(e, _) => e.as_ast_nodes(),
|
||||
ObjectLike::TSTypeLiteral(ty, _) => ty.as_ast_nodes(),
|
||||
}
|
||||
}
|
||||
|
||||
/// It is the type of the only parameter of a function:
|
||||
/// `const fn = ({ foo }: { foo: string }) => {}`.
|
||||
pub(crate) fn should_hug(&self, f: &Formatter<'a>) -> bool {
|
||||
@@ -46,7 +53,7 @@ impl<'a> ObjectLike<'a> {
|
||||
if f.options().flavor.is_oxfmt() || f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
let comments = f.comments().comments_before_end_of(self.span());
|
||||
let comments = f.comments().of_node(self.node()).dangling;
|
||||
!comments.is_empty()
|
||||
&& comments.iter().all(|comment| !comment.is_line())
|
||||
&& self.should_hug(f)
|
||||
@@ -91,12 +98,9 @@ impl<'a> Format<'a> for ObjectLike<'a> {
|
||||
false => write!(f, group(&inner).should_expand(should_expand)),
|
||||
}
|
||||
} else if self.breaks_with_pattern(f) {
|
||||
write!(f, soft_block_indent(&format_dangling_comments(self.span())));
|
||||
write!(f, soft_block_indent(&dangling_comments(self.node())));
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
format_dangling_comments(self.span()).with_soft_block_indent()
|
||||
);
|
||||
write!(f, dangling_comments(self.node()).with_soft_block_indent());
|
||||
}
|
||||
write!(f, "}");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ impl Spanned for ObjectPatternLike<'_> {
|
||||
}
|
||||
|
||||
impl<'a> ObjectPatternLike<'a> {
|
||||
fn node(&self) -> AstNodes<'a> {
|
||||
match self {
|
||||
Self::ObjectPattern(pat, _) => pat.as_ast_nodes(),
|
||||
Self::ObjectAssignmentTarget(e, _) => e.as_ast_nodes(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::ObjectPattern(_, props) => props.is_empty(),
|
||||
@@ -148,10 +155,9 @@ impl<'a> Format<'a> for ObjectPatternLike<'a> {
|
||||
|
||||
write!(f, "{");
|
||||
match self.layout(f) {
|
||||
ObjectPatternLayout::Empty => write!(
|
||||
f,
|
||||
format_dangling_comments(self.span()).with_soft_block_indent()
|
||||
),
|
||||
ObjectPatternLayout::Empty => {
|
||||
write!(f, dangling_comments(self.node()).with_soft_block_indent())
|
||||
}
|
||||
ObjectPatternLayout::Inline => write!(f, format_properties),
|
||||
ObjectPatternLayout::Group { expand } => {
|
||||
write!(f, group(&format_properties).should_expand(expand))
|
||||
|
||||
+179
-224
@@ -1,8 +1,12 @@
|
||||
use super::class::dangling_comments_with_marker;
|
||||
use super::decorators::FormatDecorators;
|
||||
use crate::js::format::{FormatTypeAnnotation, format_node_without_comments};
|
||||
use crate::js::attach::Marker;
|
||||
use crate::js::format::{FormatTypeAnnotation, format_node};
|
||||
use crate::js::utils::call_expression::{
|
||||
is_angular_test_wrapper, is_test_call_expression_in_flavor,
|
||||
};
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -18,16 +22,7 @@ impl Spanned for FormatFormalParameters<'_> {
|
||||
|
||||
impl<'a> Format<'a> for FormatFormalParameters<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let func = self.0;
|
||||
if f.is_quiet() {
|
||||
return write_formal_parameters(func, f);
|
||||
}
|
||||
format_node_without_comments(
|
||||
self.span(),
|
||||
|| func.as_ast_nodes(),
|
||||
f,
|
||||
|f| write_formal_parameters(func, f),
|
||||
);
|
||||
write_formal_parameters(self.0, f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,33 +30,35 @@ fn has_modifier(param: Param<'_>) -> bool {
|
||||
param.modifiers().iter().any(|it| it.decorator().is_none())
|
||||
}
|
||||
|
||||
/// Prettier's `printFunctionParameters`.
|
||||
fn write_formal_parameters<'a>(func: Func<'a>, f: &mut Formatter<'a>) {
|
||||
let has_parameters = func.params_with_this().next().is_some();
|
||||
if f.is_quiet() {
|
||||
return match has_parameters {
|
||||
true => write_parameters_in_parentheses(func, Span::default(), f),
|
||||
false => write!(f, "()"),
|
||||
};
|
||||
}
|
||||
let span = FormatFormalParameters(func).span();
|
||||
let comments = f.comments().comments_before(span.start);
|
||||
if !comments.is_empty() {
|
||||
let count = comments_trailing_the_name(func, has_parameters, comments, f.options().flavor);
|
||||
if count > 0 {
|
||||
write!(
|
||||
// Comments that are attached to the parentheses. For oxfmt `class A { b ⏎// comment⏎ () {} }` is `b () {}⏎// comment`, and
|
||||
// `b /* prettier-ignore */ ( c ) {}` is `b/* prettier-ignore */ ( c ) {}`. For Prettier only in Flow:
|
||||
// `declare function a /* comment */ (): void`
|
||||
if !f.is_quiet() {
|
||||
let node = AstNodes::FormalParameters(func);
|
||||
let owned = f.comments().of_node(node);
|
||||
if owned.is_ignored {
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
space(),
|
||||
FormatTrailingComments::Comments(comments.get(..count).unwrap_or_default())
|
||||
FormatLeadingComments::Comments(owned.leading),
|
||||
FormatSuppressedNode(node.span())
|
||||
]
|
||||
);
|
||||
}
|
||||
if !owned.leading.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
[space(), FormatTrailingComments::Comments(owned.leading)]
|
||||
);
|
||||
}
|
||||
}
|
||||
if func.params().is_empty() && func.this_param().is_none() {
|
||||
write!(f, "(");
|
||||
write_dangling_comments_in_parentheses(func, f);
|
||||
return write!(f, ")");
|
||||
}
|
||||
write_parameters_in_parentheses(func, span, f);
|
||||
}
|
||||
|
||||
/// `span`: of the parentheses. It is only looked at if there are no parameters.
|
||||
fn write_parameters_in_parentheses<'a>(func: Func<'a>, span: Span, f: &mut Formatter<'a>) {
|
||||
if f.file().is_flow() && super::flow::is_shorthand_function_type(func) {
|
||||
return super::flow::write_shorthand_parameter(func, f);
|
||||
}
|
||||
@@ -73,9 +70,7 @@ fn write_parameters_in_parentheses<'a>(func: Func<'a>, span: Span, f: &mut Forma
|
||||
let can_hug = should_hug_function_parameters(func, parentheses_not_needed, f)
|
||||
&& !has_any_decorated_parameter;
|
||||
|
||||
let layout = if func.params().is_empty() && func.this_param().is_none() {
|
||||
ParameterLayout::NoParameters
|
||||
} else if can_hug
|
||||
let layout = if can_hug
|
||||
// The callback of a test. Not that of `inject`, `async`, `fakeAsync` and `waitForAsync`,
|
||||
// whose parameters can break.
|
||||
|| matches!(
|
||||
@@ -86,8 +81,7 @@ fn write_parameters_in_parentheses<'a>(func: Func<'a>, span: Span, f: &mut Forma
|
||||
&& is_test_call_expression_in_flavor(call, f)
|
||||
&& !is_angular_test_wrapper(call)
|
||||
)
|
||||
)
|
||||
{
|
||||
) {
|
||||
ParameterLayout::Hug
|
||||
} else {
|
||||
ParameterLayout::Default
|
||||
@@ -97,47 +91,6 @@ fn write_parameters_in_parentheses<'a>(func: Func<'a>, span: Span, f: &mut Forma
|
||||
write!(f, "(");
|
||||
}
|
||||
match layout {
|
||||
// Prettier takes a comment before the `=>` for one between the parentheses, finds none
|
||||
// there, and is left with the line breaks around it. The empty text is what makes the
|
||||
// second one count.
|
||||
ParameterLayout::NoParameters
|
||||
if !f.is_quiet()
|
||||
&& !f.comments().has_comment_in_span(span)
|
||||
&& func.arrow_span().is_some_and(|token| {
|
||||
let start = func.return_type().map_or(span.end, |ty| ty.span().end);
|
||||
f.comments().has_comment_in_range(start, token.start)
|
||||
}) =>
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
indent(&format_args!(soft_line_break(), "")),
|
||||
soft_line_break()
|
||||
]
|
||||
);
|
||||
}
|
||||
// What is left of the comments before the `(` leads what follows the `)`.
|
||||
ParameterLayout::NoParameters
|
||||
if f.is_quiet() || !f.comments().has_comment_in_span(span) => {}
|
||||
// Prettier's `printDanglingCommentsInList`: they break along with the rest of the signature.
|
||||
ParameterLayout::NoParameters => {
|
||||
let ends_with_line_comment = f
|
||||
.comments()
|
||||
.comments_before(span.end)
|
||||
.last()
|
||||
.is_some_and(|it| it.is_line());
|
||||
write!(
|
||||
f,
|
||||
indent(&format_args!(
|
||||
soft_line_break(),
|
||||
format_dangling_comments(span)
|
||||
))
|
||||
);
|
||||
match ends_with_line_comment {
|
||||
true => write!(f, hard_line_break()),
|
||||
false => write!(f, soft_line_break()),
|
||||
}
|
||||
}
|
||||
ParameterLayout::Hug => write!(f, ParameterList::with_layout(func, layout)),
|
||||
ParameterLayout::Default => write!(
|
||||
f,
|
||||
@@ -149,74 +102,87 @@ fn write_parameters_in_parentheses<'a>(func: Func<'a>, span: Span, f: &mut Forma
|
||||
}
|
||||
}
|
||||
|
||||
/// How many of `comments`, which are before the `(` of `func`, trail what is before them. The others
|
||||
/// lead what follows: the first parameter, the return type or the body.
|
||||
///
|
||||
/// Prettier's `handleFunctionNameComments`, and what it does with any comment: on a line of its own
|
||||
/// it leads what follows, at the end of a line it trails what is before.
|
||||
fn comments_trailing_the_name(
|
||||
func: Func<'_>,
|
||||
has_parameters: bool,
|
||||
comments: &[Comment],
|
||||
flavor: Flavor,
|
||||
) -> usize {
|
||||
match func.kind() {
|
||||
FnKind::Decl | FnKind::Expr | FnKind::Arrow if func.has_body() => {
|
||||
if !follows_name_or_type_parameters(func) {
|
||||
return 0;
|
||||
}
|
||||
// There is no such rule for an arrow function: with nothing but a `(` between it and the
|
||||
// first parameter, the comment leads that.
|
||||
let leads_parameter = func.is_arrow() && has_parameters;
|
||||
(comments.iter())
|
||||
.take_while(|it| {
|
||||
!it.preceded_by_newline() && (!leads_parameter || it.followed_by_newline())
|
||||
})
|
||||
.count()
|
||||
}
|
||||
_ if has_parameters && !matches!(func.as_ast_nodes(), AstNodes::Function(_)) => 0,
|
||||
// Babel has no node for the function of a method: on a line of its own the comment is between the key and
|
||||
// the first parameter, which it leads. oxfmt has that node.
|
||||
FnKind::Method | FnKind::Getter | FnKind::Setter | FnKind::Constructor
|
||||
if has_parameters && func.file().is_javascript() && !flavor.is_oxfmt() =>
|
||||
{
|
||||
(comments.iter())
|
||||
.take_while(|it| !it.preceded_by_newline())
|
||||
.count()
|
||||
}
|
||||
_ => comments.len(),
|
||||
/// The node whose parameters Prettier prints for `func`. It owns the comments in empty parentheses. Babel has no
|
||||
/// `FunctionExpression` in a method, so there it is the method.
|
||||
fn function_node<'a>(func: Func<'a>, f: &Formatter<'a>) -> AstNodes<'a> {
|
||||
let node = func.as_ast_nodes();
|
||||
if !matches!(node, AstNodes::Function(_)) || !f.context().has_tree_of_babel() {
|
||||
return node;
|
||||
}
|
||||
match func.owner() {
|
||||
Node::Member(member) => member.as_ast_nodes(),
|
||||
Node::Expr(function) => match function.parent() {
|
||||
Node::Prop(property) if property.func() == Some(func) => property.as_ast_nodes(),
|
||||
_ => node,
|
||||
},
|
||||
_ => node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Prettier has a node before the `(` of `func`, in the node that the parameters are in. The
|
||||
/// function of a method with a body starts at the type parameters or the `(`.
|
||||
pub(crate) fn follows_name_or_type_parameters(func: Func<'_>) -> bool {
|
||||
!func.type_params().is_empty()
|
||||
|| match func.kind() {
|
||||
FnKind::Decl | FnKind::Expr => func.name().is_some(),
|
||||
FnKind::Method | FnKind::Getter | FnKind::Setter | FnKind::Constructor => {
|
||||
!func.has_body()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
/// `function f(/* comment */) {}`: Prettier's `printDanglingCommentsInList`.
|
||||
fn write_dangling_comments_in_parentheses<'a>(func: Func<'a>, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let node = function_node(func, f);
|
||||
let all_dangling_comments = f.comments().of_node(node).dangling;
|
||||
if all_dangling_comments.is_empty() {
|
||||
return;
|
||||
}
|
||||
// `() /* comment */ => a`: Prettier means to leave out the comment before the `=>`, but misspells `comment.marker`. So
|
||||
// it writes the line breaks even if no comment is between them. The empty text makes the second line break count.
|
||||
let comments = dangling_comments_with_marker(node, Marker::None, f);
|
||||
let format_comments = FormatDanglingComments::Comments {
|
||||
comments,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
indent(&format_args!(
|
||||
soft_line_break(),
|
||||
comments.is_empty().then_some(""),
|
||||
format_comments
|
||||
))
|
||||
);
|
||||
match all_dangling_comments
|
||||
.iter()
|
||||
.any(|comment| comment.is_line())
|
||||
{
|
||||
true => write!(f, hard_line_break()),
|
||||
false => write!(f, soft_line_break()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments in `span`, whether they are printed or not. For what is asked several times while
|
||||
/// a node is written, and has to get the same answer.
|
||||
pub(crate) fn comments_between<'a>(
|
||||
span: Span,
|
||||
f: &Formatter<'a>,
|
||||
) -> impl Iterator<Item = &'a Comment> + use<'a> {
|
||||
let Span { start, end } = span;
|
||||
let printed = f
|
||||
.comments()
|
||||
.printed_comments()
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(move |c| c.start() >= start);
|
||||
printed
|
||||
.chain(f.comments().comments_before_iter(end))
|
||||
.filter(move |c| c.start() >= start && c.end() <= end)
|
||||
/// Prettier's `hasComment(parameter)`. A parameter without a default value is the same node as its pattern in ESTree.
|
||||
///
|
||||
/// The pattern and the type in the parameter ask this too while they are written, so [`Formatter::is_quiet`] says nothing
|
||||
/// about it.
|
||||
fn parameter_has_comment<'a>(param: Param<'a>, f: &Formatter<'a>) -> bool {
|
||||
let comments = f.comments();
|
||||
(comments.is_owner(Node::Param(param)) && comments.has_comments(param.as_ast_nodes()))
|
||||
|| (comments.is_owner(Node::Pat(param.pat()))
|
||||
&& param.default().is_none()
|
||||
&& !hugs_pattern_with_comments(f)
|
||||
&& comments.has_comments(param.pat().as_ast_nodes()))
|
||||
}
|
||||
|
||||
/// ```js
|
||||
/// function a( function a({
|
||||
/// { // comment
|
||||
/// // comment }) {}
|
||||
/// },
|
||||
/// ) {}
|
||||
/// ```
|
||||
///
|
||||
/// Prettier on the left, oxfmt on the right.
|
||||
fn hugs_pattern_with_comments(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `function a({ b } // comment⏎ = {}) {}` is `function a({⏎ b,⏎} = {}) {} // comment` for oxfmt, and
|
||||
/// `function a({ b } = {}) {} // comment` for Prettier.
|
||||
fn comment_behind_pattern_breaks_it(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// The modifiers of a parameter, in the order that they are written in.
|
||||
@@ -254,55 +220,66 @@ pub(crate) fn write_formal_parameter<'a>(param: Param<'a>, f: &mut Formatter<'a>
|
||||
};
|
||||
}
|
||||
|
||||
let pattern = param.pat();
|
||||
let type_annotation = param.ty().map(FormatTypeAnnotation);
|
||||
let is_group = !matches!(pattern.kind(), PatKind::Ident(_)) || every_parameter_is_a_group(f);
|
||||
let content = format_with(|f| {
|
||||
let left = format_with(|f| {
|
||||
let modifiers = param.modifiers();
|
||||
let keywords: &[_] = match modifiers.is_empty() {
|
||||
true => &[],
|
||||
false => &MODIFIERS,
|
||||
};
|
||||
for &(flag, keyword) in keywords {
|
||||
if modifiers.iter().any(|it| it.flag() == flag) {
|
||||
write!(f, [keyword, space()]);
|
||||
}
|
||||
let modifiers = param.modifiers();
|
||||
let keywords: &[_] = match modifiers.is_empty() {
|
||||
true => &[],
|
||||
false => &MODIFIERS,
|
||||
};
|
||||
for &(flag, keyword) in keywords {
|
||||
if modifiers.iter().any(|it| it.flag() == flag) {
|
||||
write!(f, [keyword, space()]);
|
||||
}
|
||||
write!(f, [param.pat(), param.is_optional().then_some("?")]);
|
||||
if let Some(type_annotation) = param.ty().map(FormatTypeAnnotation) {
|
||||
if !f.is_quiet()
|
||||
&& f.comments()
|
||||
.has_comment_before(type_annotation.span().start)
|
||||
{
|
||||
write!(f, space());
|
||||
}
|
||||
write!(f, type_annotation);
|
||||
}
|
||||
let left = format_with(|f| {
|
||||
let optional = param.is_optional().then_some("?");
|
||||
match param
|
||||
.default()
|
||||
.filter(|_| !comment_behind_pattern_breaks_it(f))
|
||||
{
|
||||
Some(_) => write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&pattern),
|
||||
optional,
|
||||
(type_annotation.as_ref()).map(FormatNodeWithoutTrailingComments)
|
||||
]
|
||||
),
|
||||
None => write!(f, [pattern, optional, type_annotation]),
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(initializer) = param.default() {
|
||||
let left = (&left).memoized();
|
||||
// So that the comments in it are not taken for comments before the `=`.
|
||||
left.inspect(f);
|
||||
let leading_comments = f
|
||||
.comments()
|
||||
.own_line_comments_before(initializer.span().start);
|
||||
write!(
|
||||
let Some(initializer) = param.default() else {
|
||||
return write!(f, left);
|
||||
};
|
||||
let write_assignment_pattern = |f: &mut Formatter<'a>| {
|
||||
// `({ a } // comment⏎ = {}) => {}`: the comment does not break the pattern.
|
||||
match is_group {
|
||||
true => write!(f, group(&left)),
|
||||
false => write!(f, left),
|
||||
}
|
||||
write!(f, trailing_comments(pattern.as_ast_nodes()));
|
||||
if let Some(ty) = param.ty() {
|
||||
write!(f, trailing_comments(AstNodes::TSTypeAnnotation(ty)));
|
||||
}
|
||||
write!(f, [space(), "=", space(), initializer]);
|
||||
};
|
||||
// `private a = 1`: the `AssignmentPattern` in a `TSParameterProperty` is a node of its own. Not for oxfmt.
|
||||
match param.is_parameter_property() && !f.options().flavor.is_oxfmt() {
|
||||
true => format_node(
|
||||
AstNodes::AssignmentPattern(Node::Param(param)),
|
||||
f,
|
||||
[
|
||||
FormatLeadingComments::Comments(leading_comments),
|
||||
group(&left),
|
||||
space(),
|
||||
"=",
|
||||
space(),
|
||||
initializer
|
||||
]
|
||||
);
|
||||
} else {
|
||||
write!(f, left);
|
||||
write_assignment_pattern,
|
||||
),
|
||||
false => write_assignment_pattern(f),
|
||||
}
|
||||
});
|
||||
|
||||
// A group makes no difference to what has no place to break at.
|
||||
if !has_decorators && cannot_break(param, f) {
|
||||
if !has_decorators && (!is_group || cannot_break(param, f)) {
|
||||
return write!(f, content);
|
||||
}
|
||||
|
||||
@@ -342,6 +319,20 @@ fn decorators_of_parameter_are_a_group_of_their_own(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// ```ts
|
||||
/// constructor( constructor(
|
||||
/// private a: /* comment */ private a: /* comment */ B,
|
||||
/// B, c,
|
||||
/// c, ) {}
|
||||
/// ) {}
|
||||
/// ```
|
||||
///
|
||||
/// Prettier on the left, for `a: /* comment */⏎B`: only a pattern is a group, so the line break after the comment breaks with
|
||||
/// the parameters. oxfmt on the right.
|
||||
fn every_parameter_is_a_group(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Whether `param` is a name, and its type a keyword or a name.
|
||||
fn cannot_break<'a>(param: Param<'a>, f: &Formatter<'a>) -> bool {
|
||||
f.is_quiet()
|
||||
@@ -354,15 +345,13 @@ fn cannot_break<'a>(param: Param<'a>, f: &Formatter<'a>) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) struct ParameterList<'a> {
|
||||
struct ParameterList<'a> {
|
||||
func: Func<'a>,
|
||||
layout: ParameterLayout,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) enum ParameterLayout {
|
||||
/// `()`
|
||||
NoParameters,
|
||||
enum ParameterLayout {
|
||||
/// The only parameter is written right after the `(`, even if it breaks:
|
||||
///
|
||||
/// ```javascript
|
||||
@@ -377,7 +366,7 @@ pub(crate) enum ParameterLayout {
|
||||
}
|
||||
|
||||
impl<'a> ParameterList<'a> {
|
||||
pub(crate) fn with_layout(func: Func<'a>, layout: ParameterLayout) -> Self {
|
||||
fn with_layout(func: Func<'a>, layout: ParameterLayout) -> Self {
|
||||
Self { func, layout }
|
||||
}
|
||||
}
|
||||
@@ -386,7 +375,7 @@ impl<'a> Format<'a> for ParameterList<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let params = self.func.params_with_this();
|
||||
match self.layout {
|
||||
ParameterLayout::Default | ParameterLayout::NoParameters => {
|
||||
ParameterLayout::Default => {
|
||||
// Nothing can follow a rest parameter, not even a comma.
|
||||
let trailing_separator = match self.func.params().last().is_some_and(Param::is_rest)
|
||||
{
|
||||
@@ -411,7 +400,7 @@ impl<'a> Format<'a> for ParameterList<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `a => a`: whether the options ask for it and the parameters allow it.
|
||||
/// `a => a`: whether the options ask for it and the parameters allow it. Prettier's `shouldPrintParamsWithoutParens`.
|
||||
pub(crate) fn can_avoid_parentheses<'a>(arrow: Func<'a>, f: &Formatter<'a>) -> bool {
|
||||
f.options().arrow_parentheses.is_as_needed()
|
||||
&& arrow.params().len() == 1
|
||||
@@ -424,10 +413,9 @@ pub(crate) fn can_avoid_parentheses<'a>(arrow: Func<'a>, f: &Formatter<'a>) -> b
|
||||
&& !param.is_optional()
|
||||
&& param.default().is_none()
|
||||
&& matches!(param.pat().kind(), PatKind::Ident(_))
|
||||
&& !parameter_has_comment(param, f)
|
||||
})
|
||||
&& !f
|
||||
.comments()
|
||||
.has_comment_before(arrow.arrow_span().map_or(0, |token| token.start))
|
||||
&& (f.comments().of_node(arrow.as_ast_nodes()).dangling).is_empty()
|
||||
}
|
||||
|
||||
/// Prettier's `shouldHugTheOnlyFunctionParameter`.
|
||||
@@ -440,45 +428,12 @@ pub(crate) fn should_hug_function_parameters<'a>(
|
||||
if list.len() > 1 || list.last().is_some_and(Param::is_rest) {
|
||||
return false;
|
||||
}
|
||||
// Not if there are comments around the only parameter. Those before the `(` that do not trail
|
||||
// the name lead it.
|
||||
let has_comments_around = |param: Param<'a>| {
|
||||
let span = FormatFormalParameters(func).span();
|
||||
let start = match func.kind() {
|
||||
FnKind::Decl | FnKind::Expr | FnKind::Arrow if func.has_body() => {
|
||||
(func.type_params().angle_brackets_span())
|
||||
.or_else(|| func.name().map(|it| it.span()))
|
||||
.map_or_else(|| func.span().start, |it| it.end)
|
||||
}
|
||||
_ => span.start,
|
||||
};
|
||||
let mut comments = comments_between(Span::new(start, span.end), f).peekable();
|
||||
if comments.peek().is_none() {
|
||||
return false;
|
||||
}
|
||||
let follows_name = follows_name_or_type_parameters(func);
|
||||
let leads_parameter = |comment: &Comment| {
|
||||
if !follows_name || comment.preceded_by_newline() {
|
||||
return true;
|
||||
}
|
||||
match comment.start() >= span.start || func.is_arrow() {
|
||||
// At the end of the line it trails the name.
|
||||
true => !comment.followed_by_newline(),
|
||||
false => false,
|
||||
}
|
||||
};
|
||||
comments.any(|comment| {
|
||||
comment.start() >= param.span().end
|
||||
|| (comment.end() <= param.span().start && leads_parameter(comment))
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(this_param) = func.this_param() {
|
||||
return list.is_empty()
|
||||
&& this_param
|
||||
.ty()
|
||||
.is_some_and(|ty| matches!(ty.kind(), TypeKind::Object(_) | TypeKind::Mapped(_)))
|
||||
&& !has_comments_around(this_param);
|
||||
&& !parameter_has_comment(this_param, f);
|
||||
}
|
||||
let Some(only_parameter) = list.first() else {
|
||||
return false;
|
||||
@@ -498,7 +453,7 @@ pub(crate) fn should_hug_function_parameters<'a>(
|
||||
is_huggable
|
||||
&& !has_modifier(only_parameter)
|
||||
&& !(f.file().is_flow() && super::flow::is_component(func, f))
|
||||
&& !has_comments_around(only_parameter)
|
||||
&& !parameter_has_comment(only_parameter, f)
|
||||
}
|
||||
|
||||
/// Whether all parameters are plain names, without default values. A rest parameter is not.
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
use super::object_pattern_like::ObjectPatternLike;
|
||||
use crate::js::format::format_node;
|
||||
use crate::js::utils::array::write_array_node;
|
||||
use crate::js::utils::assignment_like::{AssignmentLike, comments_stay_around_operator};
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::js::utils::assignment_like::AssignmentLike;
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
|
||||
@@ -23,9 +22,15 @@ fn write_array_pattern<'a>(pat: Pat<'a>, elements: List<'a, PatElem<'a>>, f: &mu
|
||||
if elements.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
format_dangling_comments(pat.span()).with_soft_block_indent()
|
||||
dangling_comments(pat.as_ast_nodes()).with_soft_block_indent()
|
||||
);
|
||||
} else {
|
||||
// `[, /* comment */]`: a hole is no node, so the comment dangles. Prettier's `printArray` writes it after the elements.
|
||||
let dangling_comments = match f.is_quiet() {
|
||||
true => CommentList::empty(),
|
||||
false => f.comments().of_node(pat.as_ast_nodes()).dangling,
|
||||
};
|
||||
let has_dangling_line_comment = dangling_comments.iter().any(|comment| comment.is_line());
|
||||
let rest = elements.last().filter(|it| is_rest_element(*it));
|
||||
let count = elements.len() - usize::from(rest.is_some());
|
||||
write!(
|
||||
@@ -41,8 +46,19 @@ fn write_array_pattern<'a>(pat: Pat<'a>, elements: List<'a, PatElem<'a>>, f: &mu
|
||||
f,
|
||||
);
|
||||
}
|
||||
write!(f, rest);
|
||||
// Prettier's `printArrayItems`: each element is a group.
|
||||
if let Some(rest) = rest {
|
||||
write!(f, group(&rest));
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
FormatDanglingComments::Comments {
|
||||
comments: dangling_comments,
|
||||
indent: DanglingIndentMode::None,
|
||||
}
|
||||
);
|
||||
})))
|
||||
.should_expand(has_dangling_line_comment)
|
||||
);
|
||||
}
|
||||
write!(f, "]");
|
||||
@@ -50,21 +66,7 @@ fn write_array_pattern<'a>(pat: Pat<'a>, elements: List<'a, PatElem<'a>>, f: &mu
|
||||
|
||||
/// `left = right`, ESTree's `AssignmentPattern`.
|
||||
fn write_assignment_pattern<'a>(left: Pat<'a>, right: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let left = left.memoized();
|
||||
// So that the comments in it are not taken for comments before the `=`.
|
||||
left.inspect(f);
|
||||
let comments = f.comments().own_line_comments_before(right.span().start);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatLeadingComments::Comments(comments),
|
||||
group(&left),
|
||||
space(),
|
||||
"=",
|
||||
space(),
|
||||
right
|
||||
]
|
||||
);
|
||||
write!(f, [group(&left), space(), "=", space(), right]);
|
||||
}
|
||||
|
||||
/// `[...a = 1]` is an error, and `a = 1` in typescript-estree's tree.
|
||||
@@ -77,13 +79,12 @@ pub(crate) fn write_array_pattern_element<'a>(element: PatElem<'a>, f: &mut Form
|
||||
let Some(pat) = element.pat() else {
|
||||
return;
|
||||
};
|
||||
let parent = || crate::js::ast_nodes::node_as_ast_nodes(element.parent());
|
||||
if is_rest_element(element) {
|
||||
format_node(element.span(), parent, f, |f| write!(f, ["...", pat]));
|
||||
let node = AstNodes::BindingRestElement(Node::PatElem(element));
|
||||
format_node(node, f, |f| write!(f, ["...", pat]));
|
||||
} else if let Some(default) = element.default() {
|
||||
format_node(element.span(), parent, f, |f| {
|
||||
write_assignment_pattern(pat, default, f)
|
||||
});
|
||||
let node = AstNodes::AssignmentPattern(Node::PatElem(element));
|
||||
format_node(node, f, |f| write_assignment_pattern(pat, default, f));
|
||||
} else {
|
||||
pat.fmt(f);
|
||||
}
|
||||
@@ -94,25 +95,6 @@ pub(crate) fn write_binding_property<'a>(property: PatProp<'a>, f: &mut Formatte
|
||||
if property.is_rest() {
|
||||
return write!(f, ["...", property.value()]);
|
||||
}
|
||||
// Prettier's `handlePropertyComments`: a comment at the end of the line of the key leads the
|
||||
// property.
|
||||
if !f.is_quiet()
|
||||
&& !comments_stay_around_operator(f)
|
||||
&& !property.is_shorthand()
|
||||
&& let Some(key) = property.key()
|
||||
&& !f.comments().has_comment_in_span(key.span(f.file()))
|
||||
{
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_leading_property(key.span(f.file()).end, property.value().span().start);
|
||||
if comments
|
||||
.iter()
|
||||
.any(|comment| f.comments().is_suppression_comment(comment))
|
||||
{
|
||||
return write!(f, FormatSuppressedNode(property.span()));
|
||||
}
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
AssignmentLike::BindingProperty(property).fmt(f);
|
||||
}
|
||||
|
||||
@@ -124,9 +106,9 @@ impl<'a> Format<'a> for FormatBindingPropertyValue<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let (property, value) = (self.0, self.0.value());
|
||||
match property.default() {
|
||||
// With the parentheses of the default, as in ESTree.
|
||||
Some(default) => format_node(
|
||||
value.span().to(default.span()),
|
||||
|| AstNodes::BindingProperty(property),
|
||||
AstNodes::AssignmentPattern(Node::PatProp(property)),
|
||||
f,
|
||||
|f| write_assignment_pattern(value, default, f),
|
||||
),
|
||||
|
||||
+57
-226
@@ -1,18 +1,14 @@
|
||||
//! The file, its `#!` line, directives, and lists of statements.
|
||||
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use super::statements::{
|
||||
CommentPlacement, comment_placements, expression_statement_needs_semicolon,
|
||||
follows_type_cast_comment,
|
||||
};
|
||||
use super::statements::{expression_statement_needs_semicolon, follows_type_cast_comment};
|
||||
use crate::ir::element::TextWidth;
|
||||
use crate::js::format::FormatStatementBeforeAnother;
|
||||
use crate::js::ast_nodes::node_as_ast_nodes;
|
||||
use crate::js::sort_imports::ImportRun;
|
||||
use crate::js::trivia::comment_before_semicolon_keeps_its_line;
|
||||
use crate::js::utils::string::{FormatLiteralStringToken, StringLiteralParentKind};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
use smallvec::SmallVec;
|
||||
use crate::write;
|
||||
|
||||
pub(crate) fn write_program<'a>(file: &'a File<'a>, f: &mut Formatter<'a>) {
|
||||
let source = file.text();
|
||||
@@ -21,48 +17,22 @@ pub(crate) fn write_program<'a>(file: &'a File<'a>, f: &mut Formatter<'a>) {
|
||||
f.write_text(&source[..3], Some(TextWidth::single(0)));
|
||||
}
|
||||
write_hashbang(false, f);
|
||||
// Nothing that a comment could belong to: they are all Prettier's dangling comments of the
|
||||
// program, which have no empty lines between them.
|
||||
if !file.body().is_empty()
|
||||
&& file
|
||||
.body()
|
||||
.iter()
|
||||
.all(|it| matches!(it.kind(), StmtKind::Empty))
|
||||
{
|
||||
let comments = f.comments().unprinted_comments();
|
||||
let indent = DanglingIndentMode::None;
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
FormatDanglingComments::Comments { comments, indent },
|
||||
hard_line_break()
|
||||
]
|
||||
);
|
||||
}
|
||||
write!(f, FormatStatements(file.body()));
|
||||
let mut rest = f.comments().unprinted_comments();
|
||||
// The empty lines before the first thing in a file go.
|
||||
let is_blank = |before: &[u8]| {
|
||||
(before.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(before))
|
||||
.trim_ascii()
|
||||
.is_empty()
|
||||
};
|
||||
if let Some((first, others)) = rest.split_first()
|
||||
&& source
|
||||
.get(..first.span.start as usize)
|
||||
.is_some_and(is_blank)
|
||||
{
|
||||
let (comments, indent) = (std::slice::from_ref(first), DanglingIndentMode::None);
|
||||
write!(f, FormatDanglingComments::Comments { comments, indent });
|
||||
rest = others;
|
||||
}
|
||||
// The program only owns comments if there is no statement to attach them to.
|
||||
let program = node_as_ast_nodes(Node::File(file));
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(program),
|
||||
FormatStatements(file.body()),
|
||||
dangling_comments(program)
|
||||
]
|
||||
);
|
||||
// Behind the comments, if that is all there is in the file.
|
||||
let ends_with_empty_line = (file.body().last()).is_some_and(|last| {
|
||||
is_directive_before_empty_line(last, f)
|
||||
&& (source.get(last.span().end as usize..))
|
||||
.is_some_and(|behind| !behind.trim_ascii().is_empty())
|
||||
});
|
||||
write!(f, FormatTrailingComments::Comments(rest));
|
||||
match ends_with_empty_line {
|
||||
true => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
@@ -146,7 +116,7 @@ fn is_next_line_empty_after<'a>(statement: Stmt<'a>, f: &Formatter<'a>) -> bool
|
||||
// An `ExportNamedDeclaration` around a declaration.
|
||||
|| statement.is_exported()
|
||||
|| empty_line_before_semicolon_is_after_any_statement(f))
|
||||
&& is_next_line_empty(source, f.comments().without_semicolon(span).end)
|
||||
&& is_next_line_empty(source, f.file().without_semicolon(span).end)
|
||||
}
|
||||
|
||||
/// `type A = 1⏎⏎;(b)`: the `;` is that of the type alias. oxfmt keeps the empty line all the same.
|
||||
@@ -163,86 +133,25 @@ impl<'a> Format<'a> for FormatStatements<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let mut previous: Option<Stmt<'a>> = None;
|
||||
let mut imports = ImportRun::new(f);
|
||||
let last_index = self.0.len().saturating_sub(1);
|
||||
for (index, statement) in self
|
||||
.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, it)| it.tag() != StmtTag::Empty)
|
||||
{
|
||||
let write_statement = |f: &mut Formatter<'a>| match index == last_index {
|
||||
true => write!(f, statement),
|
||||
false => write!(f, FormatStatementBeforeAnother(statement)),
|
||||
};
|
||||
let Some(previous_statement) = previous.replace(statement) else {
|
||||
imports.before_statement(statement, f);
|
||||
write_semicolon_before_type_cast_comment(statement, f);
|
||||
write_statement(f);
|
||||
continue;
|
||||
};
|
||||
// Nearly always, what is left between two statements starts its line.
|
||||
let comments = match f.is_quiet() {
|
||||
true => &[][..],
|
||||
false => f.comments().comments_before(statement.span().start),
|
||||
};
|
||||
let starts_line = |comment: &Comment| {
|
||||
comment.preceded_by_newline()
|
||||
|| is_behind_empty_statements_that_start_line(previous_statement, comment, f)
|
||||
};
|
||||
let placements = match comments.iter().all(starts_line) {
|
||||
true => SmallVec::new(),
|
||||
false => write_more_trailing_comments(previous_statement, comments, statement, f),
|
||||
};
|
||||
imports.before_separator(statement, placements.is_empty(), f);
|
||||
// `a ⏎ /* comment */; ⏎ b`
|
||||
let is_next_line_empty = match comments.first() {
|
||||
Some(first)
|
||||
if first.preceded_by_newline()
|
||||
&& first.span.start < previous_statement.span().end
|
||||
&& comment_before_semicolon_keeps_its_line(f) =>
|
||||
{
|
||||
f.lines_before(first.span) > 1
|
||||
for statement in self.0.iter().filter(|it| it.tag() != StmtTag::Empty) {
|
||||
if let Some(previous_statement) = previous.replace(statement) {
|
||||
imports.before_separator(statement, true, f);
|
||||
let is_next_line_empty =
|
||||
match own_line_comment_before_semicolon(previous_statement, f) {
|
||||
Some(comment) => f.lines_before(comment.span()) > 1,
|
||||
None => {
|
||||
!is_behind_semicolon_and_blanks(statement, f)
|
||||
&& is_next_line_empty_after(previous_statement, f)
|
||||
}
|
||||
};
|
||||
match is_next_line_empty {
|
||||
true => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
}
|
||||
None if is_behind_semicolon_and_blanks(statement, f) => false,
|
||||
_ => is_next_line_empty_after(previous_statement, f),
|
||||
};
|
||||
match is_next_line_empty {
|
||||
true => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
}
|
||||
imports.before_statement(statement, f);
|
||||
for (comment, _) in comments
|
||||
.iter()
|
||||
.zip(&placements)
|
||||
.filter(|(_, placement)| placement.leads())
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
FormatLeadingComments::Comments(std::slice::from_ref(comment))
|
||||
);
|
||||
}
|
||||
write_semicolon_before_type_cast_comment(statement, f);
|
||||
write_statement(f);
|
||||
}
|
||||
|
||||
// `a; // comment\n;`: the comments around an empty statement at the end trail `a`.
|
||||
if previous.is_some()
|
||||
&& !f.is_quiet()
|
||||
&& let Some(last) = self.0.last().filter(|last| last.tag() == StmtTag::Empty)
|
||||
{
|
||||
let end = last.ast_parent().span().end;
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(f.comments().comments_before(end))
|
||||
);
|
||||
}
|
||||
// `return // a⏎/* b */;`: what `return` has left for the next statement, and there is none.
|
||||
if let Some(last) = previous
|
||||
&& !f.is_quiet()
|
||||
&& f.options().flavor.is_oxfmt()
|
||||
{
|
||||
let comments = f.comments().comments_before(last.span().end);
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
write!(f, statement);
|
||||
}
|
||||
imports.finish(f);
|
||||
if let Some(last) = previous
|
||||
@@ -254,9 +163,28 @@ impl<'a> Format<'a> for FormatStatements<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `a ⏎ /* comment */; ⏎⏎ b`: see [`comment_before_semicolon_keeps_its_line`]. The comment is written on its own line behind
|
||||
/// `a;`, so an empty line is kept between the two only if there is one before the comment. A comment on the line of `a` can
|
||||
/// come first: `a // comment ⏎ // comment ⏎; ⏎⏎ b`
|
||||
fn own_line_comment_before_semicolon<'a>(
|
||||
statement: Stmt<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> Option<Comment<'a>> {
|
||||
if f.is_quiet() || !comment_before_semicolon_keeps_its_line(f) {
|
||||
return None;
|
||||
}
|
||||
let semicolon = statement.span().end.checked_sub(1)?;
|
||||
if f.source_text().byte_at(semicolon) != Some(b';') {
|
||||
return None;
|
||||
}
|
||||
(f.comments().comments_directly_before(semicolon).iter())
|
||||
.find(|comment| comment.has_newline_before())
|
||||
}
|
||||
|
||||
/// `a()⏎⏎; [b].c()`: oxfmt looks for an empty line before a statement, and over a `;` only if that is right before it.
|
||||
fn is_behind_semicolon_and_blanks<'a>(statement: Stmt<'a>, f: &Formatter<'a>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
&& !f.comments().has_comment_before(statement.span().start)
|
||||
&& (f.file().text().get(..statement.span().start as usize)).is_some_and(|before| {
|
||||
let code = before.trim_ascii_end();
|
||||
code.len() < before.len()
|
||||
@@ -265,115 +193,17 @@ fn is_behind_semicolon_and_blanks<'a>(statement: Stmt<'a>, f: &Formatter<'a>) ->
|
||||
})
|
||||
}
|
||||
|
||||
/// `a;⏎;// comment⏎b;`, as bundlers write it: with the empty statement gone the comment starts its line, and for oxfmt it
|
||||
/// leads `b`. For Prettier it trails `a`.
|
||||
fn is_behind_empty_statements_that_start_line<'a>(
|
||||
previous: Stmt<'a>,
|
||||
comment: &Comment,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
&& previous.span().end <= comment.span.start
|
||||
&& (f
|
||||
.source_text()
|
||||
.text_for(&previous.span().between(comment.span))
|
||||
.iter()
|
||||
.rev())
|
||||
.find(|byte| !matches!(byte, b' ' | b'\t' | b';'))
|
||||
.is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
|
||||
}
|
||||
|
||||
/// `function a() { "use strict";⏎⏎}`: oxfmt keeps the empty line after the last directive, whatever follows, be it
|
||||
/// nothing.
|
||||
fn is_directive_before_empty_line<'a>(last: Stmt<'a>, f: &Formatter<'a>) -> bool {
|
||||
f.options().flavor.is_oxfmt() && last.directive().is_some() && is_next_line_empty_after(last, f)
|
||||
}
|
||||
|
||||
/// Of `comments`, which are what is left between `previous` and `next`, writes those that trail
|
||||
/// `previous`: there is a `;` of `previous`, or an empty statement, between them and what is around
|
||||
/// them.
|
||||
///
|
||||
/// a
|
||||
/// // leads b
|
||||
/// ; // trails a
|
||||
/// ; // trails a
|
||||
/// b;
|
||||
///
|
||||
/// Returns where the comments are, if those that lead `next` have to be written by the caller
|
||||
/// because one of them is before one that trails.
|
||||
#[cold]
|
||||
fn write_more_trailing_comments<'a>(
|
||||
previous: Stmt<'a>,
|
||||
comments: &'a [Comment],
|
||||
next: Stmt<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> SmallVec<[CommentPlacement; 8]> {
|
||||
let placements = comment_placements(comments, next.span().start, f);
|
||||
// Prettier's `printTrailingComment`
|
||||
let mut is_after_line_comment =
|
||||
(f.comments().printed_comments().last()).is_some_and(|comment| {
|
||||
// It trails `previous`, and is not in it, as in `{ // comment⏎};`: nothing is between it and the end of
|
||||
// `previous` but the `;` and other comments.
|
||||
let end = previous.span().end;
|
||||
let is_blank = |from: u32, to: u32| {
|
||||
(f.source_text()).all_bytes(Span::new(from.min(to), to), |b| {
|
||||
b.is_ascii_whitespace() || b == b';'
|
||||
})
|
||||
};
|
||||
let mut at = comment.end();
|
||||
comment.is_line()
|
||||
&& comment.span.start >= previous.span().start
|
||||
&& (comments.iter())
|
||||
.take_while(|it| it.start() < end)
|
||||
.all(|it| is_blank(core::mem::replace(&mut at, it.end()), it.start()))
|
||||
&& is_blank(at, end.max(at))
|
||||
});
|
||||
let mut has_line_suffix = is_after_line_comment;
|
||||
for (comment, _) in comments
|
||||
.iter()
|
||||
.zip(&placements)
|
||||
.filter(|(_, placement)| !placement.leads())
|
||||
{
|
||||
f.comments_mut().increment_printed_count();
|
||||
if is_after_line_comment {
|
||||
write!(f, line_suffix(&format_args!(hard_line_break(), comment)));
|
||||
} else if comment.is_line() || has_line_suffix {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
line_suffix(&format_args!(space(), comment)),
|
||||
expand_parent()
|
||||
]
|
||||
);
|
||||
} else {
|
||||
write!(f, [space(), comment]);
|
||||
}
|
||||
has_line_suffix |= comment.is_line() || is_after_line_comment;
|
||||
is_after_line_comment = comment.is_line();
|
||||
}
|
||||
|
||||
let trailing_count = placements
|
||||
.iter()
|
||||
.take_while(|placement| !placement.leads())
|
||||
.count();
|
||||
match placements
|
||||
.iter()
|
||||
.skip(trailing_count)
|
||||
.all(|placement| placement.leads())
|
||||
{
|
||||
true => SmallVec::new(),
|
||||
false => placements,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `shouldExpressionStatementPrintOwnComments`: without semicolons, the `;` that a
|
||||
/// statement has to start with goes before a type cast comment, which has to stay next to its `(`.
|
||||
#[inline]
|
||||
fn write_semicolon_before_type_cast_comment<'a>(statement: Stmt<'a>, f: &mut Formatter<'a>) {
|
||||
if !f.is_quiet()
|
||||
&& f.comments().has_type_cast_comments()
|
||||
&& f.options().semicolons.is_as_needed()
|
||||
{
|
||||
if !f.is_quiet() && f.options().semicolons.is_as_needed() {
|
||||
write_semicolon_before_type_cast_comment_of(statement, f);
|
||||
}
|
||||
}
|
||||
@@ -383,10 +213,10 @@ fn write_semicolon_before_type_cast_comment_of<'a>(statement: Stmt<'a>, f: &mut
|
||||
let StmtKind::Expr(expression) = statement.kind() else {
|
||||
return;
|
||||
};
|
||||
let start = statement.span().start;
|
||||
if let [rest @ .., _] = f.comments().comments_before(start)
|
||||
&& follows_type_cast_comment(start, f)
|
||||
&& (!f.comments().is_suppressed(start)
|
||||
let node = statement.as_ast_nodes();
|
||||
if let Some((_, rest)) = f.comments().of_node(node).leading.split_last()
|
||||
&& follows_type_cast_comment(statement.span().start, f)
|
||||
&& (!f.comments().is_ignored(node)
|
||||
|| semicolon_is_before_cast_comment_of_ignored_statement(f))
|
||||
&& expression_statement_needs_semicolon(statement, expression, f)
|
||||
{
|
||||
@@ -394,13 +224,14 @@ fn write_semicolon_before_type_cast_comment_of<'a>(statement: Stmt<'a>, f: &mut
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier writes the `;` of a statement that is not formatted right before its text. oxfmt keeps the
|
||||
/// comment next to its `(`.
|
||||
fn semicolon_is_before_cast_comment_of_ignored_statement(f: &Formatter<'_>) -> bool {
|
||||
/// `// prettier-ignore ⏎ /** @type {T} */ (a).b()` without semicolons: Prettier writes the `;` right before the copied text,
|
||||
/// between the comment and its `(`. oxfmt keeps the two together: `;/** @type {T} */ (a).b()`
|
||||
pub(crate) fn semicolon_is_before_cast_comment_of_ignored_statement(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `"use strict";`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_directive<'a>(statement: Stmt<'a>, f: &mut Formatter<'a>) {
|
||||
let StmtKind::Expr(expression) = statement.kind() else {
|
||||
return;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::jsx::ignored_jsx_gets_no_parentheses;
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use crate::js::format::write_trailing_comments_of;
|
||||
use crate::js::utils::expression::ExpressionLeftSide;
|
||||
use crate::js::utils::typecast::is_cast_target_that_hides_its_kind;
|
||||
use crate::js::utils::typecast::{
|
||||
comments_of_outermost_node, has_leading_own_line_comment, is_cast_target,
|
||||
is_cast_target_that_hides_its_kind, is_chain_expression_without_comments,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -22,6 +22,7 @@ pub(crate) fn write_throw_statement<'a>(
|
||||
write_return_or_throw("throw", statement, Some(argument), f);
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn write_return_or_throw<'a>(
|
||||
keyword: &'static str,
|
||||
statement: Stmt<'a>,
|
||||
@@ -29,88 +30,59 @@ fn write_return_or_throw<'a>(
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
write!(f, keyword);
|
||||
let Some(content_end) = end_before_comments_that_count_for_the_width(statement, argument, f)
|
||||
else {
|
||||
if let Some(argument) = argument {
|
||||
write!(f, [space(), FormatAdjacentArgument(argument)]);
|
||||
}
|
||||
return write!(f, OptionalSemicolon);
|
||||
};
|
||||
if let Some(argument) = argument {
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(content_end);
|
||||
write!(f, [space(), FormatAdjacentArgument(argument)]);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
}
|
||||
let comments = f.comments().comments_before(statement.span().end);
|
||||
let on_the_line = (comments.iter())
|
||||
.take_while(|comment| !comment.preceded_by_newline())
|
||||
.count();
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
OptionalSemicolon,
|
||||
space(),
|
||||
FormatDanglingComments::Comments {
|
||||
comments: &comments[..on_the_line],
|
||||
indent: DanglingIndentMode::None,
|
||||
}
|
||||
]
|
||||
);
|
||||
write!(f, OptionalSemicolon);
|
||||
let comments = comments_that_count_for_the_width(statement, f);
|
||||
if !comments.is_empty() {
|
||||
let dangling = FormatDanglingComments::Comments {
|
||||
comments,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
// `throw a // comment⏎;/* comment */`: the second one starts a line, because it follows a line comment.
|
||||
let trailing = f.comments().of_node(statement.as_ast_nodes()).trailing;
|
||||
let others = trailing.slice(comments.len(), trailing.len());
|
||||
write!(
|
||||
f,
|
||||
[space(), dangling, FormatTrailingComments::Comments(others)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `return a // comment⏎;`: the `;` is on a later line, so the comment is in the statement. oxfmt writes it behind the
|
||||
/// `;` as a text like another, which has to fit on the line. Returns where what is before the comment ends.
|
||||
fn end_before_comments_that_count_for_the_width<'a>(
|
||||
/// `;` as ordinary text, which has to fit on the line. Prettier writes it like any other trailing comment of the statement.
|
||||
fn comments_that_count_for_the_width<'a>(
|
||||
statement: Stmt<'a>,
|
||||
argument: Option<Expr<'a>>,
|
||||
f: &Formatter<'a>,
|
||||
) -> Option<u32> {
|
||||
) -> CommentList<'a> {
|
||||
if !f.options().flavor.is_oxfmt() || f.is_quiet() {
|
||||
return None;
|
||||
return CommentList::empty();
|
||||
}
|
||||
let span = statement.span();
|
||||
let content_end = match argument {
|
||||
Some(argument) => argument.outer_span().end,
|
||||
None => span.start + "return".len() as u32,
|
||||
};
|
||||
let first = (f.comments())
|
||||
.comments_in(Span::new(content_end, span.end.max(content_end)))
|
||||
.first()?;
|
||||
let is_right_behind = (f.source_text())
|
||||
.text_for(&Span::new(content_end, first.start()))
|
||||
.trim_ascii()
|
||||
.is_empty();
|
||||
(is_right_behind
|
||||
&& !first.preceded_by_newline()
|
||||
&& !f.comments().has_trailing_suppression_comment(content_end))
|
||||
.then_some(content_end)
|
||||
let comments = f.comments().of_node(statement.as_ast_nodes()).trailing;
|
||||
let count = (comments.iter())
|
||||
.take_while(|comment| {
|
||||
comment.span().end <= statement.span().end && !comment.has_newline_before()
|
||||
})
|
||||
.count();
|
||||
comments.slice(0, count)
|
||||
}
|
||||
|
||||
/// What has to start on the line of the keyword before it: the argument of `return` or `throw`.
|
||||
/// If it breaks, or starts with a comment, it is written in parentheses.
|
||||
/// The argument of `return` or `throw`, which has to start on the line of the keyword. If it breaks, or starts with a
|
||||
/// comment, it is written in parentheses. Prettier's `printArgumentNode`.
|
||||
struct FormatAdjacentArgument<'a>(Expr<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatAdjacentArgument<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let argument = self.0;
|
||||
let is_sequence = matches!(
|
||||
argument.kind(),
|
||||
ExprKind::Binary {
|
||||
op: BinOp::Comma,
|
||||
..
|
||||
}
|
||||
);
|
||||
// JSX writes parentheses of its own around its comments, unless it is written as it is.
|
||||
let has_own_parentheses = matches!(argument.kind(), ExprKind::Jsx(_))
|
||||
&& !(ignored_jsx_gets_no_parentheses(f)
|
||||
&& f.comments().is_suppressed(argument.span().start));
|
||||
|
||||
if !has_own_parentheses && !f.is_quiet() && has_argument_leading_comments(argument, f) {
|
||||
let is_binaryish =
|
||||
matches!(argument.kind(), ExprKind::Binary { op, .. } if op != BinOp::Comma);
|
||||
if !f.is_quiet() && has_argument_leading_comments(argument, f) {
|
||||
write!(f, ["(", block_indent(&argument), ")"]);
|
||||
} else if is_cast_target_that_hides_its_kind(argument, f) {
|
||||
// For Prettier it is a `ParenthesizedExpression`, whatever is in it.
|
||||
write!(f, argument);
|
||||
} else if (matches!(argument.kind(), ExprKind::Binary { .. }) && !is_sequence)
|
||||
} else if is_binaryish
|
||||
|| (f.options().experimental_ternaries
|
||||
&& matches!(argument.kind(), ExprKind::Cond { yes, no, .. }
|
||||
if matches!(yes.kind(), ExprKind::Cond { .. }) || matches!(no.kind(), ExprKind::Cond { .. })))
|
||||
@@ -123,80 +95,135 @@ impl<'a> Format<'a> for FormatAdjacentArgument<'a> {
|
||||
if_group_breaks(&")")
|
||||
))
|
||||
);
|
||||
} else if is_sequence {
|
||||
let format_argument = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!("(", soft_block_indent(&argument), ")"))
|
||||
)
|
||||
});
|
||||
if f.is_quiet() {
|
||||
return write!(f, format_argument);
|
||||
}
|
||||
let span = argument.span();
|
||||
if f.comments().get_type_cast_comment_index(span).is_none() {
|
||||
write!(f, format_leading_comments(span));
|
||||
}
|
||||
// The comments before the `)` go behind it. After `return`, only those that start
|
||||
// their line: see `write_comments_before_closing_parenthesis`.
|
||||
let is_return = matches!(argument.ast_parent(), AstNodes::ReturnStatement(_));
|
||||
let limit = (f
|
||||
.comments()
|
||||
.comments_in(Span::after(span, argument.outer_span().end))
|
||||
.iter())
|
||||
.find(|comment| !is_return || comment.preceded_by_newline())
|
||||
.map_or_else(|| argument.outer_span().end, |comment| comment.span.start);
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(limit);
|
||||
write!(f, format_argument);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
write_trailing_comments_of(argument.as_ast_nodes(), f);
|
||||
} else if argument.binary_op() == Some(BinOp::Comma) && is_cast_target(argument, f) {
|
||||
// `return (/** @type {T} */ (a, b));`: oxfmt looks at what is in the parentheses of the type cast, and writes the
|
||||
// parentheses of a sequence around all of it.
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!("(", soft_block_indent(&argument), ")"))
|
||||
);
|
||||
} else {
|
||||
write!(f, argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a comment that ends its line, or spans several, is before `argument` or before anything
|
||||
/// down its left edge.
|
||||
/// Prettier's `hasNakedLeftSide`.
|
||||
fn has_naked_left_side(e: Expr<'_>) -> bool {
|
||||
match e.kind() {
|
||||
ExprKind::Assign { .. }
|
||||
| ExprKind::Binary { .. }
|
||||
| ExprKind::Cond { .. }
|
||||
| ExprKind::Call(_)
|
||||
| ExprKind::Dot { .. }
|
||||
| ExprKind::Index { .. }
|
||||
| ExprKind::TaggedTemplate(_)
|
||||
| ExprKind::NonNull(_)
|
||||
| ExprKind::Satisfies { .. } => true,
|
||||
ExprKind::As { .. } | ExprKind::AsConst(_) => !e.is_angle_bracket_assertion(),
|
||||
ExprKind::Unary { op, .. } => matches!(op, UnOp::PostInc | UnOp::PostDec),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `getLeftSide`. It takes the first of a few properties that the node has, so it also goes into the operand of
|
||||
/// `!a`, `await a` and `<T>a`, the callee of `new A()` and the first `${}` of a template.
|
||||
fn left_side(e: Expr<'_>) -> Option<Expr<'_>> {
|
||||
match e.kind() {
|
||||
ExprKind::Binary {
|
||||
op: BinOp::Comma, ..
|
||||
} => e.sequence().first().copied(),
|
||||
ExprKind::Template(template) => template.exprs().first(),
|
||||
ExprKind::Binary { left, .. } => Some(left),
|
||||
ExprKind::Assign { target, .. } => Some(target),
|
||||
ExprKind::Cond { test, .. } => Some(test),
|
||||
ExprKind::Call(call) | ExprKind::New(call) | ExprKind::TaggedTemplate(call) => {
|
||||
Some(call.callee())
|
||||
}
|
||||
ExprKind::Dot { obj, .. } | ExprKind::Index { obj, .. } => Some(obj),
|
||||
ExprKind::Unary { operand, .. } | ExprKind::Await(operand) | ExprKind::Spread(operand) => {
|
||||
Some(operand)
|
||||
}
|
||||
ExprKind::Yield { value, .. } => value,
|
||||
ExprKind::NonNull(expr)
|
||||
| ExprKind::As { expr, .. }
|
||||
| ExprKind::AsConst(expr)
|
||||
| ExprKind::Satisfies { expr, .. }
|
||||
| ExprKind::Instantiation { expr, .. } => Some(expr),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `return !(// comment⏎a) && b;`: Prettier finds the comment, because its `getLeftSide` goes into the operand of `!`, and writes
|
||||
/// parentheses around the argument. oxfmt stops at `!a`.
|
||||
fn left_edge_goes_on_in_operands(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `return a⏎// comment⏎.b;`: for Prettier the comment leads `a.b`. For oxfmt it leads the name `b`, and counts all the same. Not
|
||||
/// after `yield`, which only oxfmt asks about.
|
||||
fn own_line_comment_before_property_counts<'a>(argument: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
f.options().flavor.is_oxfmt() && !matches!(argument.ast_parent(), AstNodes::YieldExpression(_))
|
||||
}
|
||||
|
||||
/// See [`own_line_comment_before_property_counts`].
|
||||
fn has_own_line_comment_before_property<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let ExprKind::Dot { name, .. } = e.kind() else {
|
||||
return false;
|
||||
};
|
||||
let comments = f.comments().of_name(name.span().start).leading;
|
||||
comments.iter().any(|comment| {
|
||||
comment.has_newline_before()
|
||||
&& (comment.has_newline_after() || comment.is_multiline_block())
|
||||
})
|
||||
}
|
||||
|
||||
/// Prettier's `returnArgumentHasLeadingComment`: whether a leading comment of `argument` ends its line or spans several, or a
|
||||
/// leading comment of anything down its left edge ends its line.
|
||||
pub(crate) fn has_argument_leading_comments<'a>(argument: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let comments = f.comments();
|
||||
|
||||
// The comments inside the parentheses of a type cast are dealt with there.
|
||||
let type_cast_comment_end = comments
|
||||
.get_type_cast_comment_index(argument.span())
|
||||
.and_then(|index| comments.unprinted_comments().get(index))
|
||||
.map(|comment| comment.span.end);
|
||||
let is_before_type_cast =
|
||||
|comment: &Comment| type_cast_comment_end.is_none_or(|end| comment.span.start < end);
|
||||
|
||||
for left_side in ExpressionLeftSide::from(argument).iter() {
|
||||
let leading_comments = comments.comments_before(left_side.span().start);
|
||||
if leading_comments.iter().any(|comment| {
|
||||
(comment.is_multiline_block() || comment.followed_by_newline())
|
||||
&& is_before_type_cast(comment)
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
// After `yield`, which only oxfmt asks about, only what is before the left edge counts.
|
||||
if left_side.is_assignment_target
|
||||
|| matches!(argument.ast_parent(), AstNodes::YieldExpression(_))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let ExprKind::Dot { obj, name, .. } = left_side.expr.kind()
|
||||
&& !name.bytes().starts_with(b"#")
|
||||
&& comments
|
||||
.comments_in(Span::after(obj.span(), name.span().end))
|
||||
.iter()
|
||||
.any(|comment| {
|
||||
// Prettier's `handleMemberExpressionComments`: it leads the member expression.
|
||||
comment.preceded_by_newline()
|
||||
&& (comment.is_multiline_block() || comment.followed_by_newline())
|
||||
&& is_before_type_cast(comment)
|
||||
})
|
||||
let is_cast = is_cast_target(argument, f);
|
||||
let has_leading_comment = match argument.tag() == ExprTag::Jsx && !is_cast {
|
||||
true => has_leading_own_line_comment(argument, f),
|
||||
false => (comments_of_outermost_node(argument, f).leading.iter())
|
||||
.any(|comment| comment.has_newline_after() || comment.is_multiline_block()),
|
||||
};
|
||||
if has_leading_comment {
|
||||
return true;
|
||||
}
|
||||
if is_cast || !has_naked_left_side(argument) {
|
||||
return false;
|
||||
}
|
||||
let looks_at_properties = own_line_comment_before_property_counts(argument, f);
|
||||
let has_own_line_comment = |e: Expr<'a>| {
|
||||
let comments = f.comments().of_node(e.as_chain_element()).leading;
|
||||
comments.iter().any(|comment| comment.has_newline_after())
|
||||
|| (looks_at_properties && has_own_line_comment_before_property(e, f))
|
||||
};
|
||||
// The member access or the call in a `ChainExpression`.
|
||||
if (is_chain_expression_without_comments(argument, f) || looks_at_properties)
|
||||
&& has_own_line_comment(argument)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let mut current = argument;
|
||||
while let Some(left) = left_side(current)
|
||||
&& (has_naked_left_side(current) || left_edge_goes_on_in_operands(f))
|
||||
{
|
||||
// Every `ParenthesizedExpression` around it is a step of its own.
|
||||
let has_comment_before_parentheses = f.comments().has_type_cast_comments()
|
||||
&& left.parens().any(|parentheses| {
|
||||
let comments = f.comments().of_cast_parens(parentheses.start).leading;
|
||||
comments.iter().any(|comment| comment.has_newline_after())
|
||||
});
|
||||
if has_comment_before_parentheses
|
||||
|| match left.tag() {
|
||||
ExprTag::Jsx => has_leading_own_line_comment(left, f),
|
||||
_ => has_own_line_comment(left),
|
||||
}
|
||||
{
|
||||
return true;
|
||||
}
|
||||
current = left;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::js::utils::typecast::is_cast_target;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A `;`, unless the options say to leave them out.
|
||||
@@ -12,114 +11,3 @@ impl<'a> Format<'a> for OptionalSemicolon {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `a = (b /* comment */);` is `a = b; /* comment */` for oxfmt: the parentheses are not written, so the
|
||||
/// comment is before the `;`, and goes behind it like any other that is. Prettier writes
|
||||
/// `a = b /* comment */;`, and makes that of it when it formats it again.
|
||||
fn comments_in_dropped_parentheses_go_behind_semicolon(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier's `handleParenthesizedExpressionTrailingComment`: whether the parentheses at the end of `e`
|
||||
/// are written, with the comment before the `)` in them: those of a sequence or an assignment that is
|
||||
/// the value of a variable, the argument of `return`, the body of an arrow function or the right side of
|
||||
/// an assignment. `is_in_such_a_place`: `e` is.
|
||||
fn keeps_comment_in_parentheses(mut e: Expr<'_>, mut is_in_such_a_place: bool) -> bool {
|
||||
loop {
|
||||
match e.kind() {
|
||||
ExprKind::Binary {
|
||||
op: BinOp::Comma, ..
|
||||
} => return is_in_such_a_place,
|
||||
ExprKind::Assign { value, .. } => {
|
||||
let is_sequence = matches!(
|
||||
value.kind(),
|
||||
ExprKind::Binary {
|
||||
op: BinOp::Comma,
|
||||
..
|
||||
}
|
||||
);
|
||||
if is_in_such_a_place || is_sequence {
|
||||
return true;
|
||||
}
|
||||
e = value;
|
||||
}
|
||||
_ => match e.arrow_function().map(|arrow| arrow.body()) {
|
||||
Some(FnBody::Expr(body)) => (e, is_in_such_a_place) = (body, true),
|
||||
_ => return false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The last thing in `e` that is no assignment and no arrow function with an expression for a body. The
|
||||
/// parentheses between its end and the end of `e` are not written.
|
||||
fn last_of_assignments_and_arrows(mut e: Expr<'_>) -> Expr<'_> {
|
||||
loop {
|
||||
e = match e.kind() {
|
||||
ExprKind::Assign { value, .. } => value,
|
||||
_ => match e.arrow_function().map(|arrow| arrow.body()) {
|
||||
Some(FnBody::Expr(body))
|
||||
if !matches!(
|
||||
body.kind(),
|
||||
ExprKind::Assign { .. }
|
||||
| ExprKind::Jsx(_)
|
||||
| ExprKind::Binary {
|
||||
op: BinOp::Comma,
|
||||
..
|
||||
}
|
||||
) =>
|
||||
{
|
||||
body
|
||||
}
|
||||
_ => return e,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_in_dropped_parentheses_go_behind_semicolon`]: where the comments start that go behind
|
||||
/// the `;` after `e`, if there are any in `e` or in the parentheses around it.
|
||||
///
|
||||
/// `is_in_such_a_place`: see [`keeps_comment_in_parentheses`]. `None`: it makes no difference.
|
||||
pub(crate) fn start_of_comments_in_dropped_parentheses<'a>(
|
||||
e: Expr<'a>,
|
||||
is_in_such_a_place: Option<bool>,
|
||||
f: &Formatter<'a>,
|
||||
) -> Option<u32> {
|
||||
if f.is_quiet()
|
||||
|| !comments_in_dropped_parentheses_go_behind_semicolon(f)
|
||||
|| is_in_such_a_place.is_some_and(|it| keeps_comment_in_parentheses(e, it))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let last = last_of_assignments_and_arrows(e);
|
||||
let position = last.span().end;
|
||||
let has_comments = f
|
||||
.comments()
|
||||
.has_comment_in_span(Span::after(last.span(), e.outer_span().end));
|
||||
// One that is about what is before it has to be seen there.
|
||||
(has_comments
|
||||
&& !is_cast_target(last, f)
|
||||
&& !f.comments().has_trailing_suppression_comment(position))
|
||||
.then_some(position)
|
||||
}
|
||||
|
||||
/// The same for the `;` of `statement`.
|
||||
pub(crate) fn start_of_comments_behind_semicolon<'a>(
|
||||
statement: Stmt<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> Option<u32> {
|
||||
if f.is_quiet() || !comments_in_dropped_parentheses_go_behind_semicolon(f) {
|
||||
return None;
|
||||
}
|
||||
match statement.kind() {
|
||||
StmtKind::Expr(e) => start_of_comments_in_dropped_parentheses(e, Some(false), f),
|
||||
StmtKind::Var(declarations) => (declarations.last())
|
||||
.and_then(VarDecl::init)
|
||||
.and_then(|e| start_of_comments_in_dropped_parentheses(e, Some(true), f)),
|
||||
// Those in the parentheses around all of it stay there.
|
||||
StmtKind::Return(Some(e)) => start_of_comments_in_dropped_parentheses(e, None, f)
|
||||
.filter(|&position| position < e.span().end),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::js::parentheses::expression::has_own_line_comment_between;
|
||||
use super::return_or_throw_statement::has_argument_leading_comments;
|
||||
use crate::js::utils::typecast::is_cast_target;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -15,29 +16,6 @@ fn sequence_after_return_is_a_group_of_its_own(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `((/* comment */ a), b)`: the parentheses of `a` go, and for oxfmt the comment in them leads the sequence: it is written
|
||||
/// before the parentheses that the sequence gets. For Prettier it leads `a`. Returns what the comments before `e` are the
|
||||
/// comments before: oxc's `sequence_leading_comments_start`.
|
||||
pub(crate) fn span_that_comments_lead<'a>(e: Expr<'a>, f: &Formatter<'a>) -> Span {
|
||||
let span = e.span();
|
||||
if !f.options().flavor.is_oxfmt()
|
||||
|| !matches!(
|
||||
e.kind(),
|
||||
ExprKind::Binary {
|
||||
op: BinOp::Comma,
|
||||
..
|
||||
}
|
||||
)
|
||||
// A type cast comment is about the parentheses behind it, which stay.
|
||||
|| f.comments().is_cast_parenthesis(span.start)
|
||||
{
|
||||
return span;
|
||||
}
|
||||
e.sequence()
|
||||
.first()
|
||||
.map_or(span, |first| Span::new(first.span().start, span.end))
|
||||
}
|
||||
|
||||
/// `a, b, c`
|
||||
pub(crate) fn write_sequence_expression<'a>(e: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let parent = e.ast_parent();
|
||||
@@ -70,48 +48,19 @@ pub(crate) fn write_sequence_expression<'a>(e: Expr<'a>, f: &mut Formatter<'a>)
|
||||
let format_inner = format_with(|f| {
|
||||
f.join_with(format_args!(",", soft_line_break_or_space()))
|
||||
.entries(all.iter());
|
||||
write_comments_before_closing_parenthesis(e, f);
|
||||
});
|
||||
match parent {
|
||||
// Whether it breaks is decided at the `(`.
|
||||
AstNodes::ExpressionStatement(_) => write!(f, group(&soft_block_indent(&format_inner))),
|
||||
// The statement has a group for the parentheses and what is in them, unless a comment
|
||||
// breaks them.
|
||||
AstNodes::ReturnStatement(statement) | AstNodes::ThrowStatement(statement)
|
||||
if !sequence_after_return_is_a_group_of_its_own(f)
|
||||
&& !has_own_line_comment_between(statement.span().start, e.span().start, f) =>
|
||||
// It is in parentheses of its own, unless the statement writes parentheses around a comment and what follows it.
|
||||
AstNodes::ReturnStatement(_) | AstNodes::ThrowStatement(_)
|
||||
if !is_cast_target(e, f) && !has_argument_leading_comments(e, f) =>
|
||||
{
|
||||
write!(f, format_inner);
|
||||
match sequence_after_return_is_a_group_of_its_own(f) {
|
||||
true => write!(f, group(&soft_block_indent(&group(&format_inner)))),
|
||||
false => write!(f, group(&soft_block_indent(&format_inner))),
|
||||
}
|
||||
}
|
||||
_ => write!(f, group(&format_inner)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `handleParenthesizedExpressionTrailingComment`: the comments between the end of `e`,
|
||||
/// a sequence or an assignment, and the `)` after it trail the last expression in it.
|
||||
pub(crate) fn write_comments_before_closing_parenthesis<'a>(e: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let is_whole_side = match e.ast_parent() {
|
||||
AstNodes::ExpressionStatement(statement) => statement.is_arrow_function_body(),
|
||||
AstNodes::VariableDeclarator(declarator) => declarator.init() == Some(e),
|
||||
AstNodes::AssignmentExpression(assignment) => assignment.right() == Some(e),
|
||||
AstNodes::ReturnStatement(_) => true,
|
||||
_ => false,
|
||||
};
|
||||
if !is_whole_side {
|
||||
return;
|
||||
}
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_in(Span::after(e.span(), e.outer_span().end));
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|comment| !comment.preceded_by_newline())
|
||||
.count();
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments.get(..count).unwrap_or_default())
|
||||
);
|
||||
}
|
||||
+311
-478
File diff suppressed because it is too large.
Load diff
@@ -1,12 +1,12 @@
|
||||
use super::program::{FormatStatements, is_next_line_empty};
|
||||
use crate::js::format::write_declaration;
|
||||
use crate::js::trivia::{comments_stay_between_head_and_body, write_trailing_comments_before};
|
||||
use crate::js::trivia::comments_stay_between_head_and_body;
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::statement_body::FormatStatementBody;
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::js::utils::statement_body::{write_comments_before_token, write_space_before_body};
|
||||
use crate::js::utils::typecast::comments_of_expression;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_switch_statement<'a>(
|
||||
statement: Stmt<'a>,
|
||||
discriminant: Expr<'a>,
|
||||
@@ -29,12 +29,10 @@ pub(crate) fn write_switch_statement<'a>(
|
||||
]
|
||||
);
|
||||
if cases.is_empty() {
|
||||
match f.comments().has_comment_before(statement.span().end) {
|
||||
true => write!(
|
||||
f,
|
||||
format_dangling_comments(statement.span()).with_block_indent()
|
||||
),
|
||||
false => write!(f, hard_line_break()),
|
||||
let node = AstNodes::SwitchStatement(statement);
|
||||
match f.comments().of_node(node).dangling.is_empty() {
|
||||
true => write!(f, hard_line_break()),
|
||||
false => write!(f, dangling_comments(node).with_block_indent()),
|
||||
}
|
||||
return write!(f, "}");
|
||||
}
|
||||
@@ -56,92 +54,78 @@ pub(crate) fn write_switch_statement<'a>(
|
||||
|
||||
/// `case a: ..`, `default: ..`
|
||||
pub(crate) fn write_switch_case<'a>(case: Case<'a>, f: &mut Formatter<'a>) {
|
||||
// `case 1: a(); // prettier-ignore`: the comment is not in the `case`, so it trails all of it.
|
||||
if !f.is_quiet()
|
||||
&& f.comments()
|
||||
.has_trailing_suppression_comment(case.span().end)
|
||||
{
|
||||
return write!(f, FormatSuppressedNode(case.span()));
|
||||
}
|
||||
let consequent = case.body();
|
||||
let mut statements = consequent
|
||||
.iter()
|
||||
.filter(|it| !matches!(it.kind(), StmtKind::Empty));
|
||||
if !f.is_quiet()
|
||||
let first_statement = statements.next();
|
||||
let only_block = first_statement
|
||||
.filter(|it| matches!(it.kind(), StmtKind::Block(_)) && statements.next().is_none());
|
||||
if let Some(block) = only_block
|
||||
&& !f.is_quiet()
|
||||
&& comments_stay_between_head_and_body(f)
|
||||
&& let (Some(block), None) = (statements.clone().next(), statements.clone().nth(1))
|
||||
&& matches!(block.kind(), StmtKind::Block(_))
|
||||
{
|
||||
match case.test() {
|
||||
Some(test) => {
|
||||
write!(
|
||||
f,
|
||||
["case", space(), FormatNodeWithoutTrailingComments(&test)]
|
||||
);
|
||||
let follows_line_comment = write_trailing_comments_before(test.span().end, b':', f);
|
||||
write!(f, [":", follows_line_comment.then_some(hard_line_break())]);
|
||||
}
|
||||
None => write!(f, ["default", ":"]),
|
||||
}
|
||||
return write!(f, FormatStatementBody::new(block));
|
||||
return write_switch_case_with_comments_in_place(case, block, f);
|
||||
}
|
||||
|
||||
match case.test() {
|
||||
Some(test) => write!(f, ["case", space(), test, ":"]),
|
||||
None => write!(f, ["default", ":"]),
|
||||
None => write!(f, "default:"),
|
||||
}
|
||||
|
||||
let Some(first_statement) = statements.next() else {
|
||||
// `default /* comment */:`
|
||||
if case.test().is_none() && f.comments().has_comment_before(case.span().end) {
|
||||
write!(f, [space(), format_dangling_comments(case.span())]);
|
||||
}
|
||||
// `default: // comment`
|
||||
let node = AstNodes::SwitchCase(case);
|
||||
if !f.is_quiet() && !f.comments().of_node(node).dangling.is_empty() {
|
||||
write!(f, [space(), dangling_comments(node)]);
|
||||
}
|
||||
if first_statement.is_none() {
|
||||
return;
|
||||
};
|
||||
// The `{` of a block that is all there is goes on the line of the `case`.
|
||||
let is_single_block_statement =
|
||||
matches!(first_statement.kind(), StmtKind::Block(_)) && statements.next().is_none();
|
||||
|
||||
// Prettier's `handleSwitchDefaultCaseComments`: the comments on the line of `default:` stay
|
||||
// there, but for a line comment before a block, which is written in the block.
|
||||
if case.test().is_none() && !f.is_quiet() && consequent.first() == Some(first_statement) {
|
||||
let mut comments = f
|
||||
.comments()
|
||||
.end_of_line_comments_after(case.span().start + "default".len() as u32);
|
||||
let mut is_comment_in_block = false;
|
||||
if let [rest @ .., last] = comments
|
||||
&& last.is_line()
|
||||
&& matches!(first_statement.kind(), StmtKind::Block(_))
|
||||
{
|
||||
comments = rest;
|
||||
is_comment_in_block = true;
|
||||
}
|
||||
if !comments.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
space(),
|
||||
FormatDanglingComments::Comments {
|
||||
comments,
|
||||
indent: DanglingIndentMode::None
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
if is_comment_in_block && is_single_block_statement {
|
||||
write!(f, space());
|
||||
return write_declaration(first_statement, f);
|
||||
}
|
||||
}
|
||||
|
||||
if is_single_block_statement {
|
||||
write!(f, [space(), FormatStatements(consequent)]);
|
||||
} else {
|
||||
write!(
|
||||
// The `{` of a block that is all there is goes on the line of the `case`.
|
||||
match only_block {
|
||||
Some(_) => write!(f, [space(), FormatStatements(consequent)]),
|
||||
None => write!(
|
||||
f,
|
||||
indent(&format_args!(
|
||||
hard_line_break(),
|
||||
FormatStatements(consequent)
|
||||
))
|
||||
);
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_stay_between_head_and_body`]: a `case` whose only statement is `block`. The comments before the `:` and
|
||||
/// before the `{` keep their lines.
|
||||
///
|
||||
/// ```js
|
||||
/// case a: // comment
|
||||
/// {
|
||||
/// }
|
||||
/// case b
|
||||
/// // comment
|
||||
/// : {
|
||||
/// }
|
||||
/// ```
|
||||
fn write_switch_case_with_comments_in_place<'a>(
|
||||
case: Case<'a>,
|
||||
block: Stmt<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let mut follows_line_comment = false;
|
||||
match case.test() {
|
||||
Some(test) => {
|
||||
write!(
|
||||
f,
|
||||
["case", space(), FormatNodeWithoutTrailingComments(&test)]
|
||||
);
|
||||
follows_line_comment =
|
||||
write_comments_before_token(comments_of_expression(test, f).trailing, f);
|
||||
write!(f, ":");
|
||||
}
|
||||
None => write!(f, "default:"),
|
||||
}
|
||||
match follows_line_comment {
|
||||
true => write!(f, hard_line_break()),
|
||||
false => write_space_before_body(block.as_ast_nodes(), f),
|
||||
}
|
||||
write!(f, FormatStatements(case.body()));
|
||||
}
|
||||
@@ -8,10 +8,10 @@ use crate::css::embed;
|
||||
use crate::cursor::around_node_at;
|
||||
use crate::ir::width::string_width_as;
|
||||
use crate::js::utils::call_expression::is_test_each_pattern;
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::tailwindcss::{
|
||||
InTailwindContext, context_of_function, has_white_space, sorted_template_text,
|
||||
};
|
||||
use crate::js::utils::typecast::has_comments;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -33,12 +33,24 @@ fn can_be_in_another_language<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `isJestEachTemplateLiteral`, and the test of `printJestEachTemplateLiteral` that the table has a header.
|
||||
fn is_jest_each_template_literal<'a>(e: Expr<'a>, template: Template<'a>) -> bool {
|
||||
matches!(e.parent(), Node::Expr(parent)
|
||||
if matches!(parent.kind(), ExprKind::TaggedTemplate(call)
|
||||
if call.template() == Some(e) && is_test_each_pattern(call.callee())))
|
||||
&& !template.raw(0).trim_ascii().is_empty()
|
||||
}
|
||||
|
||||
/// `` `a${b}c` ``
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_template_literal<'a>(
|
||||
e: Expr<'a>,
|
||||
template: Template<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if is_jest_each_template_literal(e, template) {
|
||||
return EachTemplateTable::from_template(template, f).fmt(f);
|
||||
}
|
||||
let context = f.context().tailwind_context();
|
||||
let content = format_with(|f| {
|
||||
if !can_be_in_another_language(e, f)
|
||||
@@ -71,25 +83,13 @@ pub(crate) fn write_embedded_template_expression<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// ``a /* comment */ ⏎ `b` ``: for oxfmt every comment between a tag and its template leads the template. For Prettier
|
||||
/// one that ends its line trails the tag.
|
||||
fn comments_behind_tag_lead_the_template(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `` tag`a${b}c` ``
|
||||
/// `` tag`a${b}c` ``. Prettier's `printTaggedTemplateExpression`.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_tagged_template_expression<'a>(
|
||||
e: Expr<'a>,
|
||||
call: Call<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let tag_end = call
|
||||
.type_args()
|
||||
.angle_brackets_span()
|
||||
.map_or_else(|| call.callee().span().end, |it| it.end);
|
||||
let leads_template = comments_behind_tag_lead_the_template(f);
|
||||
let view_limit =
|
||||
(leads_template && !f.is_quiet()).then(|| f.comments_mut().limit_comments_up_to(tag_end));
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
@@ -97,40 +97,24 @@ pub(crate) fn write_tagged_template_expression<'a>(
|
||||
type_arguments(call.type_args(), Node::Expr(e))
|
||||
]
|
||||
);
|
||||
if let Some(view_limit) = view_limit {
|
||||
f.comments_mut().restore_view_limit(view_limit);
|
||||
}
|
||||
let Some(quasi) = call.template() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let comments = f.comments().comments_before(quasi.span().start);
|
||||
if let Some(first) = comments.first() {
|
||||
match f
|
||||
.source_text()
|
||||
.contains_newline(Span::before(tag_end, first.span))
|
||||
{
|
||||
if !f.is_quiet()
|
||||
&& let Some(comment) = f.comments().of_node(quasi.as_ast_nodes()).leading.first()
|
||||
{
|
||||
let tag_end = (call.type_args().angle_brackets_span())
|
||||
.map_or_else(|| call.callee().span().end, |it| it.end);
|
||||
match (f.source_text()).contains_newline(Span::before(tag_end, comment.span())) {
|
||||
true => write!(f, soft_line_break()),
|
||||
false => write!(f, space()),
|
||||
}
|
||||
}
|
||||
let comments = FormatLeadingComments::Comments(comments);
|
||||
match leads_template {
|
||||
true => write!(f, [comments, line_suffix_boundary()]),
|
||||
false => write!(f, [line_suffix_boundary(), comments]),
|
||||
}
|
||||
|
||||
let ExprKind::Template(template) = quasi.kind() else {
|
||||
return;
|
||||
};
|
||||
// Without a header it is not a table.
|
||||
let content = format_with(|f| {
|
||||
match is_test_each_pattern(call.callee()) && !template.raw(0).trim_ascii().is_empty() {
|
||||
true => EachTemplateTable::from_template(template, f).fmt(f),
|
||||
false => write_template_literal(quasi, template, f),
|
||||
}
|
||||
});
|
||||
InTailwindContext(context_of_function(call.callee(), f), content).fmt(f);
|
||||
let context = context_of_function(call.callee(), f);
|
||||
write!(
|
||||
f,
|
||||
[line_suffix_boundary(), InTailwindContext(context, quasi)]
|
||||
);
|
||||
}
|
||||
|
||||
/// `` `a${B}c` `` in a type
|
||||
@@ -329,29 +313,18 @@ enum Place {
|
||||
After(TemplateElementIndention),
|
||||
}
|
||||
|
||||
/// Prettier's `hasComment(node)`. oxfmt only looks at the comments of expressions.
|
||||
fn type_has_comment<'a>(ty: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
!f.is_quiet() && !f.options().flavor.is_oxfmt() && f.comments().has_comments(ty.as_ast_nodes())
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatTemplateExpression<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let mut has_comment_in_expression = false;
|
||||
|
||||
let interned_expression = match self.expression {
|
||||
let (has_comment, interned_expression) = match self.expression {
|
||||
TemplateExpression::Expression(e) => {
|
||||
has_comment_in_expression = !f.is_quiet()
|
||||
&& (f.comments().has_comment_before(e.span().start)
|
||||
|| f.comments()
|
||||
.has_comment_in_range(e.span().end, self.interpolation.end));
|
||||
f.intern(&format_with(|f| {
|
||||
match e.kind() {
|
||||
ExprKind::Jsx(_) => e.fmt(f),
|
||||
_ => FormatNodeWithoutTrailingComments(&e).fmt(f),
|
||||
}
|
||||
if !f.is_quiet() {
|
||||
let trailing_comments =
|
||||
f.comments().comments_before(self.interpolation.end);
|
||||
FormatTrailingComments::Comments(trailing_comments).fmt(f);
|
||||
}
|
||||
}))
|
||||
(!f.is_quiet() && has_comments(e, f), f.intern(&e))
|
||||
}
|
||||
TemplateExpression::TSType(t) => f.intern(&t),
|
||||
TemplateExpression::TSType(t) => (type_has_comment(t, f), f.intern(&t)),
|
||||
};
|
||||
let Some(element) = interned_expression else {
|
||||
return write!(f, "${}");
|
||||
@@ -374,26 +347,26 @@ impl<'a> Format<'a> for FormatTemplateExpression<'a> {
|
||||
}
|
||||
TemplateElementLayout::SingleLine => f.write_without_soft_lines(&content),
|
||||
TemplateElementLayout::Fit => {
|
||||
let indent = matches!(self.expression, TemplateExpression::Expression(e) if {
|
||||
// Prettier's `stripChainElementWrappers`
|
||||
let mut stripped = e;
|
||||
while let ExprKind::NonNull(inner) = stripped.kind() {
|
||||
stripped = inner;
|
||||
}
|
||||
has_comment_in_expression
|
||||
|| matches!(stripped.kind(), ExprKind::Dot { .. } | ExprKind::Index { .. })
|
||||
|| matches!(
|
||||
e.as_ast_nodes(),
|
||||
AstNodes::ConditionalExpression(_)
|
||||
| AstNodes::SequenceExpression(_)
|
||||
| AstNodes::TSAsExpression(_)
|
||||
| AstNodes::TSSatisfiesExpression(_)
|
||||
| AstNodes::BinaryExpression(_)
|
||||
| AstNodes::LogicalExpression(_)
|
||||
| AstNodes::PrivateInExpression(_)
|
||||
| AstNodes::IdentifierReference(_)
|
||||
)
|
||||
});
|
||||
let indent = has_comment
|
||||
|| matches!(self.expression, TemplateExpression::Expression(e) if {
|
||||
// Prettier's `stripChainElementWrappers`
|
||||
let mut stripped = e;
|
||||
while let ExprKind::NonNull(inner) = stripped.kind() {
|
||||
stripped = inner;
|
||||
}
|
||||
matches!(stripped.kind(), ExprKind::Dot { .. } | ExprKind::Index { .. })
|
||||
|| matches!(
|
||||
e.as_ast_nodes(),
|
||||
AstNodes::ConditionalExpression(_)
|
||||
| AstNodes::SequenceExpression(_)
|
||||
| AstNodes::TSAsExpression(_)
|
||||
| AstNodes::TSSatisfiesExpression(_)
|
||||
| AstNodes::BinaryExpression(_)
|
||||
| AstNodes::LogicalExpression(_)
|
||||
| AstNodes::PrivateInExpression(_)
|
||||
| AstNodes::IdentifierReference(_)
|
||||
)
|
||||
});
|
||||
match indent {
|
||||
true => write!(f, soft_block_indent(&content)),
|
||||
false => write!(f, content),
|
||||
|
||||
@@ -1,41 +1,31 @@
|
||||
use super::block_statement::is_empty_block;
|
||||
use crate::js::format::{
|
||||
FormatTypeAnnotation, format_node_without_comments, write_declaration,
|
||||
write_trailing_comments_of,
|
||||
};
|
||||
use crate::js::trivia::{
|
||||
comments_stay_between_head_and_body, write_comments_between_blocks, write_head_body_separator,
|
||||
};
|
||||
use crate::js::format::{FormatTypeAnnotation, format_node};
|
||||
use crate::js::trivia::comments_stay_between_head_and_body;
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::statement_body::{write_comments_before_keyword, write_space_before_body};
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
use bun_core::strings;
|
||||
|
||||
/// Whether one of `comments` starts or ends its line. Prettier's `handleTryStatementComments` moves
|
||||
/// those into the next block.
|
||||
fn has_comment_with_line_break(comments: &[Comment]) -> bool {
|
||||
comments
|
||||
.iter()
|
||||
.any(|comment| comment.preceded_by_newline() || comment.followed_by_newline())
|
||||
}
|
||||
|
||||
/// A space and a block after `try`, `catch (e)` or `finally`. The comments after it are left to the
|
||||
/// caller.
|
||||
fn write_block<'a>(block: Stmt<'a>, f: &mut Formatter<'a>) {
|
||||
if !f.is_quiet() && comments_stay_between_head_and_body(f) {
|
||||
write_head_body_separator(block.span().start, f);
|
||||
return write!(f, FormatNodeWithoutTrailingComments(&block));
|
||||
/// Writes `part`, which is followed by `catch` or `finally`, and the space before that keyword.
|
||||
///
|
||||
/// See [`comments_stay_between_head_and_body`]: for oxfmt the comments between the two keep their lines. They are the
|
||||
/// trailing comments of `node`, which `part` writes, and `comments_before_keyword`.
|
||||
fn write_part_before_keyword<'a>(
|
||||
part: &(impl Format<'a> + Spanned),
|
||||
node: AstNodes<'a>,
|
||||
comments_before_keyword: impl FnOnce(&Formatter<'a>) -> CommentList<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if f.is_quiet() || !comments_stay_between_head_and_body(f) {
|
||||
return write!(f, [part, space()]);
|
||||
}
|
||||
write!(f, space());
|
||||
match !f.is_quiet()
|
||||
&& has_comment_with_line_break(f.comments().comments_before(block.span().start))
|
||||
{
|
||||
// The comments before it are written in it.
|
||||
true => write_declaration(block, f),
|
||||
false => write!(f, FormatNodeWithoutTrailingComments(&block)),
|
||||
write!(f, FormatNodeWithoutTrailingComments(part));
|
||||
let trailing = f.comments().of_node(node).trailing;
|
||||
if write_comments_before_keyword(trailing, comments_before_keyword(f), f) {
|
||||
write!(f, space());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_try_statement<'a>(
|
||||
statement: Stmt<'a>,
|
||||
block: Stmt<'a>,
|
||||
@@ -45,147 +35,96 @@ pub(crate) fn write_try_statement<'a>(
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
write!(f, "try");
|
||||
write_block(block, f);
|
||||
|
||||
if let Some(handler) = handler {
|
||||
write!(f, space());
|
||||
let node = AstNodes::CatchClause(statement);
|
||||
format_node_without_comments(
|
||||
node.span(),
|
||||
|| node.parent(),
|
||||
f,
|
||||
|f| write_catch_clause(node, param, handler, f),
|
||||
);
|
||||
write_space_before_body(block.as_ast_nodes(), f);
|
||||
// oxfmt writes the comments of an ignored clause like any other leading comments: `} // prettier-ignore⏎catch ( e ) {}`
|
||||
let before_catch = |f: &Formatter<'a>| {
|
||||
let owned = f.comments().of_node(AstNodes::CatchClause(statement));
|
||||
match owned.is_ignored {
|
||||
true => CommentList::empty(),
|
||||
false => owned.leading,
|
||||
}
|
||||
};
|
||||
let before_finally = |f: &Formatter<'a>| {
|
||||
f.comments()
|
||||
.of_node(AstNodes::TryStatement(statement))
|
||||
.dangling
|
||||
};
|
||||
match handler {
|
||||
Some(_) => write_part_before_keyword(&block, block.as_ast_nodes(), before_catch, f),
|
||||
None => write_part_before_keyword(&block, block.as_ast_nodes(), before_finally, f),
|
||||
}
|
||||
if let Some(body) = handler {
|
||||
let catch_clause = FormatCatchClause {
|
||||
statement,
|
||||
param,
|
||||
body,
|
||||
};
|
||||
match finalizer {
|
||||
Some(_) => {
|
||||
write_part_before_keyword(&catch_clause, catch_clause.node(), before_finally, f);
|
||||
}
|
||||
None => write!(f, catch_clause),
|
||||
}
|
||||
}
|
||||
if let Some(finalizer) = finalizer {
|
||||
// `} /* comment */ finally {`
|
||||
if !f.is_quiet() && comments_stay_between_head_and_body(f) {
|
||||
let previous = handler.unwrap_or(block).span();
|
||||
write_comments_between_blocks(
|
||||
f.comments().comments_before_character(previous.end, b'f'),
|
||||
f,
|
||||
);
|
||||
} else if !f.is_quiet() {
|
||||
let comments = f.comments().comments_before(finalizer.span().start);
|
||||
let mut position = handler.unwrap_or(block).span().end;
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|comment| {
|
||||
let gap = f
|
||||
.source_text()
|
||||
.text_for(&Span::before(position, comment.span));
|
||||
position = comment.span.end;
|
||||
strings::trim_js_whitespace(gap).is_empty()
|
||||
})
|
||||
.count();
|
||||
let comments = comments.get(..count).unwrap_or_default();
|
||||
if !has_comment_with_line_break(comments) {
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
write!(f, [space(), "finally"]);
|
||||
write_block(finalizer, f);
|
||||
write!(f, "finally");
|
||||
write_space_before_body(finalizer.as_ast_nodes(), f);
|
||||
write!(f, finalizer);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_catch_clause<'a>(
|
||||
node: AstNodes<'a>,
|
||||
/// `catch (e) {}`
|
||||
#[derive(Copy, Clone)]
|
||||
struct FormatCatchClause<'a> {
|
||||
/// The `try` statement.
|
||||
statement: Stmt<'a>,
|
||||
param: Option<VarDecl<'a>>,
|
||||
body: Stmt<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
// ```js
|
||||
// try {
|
||||
// } // comment
|
||||
// catch {
|
||||
// }
|
||||
// ```
|
||||
// A comment with a line break next to it goes to the start of the block, where
|
||||
// `write_block_statement` takes it from the cache. Any other stays before `catch`.
|
||||
let leading_comments = f.comments().comments_before(node.span().start);
|
||||
if !f.is_quiet() && comments_stay_between_head_and_body(f) {
|
||||
if write_comments_between_blocks(leading_comments, f) {
|
||||
write!(f, space());
|
||||
}
|
||||
} else if has_comment_with_line_break(leading_comments) {
|
||||
let is_empty =
|
||||
matches!(body.kind(), StmtKind::Block(statements) if is_empty_block(statements));
|
||||
let format_comments = format_with(|f| match is_empty {
|
||||
true => {
|
||||
let comments = FormatDanglingComments::Comments {
|
||||
comments: leading_comments,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
write!(f, [comments, hard_line_break()]);
|
||||
}
|
||||
false => write!(f, FormatLeadingComments::Comments(leading_comments)),
|
||||
});
|
||||
if let Some(comments) = f.intern(&format_comments) {
|
||||
f.context_mut().cache_element(&node.span(), comments);
|
||||
}
|
||||
} else if !leading_comments.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
[FormatTrailingComments::Comments(leading_comments), space()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FormatCatchClause<'a> {
|
||||
fn node(self) -> AstNodes<'a> {
|
||||
AstNodes::CatchClause(self.statement)
|
||||
}
|
||||
}
|
||||
|
||||
impl Spanned for FormatCatchClause<'_> {
|
||||
fn span(&self) -> Span {
|
||||
self.node().span()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatCatchClause<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
format_node(self.node(), f, |f| {
|
||||
write_catch_clause(self.param, self.body, f)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn write_catch_clause<'a>(param: Option<VarDecl<'a>>, body: Stmt<'a>, f: &mut Formatter<'a>) {
|
||||
write!(f, "catch");
|
||||
if let Some(param) = param {
|
||||
write!(f, space());
|
||||
format_node_without_comments(
|
||||
param.span(),
|
||||
|| node,
|
||||
f,
|
||||
|f| write_catch_parameter(param, body, f),
|
||||
);
|
||||
}
|
||||
write_block(body, f);
|
||||
}
|
||||
|
||||
/// `(e)`
|
||||
fn write_catch_parameter<'a>(param: VarDecl<'a>, body: Stmt<'a>, f: &mut Formatter<'a>) {
|
||||
let (pattern, type_annotation) = (param.pat(), param.ty().map(FormatTypeAnnotation));
|
||||
write!(f, "(");
|
||||
if f.is_quiet() {
|
||||
return write!(f, [pattern, type_annotation, ")"]);
|
||||
}
|
||||
|
||||
let leading_comments = f.comments().comments_before(pattern.span().start);
|
||||
let leading_comment_with_break = leading_comments
|
||||
.iter()
|
||||
.any(|comment| comment.is_line() || comment.followed_by_newline());
|
||||
// Those before the `)`, and of those after it the ones that start or end their line.
|
||||
let before_body = f.comments().comments_in(param.span().between(body.span()));
|
||||
let count = f
|
||||
.comments()
|
||||
.comments_before_character(param.span().end, b')')
|
||||
.len()
|
||||
.max(match comments_stay_between_head_and_body(f) {
|
||||
true => 0,
|
||||
false => before_body
|
||||
.iter()
|
||||
.rposition(|it| it.preceded_by_newline() || it.followed_by_newline())
|
||||
.map_or(0, |at| at + 1),
|
||||
let node = AstNodes::CatchParameter(param);
|
||||
let parameter = format_with(|f| {
|
||||
format_node(node, f, |f| {
|
||||
write!(f, [param.pat(), param.ty().map(FormatTypeAnnotation)]);
|
||||
});
|
||||
});
|
||||
let trailing_comments = before_body.get(..count).unwrap_or_default();
|
||||
let trailing_comment_with_break = trailing_comments
|
||||
.iter()
|
||||
.any(|comment| comment.is_line() || comment.preceded_by_newline());
|
||||
|
||||
let content = format_with(|f| {
|
||||
write!(f, FormatLeadingComments::Comments(leading_comments));
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(param.span().end);
|
||||
write!(f, [pattern, type_annotation]);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
write!(f, FormatTrailingComments::Comments(trailing_comments));
|
||||
});
|
||||
match leading_comment_with_break || trailing_comment_with_break {
|
||||
true => write!(f, soft_block_indent(&content)),
|
||||
false => write!(f, content),
|
||||
match !f.is_quiet() && has_comment_with_line_break(node, f) {
|
||||
true => write!(f, [space(), "(", soft_block_indent(¶meter), ")"]),
|
||||
false => write!(f, [space(), "(", parameter, ")"]),
|
||||
}
|
||||
}
|
||||
if !comments_stay_between_head_and_body(f) {
|
||||
write_trailing_comments_of(AstNodes::CatchParameter(param), f);
|
||||
}
|
||||
write!(f, ")");
|
||||
write_space_before_body(body.as_ast_nodes(), f);
|
||||
write!(f, body);
|
||||
}
|
||||
|
||||
/// Whether a comment of the catch parameter `node` is a line comment or has a line break between it and the parameter.
|
||||
fn has_comment_with_line_break<'a>(node: AstNodes<'a>, f: &Formatter<'a>) -> bool {
|
||||
let owned = f.comments().of_node(node);
|
||||
let mut leading = owned.leading.iter();
|
||||
let mut trailing = owned.trailing.iter();
|
||||
leading.any(|comment| comment.is_line() || comment.has_newline_after())
|
||||
|| trailing.any(|comment| comment.is_line() || comment.has_newline_before())
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
//! The declarations of TypeScript.
|
||||
|
||||
use super::block_statement::is_empty_block;
|
||||
use super::class::FormatClassImplements;
|
||||
use super::import_declaration::FormatStringLiteral;
|
||||
use super::intersection_type::lone_type_of_intersection_is_a_group;
|
||||
use super::program::FormatStatements;
|
||||
use super::semicolon::OptionalSemicolon;
|
||||
use super::ts_types::{FormatModuleSpecifier, entity_name, write_ts_interface_signatures};
|
||||
use super::ts_types::{entity_name, write_ts_interface_signatures};
|
||||
use super::type_parameters::{FormatTSTypeParametersOptions, type_parameters};
|
||||
use super::union_type::{
|
||||
type_alias_union_breaks_after_operator, union_breaks_one_per_line, union_prints_itself,
|
||||
write_ts_union_type_in,
|
||||
union_prints_own_comments, write_ts_union_type_in,
|
||||
};
|
||||
use crate::cursor::around_node;
|
||||
use crate::js::format::{format_node, identifier, write_trailing_comments_of};
|
||||
use crate::js::trivia::{comments_stay_between_head_and_body, write_head_body_separator};
|
||||
use crate::js::utils::assignment_like::{
|
||||
AssignmentLikeLayout, operator_line_run, write_comments_before_operator,
|
||||
use crate::js::comments::OwnedComments;
|
||||
use crate::js::format::{
|
||||
format_ignorable_name, format_name, format_node, identifier, write_ignored_node,
|
||||
};
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::assignment_like::AssignmentLikeLayout;
|
||||
use crate::js::utils::object::{FormatKey, key_requires_quotes};
|
||||
use crate::js::utils::statement_body::write_space_before_body;
|
||||
use crate::js::utils::typescript::{should_hug_type, without_lone_operator};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
@@ -31,7 +30,50 @@ fn is_declared(statement: Stmt<'_>) -> bool {
|
||||
.any(|it| it.flag() == Flags::AMBIENT)
|
||||
}
|
||||
|
||||
/// `interface A<T> extends B { .. }`
|
||||
/// The name or the type parameters of an interface, with the comments `owned` by it. Prettier's `printClass` indents the
|
||||
/// trailing comments.
|
||||
fn write_part_of_interface_head<'a>(
|
||||
owned: &OwnedComments<'a>,
|
||||
part: &impl Format<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let trailing_comments = FormatTrailingComments::Comments(owned.trailing);
|
||||
write!(f, [FormatLeadingComments::Comments(owned.leading), part]);
|
||||
match only_comments_at_end_or_start_of_line_are_indented(f)
|
||||
&& !(owned.trailing.iter())
|
||||
.any(|comment| comment.has_newline_before() || comment.has_newline_after())
|
||||
{
|
||||
true => write!(f, trailing_comments),
|
||||
false => write!(f, indent(&trailing_comments)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ```ts
|
||||
/// interface A /** interface A /**
|
||||
/// * comment * comment
|
||||
/// */ */
|
||||
/// extends B {} extends B {}
|
||||
/// ```
|
||||
///
|
||||
/// oxfmt on the left, Prettier on the right.
|
||||
fn only_comments_at_end_or_start_of_line_are_indented(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// ```ts
|
||||
/// interface A< // comment interface A< // comment
|
||||
/// T, T,
|
||||
/// > extends B {} >
|
||||
/// extends B {}
|
||||
/// ```
|
||||
///
|
||||
/// oxfmt on the left, Prettier on the right. The comment trails the name.
|
||||
fn only_comments_before_extends_break_the_head(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `interface A<T> extends B { .. }`. Prettier's `printClass`.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_interface_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
interface: Interface<'a>,
|
||||
@@ -40,89 +82,82 @@ pub(crate) fn write_ts_interface_declaration<'a>(
|
||||
if f.file().is_flow() && super::flow::is_opaque_type(statement, f) {
|
||||
return super::flow::write_opaque_type(statement, interface, f);
|
||||
}
|
||||
let node = AstNodes::TSInterfaceDeclaration(statement);
|
||||
let id = identifier(interface.name(), node);
|
||||
let name = interface.name().span();
|
||||
let type_params = interface.type_params();
|
||||
let extends = interface.extends();
|
||||
let body_span = interface.body_span();
|
||||
let name_comments = f.comments().of_name(name.start);
|
||||
let type_parameters_node = AstNodes::TSTypeParameterDeclaration(Node::Stmt(statement));
|
||||
let type_parameters_comments = f.comments().of_node(type_parameters_node);
|
||||
|
||||
// Whether the head is a group that can break before `extends`: there are several types, or one
|
||||
// that is `A.B` without type arguments, or a comment trails the name or the type parameters.
|
||||
// Prettier's `shouldPrintClassInGroupMode`: the head can break before `extends`.
|
||||
let group_mode = extends.len() > 1
|
||||
|| extends.first().is_some_and(|first| {
|
||||
let is_member = match first.kind() {
|
||||
TypeKind::Ref { name, args } => name.len() > 1 && args.is_empty(),
|
||||
TypeKind::Heritage { expr, args } => {
|
||||
matches!(expr.kind(), ExprKind::Dot { .. } | ExprKind::Index { .. })
|
||||
&& args.is_empty()
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
// A comment after `extends` on its line leads the type. All others trail what is before.
|
||||
is_member || {
|
||||
let previous = type_params
|
||||
.angle_brackets_span()
|
||||
.unwrap_or_else(|| id.span());
|
||||
let comments = f.comments().comments_in(previous.between(first.span()));
|
||||
comments
|
||||
.iter()
|
||||
.any(|comment| comment.preceded_by_newline() || comment.followed_by_newline())
|
||||
|| comments.first().is_some_and(|comment| {
|
||||
f.source_text()
|
||||
.all_bytes(previous.between(comment.span), |b| b.is_ascii_whitespace())
|
||||
})
|
||||
|| (!name_comments.trailing.is_empty()
|
||||
&& !(only_comments_before_extends_break_the_head(f)
|
||||
&& type_params.angle_brackets_span().is_some()))
|
||||
|| !type_parameters_comments.trailing.is_empty()
|
||||
|| extends.first().is_some_and(|first| match first.kind() {
|
||||
TypeKind::Ref { name, args } => name.len() > 1 && args.is_empty(),
|
||||
TypeKind::Heritage { expr, args } => {
|
||||
matches!(expr.kind(), ExprKind::Dot { .. } | ExprKind::Index { .. })
|
||||
&& args.is_empty()
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let format_id = format_with(|f| {
|
||||
match type_params.is_empty() && extends.is_empty() {
|
||||
true => FormatNodeWithoutTrailingComments(&id).fmt(f),
|
||||
false => write!(f, id),
|
||||
}
|
||||
let format_name_and_type_parameters = format_with(|f| {
|
||||
write_part_of_interface_head(&name_comments, &source_text(name), f);
|
||||
if type_params.angle_brackets_span().is_some() {
|
||||
let options = FormatTSTypeParametersOptions {
|
||||
group_id: None,
|
||||
is_type_or_interface_decl: true,
|
||||
};
|
||||
type_parameters(type_params, Node::Stmt(statement))
|
||||
.with_options(options)
|
||||
.write_without_comments(f);
|
||||
if !extends.is_empty() {
|
||||
write_trailing_comments_of(
|
||||
AstNodes::TSTypeParameterDeclaration(Node::Stmt(statement)),
|
||||
f,
|
||||
);
|
||||
}
|
||||
let type_parameters = format_with(|f| {
|
||||
if type_parameters_comments.is_ignored {
|
||||
return write_ignored_node(type_parameters_node, f);
|
||||
}
|
||||
type_parameters(type_params, Node::Stmt(statement))
|
||||
.with_options(options)
|
||||
.write_without_comments(f)
|
||||
});
|
||||
write_part_of_interface_head(&type_parameters_comments, &type_parameters, f);
|
||||
}
|
||||
});
|
||||
|
||||
// Prettier's `printHeritageClauses`.
|
||||
let format_extends = format_with(|f| {
|
||||
let Some(first_extend) = extends.first() else {
|
||||
if extends.is_empty() {
|
||||
return;
|
||||
};
|
||||
if f.comments()
|
||||
.has_leading_own_line_comment(first_extend.span().start)
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(
|
||||
f.comments().comments_before(first_extend.span().start)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let heritage = FormatClassImplements(extends);
|
||||
let comments_before_keyword = (f
|
||||
.comments()
|
||||
.of_node(AstNodes::TSInterfaceDeclaration(statement)))
|
||||
.dangling;
|
||||
let format_comments_before_keyword = FormatDanglingComments::Comments {
|
||||
comments: comments_before_keyword,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
let heritage = format_with(|f| {
|
||||
f.join_with(soft_line_break_or_space())
|
||||
.entries_with_trailing_separator(
|
||||
extends.iter(),
|
||||
",",
|
||||
TrailingSeparator::Disallowed,
|
||||
);
|
||||
});
|
||||
if extends.len() > 1 {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
soft_line_break_or_space(),
|
||||
format_comments_before_keyword,
|
||||
(!comments_before_keyword.is_empty()).then_some(hard_line_break()),
|
||||
"extends",
|
||||
group(&soft_line_indent_or_space(&heritage))
|
||||
]
|
||||
);
|
||||
} else {
|
||||
let format_extends = format_args!("extends", space(), heritage);
|
||||
let format_extends =
|
||||
format_args!("extends", space(), format_comments_before_keyword, heritage);
|
||||
match group_mode {
|
||||
true => write!(f, [soft_line_break_or_space(), group(&format_extends)]),
|
||||
false => write!(f, [space(), format_extends]),
|
||||
@@ -130,6 +165,7 @@ pub(crate) fn write_ts_interface_declaration<'a>(
|
||||
}
|
||||
});
|
||||
|
||||
let body = AstNodes::TSInterfaceBody(statement);
|
||||
let content = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
@@ -139,30 +175,21 @@ pub(crate) fn write_ts_interface_declaration<'a>(
|
||||
space()
|
||||
]
|
||||
);
|
||||
|
||||
if !extends.is_empty() && group_mode {
|
||||
write!(f, group(&format_args!(format_id, indent(&format_extends))));
|
||||
} else {
|
||||
write!(f, [format_id, format_extends]);
|
||||
match group_mode {
|
||||
true => write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
format_name_and_type_parameters,
|
||||
indent(&format_extends)
|
||||
))
|
||||
),
|
||||
false => write!(f, [format_name_and_type_parameters, format_extends]),
|
||||
}
|
||||
|
||||
// The comments on the line of the `{` stay before it. All others are in the body.
|
||||
if !f.is_quiet() && comments_stay_between_head_and_body(f) {
|
||||
write_head_body_separator(body_span.start, f);
|
||||
} else if !f.is_quiet() {
|
||||
let comments = f.comments().comments_before(body_span.start);
|
||||
if !comments
|
||||
.iter()
|
||||
.any(|comment| comment.preceded_by_newline() || comment.followed_by_newline())
|
||||
{
|
||||
write!(f, [space(), FormatLeadingComments::Comments(comments)]);
|
||||
}
|
||||
}
|
||||
write!(f, space());
|
||||
around_node(body_span, f, |f| {
|
||||
write_space_before_body(body, f);
|
||||
format_node(body, f, |f| {
|
||||
write!(f, "{");
|
||||
match interface.members().is_empty() {
|
||||
true => write!(f, format_dangling_comments(body_span).with_block_indent()),
|
||||
true => write!(f, dangling_comments(body).with_block_indent()),
|
||||
false => {
|
||||
write!(
|
||||
f,
|
||||
@@ -179,14 +206,9 @@ pub(crate) fn write_ts_interface_declaration<'a>(
|
||||
write!(f, group(&content));
|
||||
}
|
||||
|
||||
/// `type A = B /* comment */;` is `type A = B; /* comment */` for oxfmt, as after any other statement.
|
||||
/// Prettier does that only if the type is exported.
|
||||
fn comments_before_semicolon_of_type_alias_go_behind_it(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `type A = B`. Prettier's `printTypeAlias`, with what its `printAssignment` does if the right side
|
||||
/// is a type.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_type_alias_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
alias: Alias<'a>,
|
||||
@@ -195,192 +217,141 @@ pub(crate) fn write_ts_type_alias_declaration<'a>(
|
||||
if f.file().is_flow() && super::flow::write_what_is_no_type_alias(statement, alias, f) {
|
||||
return;
|
||||
}
|
||||
let node = AstNodes::TSTypeAliasDeclaration(statement);
|
||||
let ty = without_lone_operator(alias.ty());
|
||||
let right_node = ty.as_ast_nodes();
|
||||
|
||||
// The comments before the `;` of an export are behind it: see `Comments::without_semicolon`.
|
||||
let is_exported = !f.is_quiet()
|
||||
&& (comments_before_semicolon_of_type_alias_go_behind_it(f)
|
||||
|| matches!(
|
||||
statement.as_ast_nodes(),
|
||||
AstNodes::ExportNamedDeclaration(_)
|
||||
));
|
||||
let view_limit = is_exported.then(|| {
|
||||
let end = match comments_before_semicolon_of_type_alias_go_behind_it(f) {
|
||||
// Without the parentheses around the type, which are not written.
|
||||
true => ty.span().end,
|
||||
false => f.comments().without_semicolon(statement.span()).end,
|
||||
};
|
||||
f.comments_mut().limit_comments_up_to(end)
|
||||
let comments_behind_operator = comments_behind_type_alias_operator(statement, f);
|
||||
let has_line_comment_on_operator_line = !comments_behind_operator.is_empty()
|
||||
|| (type_alias_comments_stay_around_operator(f)
|
||||
&& (comments_trailing_type_alias_left_side(statement, f).last())
|
||||
.is_some_and(|comment| comment.is_line()));
|
||||
let layout = match has_line_comment_on_operator_line {
|
||||
true => AssignmentLikeLayout::BreakAfterOperator,
|
||||
false => type_alias_layout(statement, alias, ty, f),
|
||||
};
|
||||
|
||||
let left = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
is_declared(statement).then_some("declare "),
|
||||
"type ",
|
||||
identifier(alias.name())
|
||||
]
|
||||
);
|
||||
if alias.type_params().angle_brackets_span().is_some() {
|
||||
let options = FormatTSTypeParametersOptions {
|
||||
group_id: None,
|
||||
is_type_or_interface_decl: true,
|
||||
};
|
||||
let type_parameters =
|
||||
type_parameters(alias.type_params(), Node::Stmt(statement)).with_options(options);
|
||||
let node = AstNodes::TSTypeParameterDeclaration(Node::Stmt(statement));
|
||||
format_node(node, f, |f| type_parameters.write_without_comments(f));
|
||||
}
|
||||
});
|
||||
|
||||
// Whether the left side is a group depends on the layout, which depends on the comments that
|
||||
// are left after the left side is written.
|
||||
let outer_group = f.reserve_tag();
|
||||
let left_group = f.reserve_tag();
|
||||
write!(f, [is_declared(statement).then_some("declare "), "type "]);
|
||||
let id = identifier(alias.name(), node);
|
||||
if alias.type_params().angle_brackets_span().is_some() {
|
||||
let type_parameters = type_parameters(alias.type_params(), Node::Stmt(statement));
|
||||
write!(f, [id, FormatNodeWithoutTrailingComments(&type_parameters)]);
|
||||
} else {
|
||||
write!(f, FormatNodeWithoutTrailingComments(&id));
|
||||
}
|
||||
// What oxfmt writes behind the `=`, and whether a line comment is on the line of the `=`.
|
||||
let mut operator_line: (&[Comment], bool) = (&[], false);
|
||||
if !f.is_quiet() {
|
||||
match type_alias_comments_stay_behind_operator(f) {
|
||||
true => operator_line = write_comments_after_type_alias_left_side(alias, f),
|
||||
false => {
|
||||
let right = format_with(|f| match ty.kind() {
|
||||
TypeKind::Union(types)
|
||||
if layout == AssignmentLikeLayout::BreakAfterOperator
|
||||
&& union_prints_own_comments(ty, f)
|
||||
&& !f.comments().is_ignored(right_node) =>
|
||||
{
|
||||
around_node(ty.span(), f, |f| write_ts_union_type_in(ty, types, true, f));
|
||||
}
|
||||
_ if ty != alias.ty()
|
||||
&& matches!(alias.ty().kind(), TypeKind::Intersection(_))
|
||||
&& lone_type_of_intersection_is_a_group(f) =>
|
||||
{
|
||||
write!(f, group(&ty))
|
||||
}
|
||||
_ => write!(f, ty),
|
||||
});
|
||||
let content = format_with(|f| {
|
||||
match layout {
|
||||
AssignmentLikeLayout::BreakLeftHandSide => write!(f, left),
|
||||
_ => write!(f, group(&left)),
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
space(),
|
||||
"=",
|
||||
FormatTrailingComments::Comments(comments_behind_operator)
|
||||
]
|
||||
);
|
||||
match layout {
|
||||
// A line comment that has been written after the `=` stays there.
|
||||
AssignmentLikeLayout::BreakAfterOperator
|
||||
if type_alias_comments_stay_around_operator(f)
|
||||
&& (has_line_comment_on_operator_line
|
||||
|| !matches!(ty.kind(), TypeKind::Cond { .. })) =>
|
||||
{
|
||||
write!(f, soft_line_indent_or_space(&right));
|
||||
}
|
||||
AssignmentLikeLayout::BreakAfterOperator => {
|
||||
write!(f, group(&soft_line_indent_or_space(&right)))
|
||||
}
|
||||
AssignmentLikeLayout::BreakLeftHandSide => write!(f, [space(), group(&right)]),
|
||||
AssignmentLikeLayout::NeverBreakAfterOperator => write!(f, [space(), right]),
|
||||
_ => {
|
||||
let group_id = f.group_id("assignment_like");
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments_before_type_alias_operator(
|
||||
id.span().end,
|
||||
ty,
|
||||
f
|
||||
))
|
||||
[
|
||||
group(&indent(&soft_line_break_or_space())).with_group_id(Some(group_id)),
|
||||
line_suffix_boundary(),
|
||||
indent_if_group_breaks(&right, group_id)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (operator_line_run, has_line_comment_on_operator_line) = operator_line;
|
||||
|
||||
let layout = match type_alias_layout(statement, alias, ty, f) {
|
||||
AssignmentLikeLayout::Fluid | AssignmentLikeLayout::NeverBreakAfterOperator
|
||||
if has_line_comment_on_operator_line =>
|
||||
{
|
||||
AssignmentLikeLayout::BreakAfterOperator
|
||||
}
|
||||
layout => layout,
|
||||
};
|
||||
if layout != AssignmentLikeLayout::BreakLeftHandSide {
|
||||
f.group_from(left_group, false);
|
||||
}
|
||||
write!(f, [space(), "="]);
|
||||
if !operator_line_run.is_empty() {
|
||||
write!(f, FormatTrailingComments::Comments(operator_line_run));
|
||||
if operator_line_run
|
||||
.iter()
|
||||
.any(|comment| f.comments().is_suppression_comment(comment))
|
||||
{
|
||||
f.comments_mut()
|
||||
.mark_suppressed_after_operator(ty.span().start);
|
||||
}
|
||||
}
|
||||
|
||||
let right = format_with(|f| {
|
||||
match ty.kind() {
|
||||
// The comments before a union are written with it.
|
||||
TypeKind::Union(types) => {
|
||||
let is_indented = layout == AssignmentLikeLayout::BreakAfterOperator;
|
||||
around_node(ty.span(), f, |f| {
|
||||
write_ts_union_type_in(ty, types, is_indented, f)
|
||||
});
|
||||
write_trailing_comments_of(ty.as_ast_nodes(), f);
|
||||
}
|
||||
_ if ty != alias.ty()
|
||||
&& matches!(alias.ty().kind(), TypeKind::Intersection(_))
|
||||
&& lone_type_of_intersection_is_a_group(f) =>
|
||||
{
|
||||
write!(f, group(&ty))
|
||||
}
|
||||
_ => write!(f, ty),
|
||||
}
|
||||
if ty != alias.ty() {
|
||||
write_trailing_comments_of(alias.ty().as_ast_nodes(), f);
|
||||
}
|
||||
});
|
||||
match layout {
|
||||
// A line comment that has been written after the `=` stays there.
|
||||
AssignmentLikeLayout::BreakAfterOperator
|
||||
if type_alias_comments_stay_behind_operator(f)
|
||||
&& (has_line_comment_on_operator_line
|
||||
|| !matches!(ty.kind(), TypeKind::Cond { .. })) =>
|
||||
{
|
||||
write!(f, soft_line_indent_or_space(&right));
|
||||
}
|
||||
AssignmentLikeLayout::BreakAfterOperator => {
|
||||
write!(f, group(&soft_line_indent_or_space(&right)))
|
||||
}
|
||||
AssignmentLikeLayout::BreakLeftHandSide => write!(f, [space(), group(&right)]),
|
||||
AssignmentLikeLayout::NeverBreakAfterOperator => write!(f, [space(), right]),
|
||||
_ => {
|
||||
let group_id = f.group_id("assignment_like");
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
group(&indent(&soft_line_break_or_space())).with_group_id(Some(group_id)),
|
||||
line_suffix_boundary(),
|
||||
indent_if_group_breaks(&right, group_id)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
f.group_from(outer_group, false);
|
||||
write!(f, OptionalSemicolon);
|
||||
if let Some(view_limit) = view_limit {
|
||||
f.comments_mut().restore_view_limit(view_limit);
|
||||
}
|
||||
write!(f, [group(&content), OptionalSemicolon]);
|
||||
}
|
||||
|
||||
/// oxfmt follows Prettier 3.8: a line comment behind the `=` of a type alias trails the left side.
|
||||
/// For 3.9 every comment after the `=` leads the type.
|
||||
/// oxfmt follows Prettier 3.8: a comment that ends the line of the left side of a type alias, or the line of the `=`, stays
|
||||
/// there. For 3.9 it leads the type.
|
||||
///
|
||||
/// ```ts
|
||||
/// type A = // comment type A =
|
||||
/// B; // comment
|
||||
/// B;
|
||||
/// ```
|
||||
fn type_alias_comments_stay_behind_operator(f: &Formatter<'_>) -> bool {
|
||||
fn type_alias_comments_stay_around_operator(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// What oxc's `AssignmentLike::fmt` does with the comments between the two sides of a type alias: it
|
||||
/// writes those that trail the left side. Returns those to write behind the `=`, and whether a line
|
||||
/// comment is on the line of the `=`. See `comments_stay_around_operator`.
|
||||
fn write_comments_after_type_alias_left_side<'a>(
|
||||
alias: Alias<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> (&'a [Comment], bool) {
|
||||
let left_end = alias
|
||||
.type_params()
|
||||
.angle_brackets_span()
|
||||
.map_or_else(|| alias.name().span().end, |it| it.end);
|
||||
if !f
|
||||
.comments()
|
||||
.has_any_comment_in(Span::before(left_end, alias.ty().span()))
|
||||
{
|
||||
return (&[], false);
|
||||
/// The trailing comments of the name of the type alias `statement`, or of its type parameters if it has any.
|
||||
pub(super) fn comments_trailing_type_alias_left_side<'a>(
|
||||
statement: Stmt<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> CommentList<'a> {
|
||||
let StmtKind::TypeAlias(alias) = statement.kind() else {
|
||||
return CommentList::empty();
|
||||
};
|
||||
match alias.type_params().angle_brackets_span() {
|
||||
Some(_) => {
|
||||
let type_parameters = AstNodes::TSTypeParameterDeclaration(Node::Stmt(statement));
|
||||
f.comments().of_node(type_parameters).trailing
|
||||
}
|
||||
None => f.comments().of_name(alias.name().span().start).trailing,
|
||||
}
|
||||
let operator_end = f.comments().position_after_character(left_end, b'=');
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(operator_end);
|
||||
write_comments_before_operator(left_end, f);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
let run = operator_line_run(operator_end, f);
|
||||
(
|
||||
run,
|
||||
!run.is_empty() || f.comments().has_printed_line_comment_after(left_end),
|
||||
)
|
||||
}
|
||||
|
||||
/// Of the comments that are left between the name of a type alias, which ends at `start`, and the
|
||||
/// first `=` after it, those that trail the left side. All others lead the type (Prettier's
|
||||
/// `handleAssignmentLikeComments`, for which the `=` of a default type is as good as any).
|
||||
fn comments_before_type_alias_operator<'a>(
|
||||
start: u32,
|
||||
ty: TypeNode<'a>,
|
||||
/// See [`type_alias_comments_stay_around_operator`]: the comments that stay behind the `=` are dangling comments of the type
|
||||
/// alias `statement`.
|
||||
pub(super) fn comments_behind_type_alias_operator<'a>(
|
||||
statement: Stmt<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> &'a [Comment] {
|
||||
let comments = f.comments().comments_before_character(start, b'=');
|
||||
let is_object = matches!(ty.kind(), TypeKind::Object(_));
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|comment| {
|
||||
!comment.preceded_by_newline()
|
||||
&& !(comment.followed_by_newline() && (is_object || comment.is_block()))
|
||||
})
|
||||
.count();
|
||||
&comments[..count]
|
||||
) -> CommentList<'a> {
|
||||
match f.is_quiet() {
|
||||
true => CommentList::empty(),
|
||||
false => {
|
||||
(f.comments()
|
||||
.of_node(AstNodes::TSTypeAliasDeclaration(statement)))
|
||||
.dangling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `chooseLayout` for the type alias `alias`, whose type is `ty`.
|
||||
@@ -395,35 +366,24 @@ fn type_alias_layout<'a>(
|
||||
TypeKind::Ref { args, .. } => !args.is_empty(),
|
||||
_ => false,
|
||||
};
|
||||
let leading_comments = match f.is_quiet() {
|
||||
true => CommentList::empty(),
|
||||
false => f.comments().of_node(ty.as_ast_nodes()).leading,
|
||||
};
|
||||
let should_break_after_operator = match ty.kind() {
|
||||
TypeKind::Union(types)
|
||||
if !union_breaks_one_per_line(f) && !should_hug_type(ty, types, f) =>
|
||||
{
|
||||
TypeKind::Union(types) if !union_breaks_one_per_line(f) && !should_hug_type(types, f) => {
|
||||
true
|
||||
}
|
||||
_ if type_alias_comments_stay_behind_operator(f)
|
||||
&& !f.is_quiet()
|
||||
&& f.comments()
|
||||
.comments_before_iter(ty.span().start)
|
||||
.any(|comment| comment.is_indentable_block()) =>
|
||||
_ if type_alias_comments_stay_around_operator(f)
|
||||
&& (leading_comments.iter()).any(|comment| comment.is_indentable_block()) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
// It is up to the union where it breaks, unless it leaves that to the `=`.
|
||||
TypeKind::Union(_)
|
||||
if type_alias_comments_stay_behind_operator(f) && union_prints_itself(ty, f) =>
|
||||
if type_alias_comments_stay_around_operator(f) && union_prints_itself(ty, f) =>
|
||||
{
|
||||
!f.is_quiet()
|
||||
&& type_alias_union_breaks_after_operator(
|
||||
statement,
|
||||
f.comments().comments_before(ty.span().start),
|
||||
f,
|
||||
)
|
||||
}
|
||||
_ if type_alias_comments_stay_behind_operator(f)
|
||||
&& f.comments().has_leading_own_line_comment(ty.span().start) =>
|
||||
{
|
||||
true
|
||||
type_alias_union_breaks_after_operator(statement, ty, f)
|
||||
}
|
||||
TypeKind::Cond { check, extends, .. }
|
||||
if f.options().experimental_ternaries || is_generic(check) || is_generic(extends) =>
|
||||
@@ -432,12 +392,8 @@ fn type_alias_layout<'a>(
|
||||
}
|
||||
// Flow's `StringLiteralTypeAnnotation`
|
||||
TypeKind::StringLit(_) if f.file().is_javascript() => true,
|
||||
_ => {
|
||||
!f.is_quiet()
|
||||
&& f.comments()
|
||||
.comments_before_iter(ty.span().start)
|
||||
.any(|comment| comment.followed_by_newline() || comment.is_indentable_block())
|
||||
}
|
||||
_ => (leading_comments.iter())
|
||||
.any(|comment| comment.has_newline_after() || comment.is_indentable_block()),
|
||||
};
|
||||
if should_break_after_operator {
|
||||
return AssignmentLikeLayout::BreakAfterOperator;
|
||||
@@ -459,12 +415,12 @@ fn type_alias_layout<'a>(
|
||||
}
|
||||
|
||||
/// `enum A { B, C = 1 }`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_enum_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
declaration: Enum<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let node = AstNodes::TSEnumDeclaration(statement);
|
||||
let is_const = statement
|
||||
.modifiers()
|
||||
.iter()
|
||||
@@ -478,29 +434,18 @@ pub(crate) fn write_ts_enum_declaration<'a>(
|
||||
space(),
|
||||
]
|
||||
);
|
||||
let name = identifier(declaration.name(), node);
|
||||
if !f.is_quiet() && comments_stay_between_head_and_body(f) {
|
||||
write!(f, FormatNodeWithoutTrailingComments(&name));
|
||||
write_head_body_separator(declaration.body_span().start, f);
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
name,
|
||||
space(),
|
||||
format_leading_comments(declaration.body_span())
|
||||
]
|
||||
);
|
||||
}
|
||||
if f.file().is_flow() {
|
||||
super::flow::write_explicit_type_of_enum(declaration, f);
|
||||
}
|
||||
around_node(declaration.body_span(), f, |f| {
|
||||
write_ts_enum_body(declaration, f)
|
||||
let body = AstNodes::TSEnumBody(statement);
|
||||
write!(f, identifier(declaration.name()));
|
||||
write_space_before_body(body, f);
|
||||
format_node(body, f, |f| {
|
||||
if f.file().is_flow() {
|
||||
super::flow::write_explicit_type_of_enum(declaration, f);
|
||||
}
|
||||
write_ts_enum_body(body, declaration, f);
|
||||
});
|
||||
}
|
||||
|
||||
fn write_ts_enum_body<'a>(declaration: Enum<'a>, f: &mut Formatter<'a>) {
|
||||
fn write_ts_enum_body<'a>(body: AstNodes<'a>, declaration: Enum<'a>, f: &mut Formatter<'a>) {
|
||||
write!(f, "{");
|
||||
let members = declaration.members();
|
||||
if members.is_empty() {
|
||||
@@ -508,7 +453,7 @@ fn write_ts_enum_body<'a>(declaration: Enum<'a>, f: &mut Formatter<'a>) {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
format_dangling_comments(declaration.body_span()).with_soft_block_indent(),
|
||||
dangling_comments(body).with_soft_block_indent(),
|
||||
expand_parent()
|
||||
]
|
||||
);
|
||||
@@ -525,6 +470,18 @@ fn write_ts_enum_body<'a>(declaration: Enum<'a>, f: &mut Formatter<'a>) {
|
||||
write!(
|
||||
f,
|
||||
block_indent(&format_with(|f| {
|
||||
// Flow's `enum A { /* comment */ ... }`
|
||||
let comments = f.comments().of_node(body).dangling;
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatDanglingComments::Comments {
|
||||
comments,
|
||||
indent: DanglingIndentMode::None
|
||||
},
|
||||
(!comments.is_empty()).then_some(hard_line_break())
|
||||
]
|
||||
);
|
||||
let trailing_separator = match members
|
||||
.last()
|
||||
.is_some_and(super::flow::is_unknown_members_mark)
|
||||
@@ -566,40 +523,21 @@ pub(crate) fn write_ts_enum_member<'a>(member: EnumMember<'a>, f: &mut Formatter
|
||||
}
|
||||
|
||||
/// `namespace A.B { .. }`, `declare module "a" { .. }`, `declare global { .. }`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_module_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
module: Module<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let node = statement.as_ast_nodes();
|
||||
let innermost = module.innermost();
|
||||
let body_span = innermost.body_span();
|
||||
write!(f, is_declared(statement).then_some("declare "));
|
||||
|
||||
// Which of the comments after the name trail it depends on the body.
|
||||
let name_end = innermost.name_span().end;
|
||||
let view_limit = (!f.is_quiet()).then(|| f.comments_mut().limit_comments_up_to(name_end));
|
||||
match module.name() {
|
||||
ModuleName::Global => {
|
||||
let comments_before_global = f.comments().comments_before(module.name_span().start);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatLeadingComments::Comments(comments_before_global),
|
||||
"global"
|
||||
]
|
||||
);
|
||||
}
|
||||
ModuleName::Global => format_name(module.name_span(), f, |f| write!(f, "global")),
|
||||
ModuleName::String(name) => write!(
|
||||
f,
|
||||
[
|
||||
"module",
|
||||
space(),
|
||||
FormatStringLiteral {
|
||||
span: name.span(),
|
||||
parent: node
|
||||
}
|
||||
]
|
||||
["module", space(), FormatStringLiteral { span: name.span() }]
|
||||
),
|
||||
ModuleName::Ident(name) => {
|
||||
let keyword = if module.uses_module_keyword() {
|
||||
@@ -607,54 +545,37 @@ pub(crate) fn write_ts_module_declaration<'a>(
|
||||
} else {
|
||||
"namespace"
|
||||
};
|
||||
write!(f, [keyword, space(), identifier(name, node)]);
|
||||
write!(f, [keyword, space(), identifier(name)]);
|
||||
}
|
||||
}
|
||||
let mut current = module;
|
||||
while let Some(nested) = current.nested() {
|
||||
if let ModuleName::Ident(name) = nested.name() {
|
||||
write!(f, [".", identifier(name, node)]);
|
||||
write!(f, [".", identifier(name)]);
|
||||
}
|
||||
current = nested;
|
||||
}
|
||||
if let Some(view_limit) = view_limit {
|
||||
f.comments_mut().restore_view_limit(view_limit);
|
||||
if !(body_span.is_some() && comments_stay_between_head_and_body(f)) {
|
||||
let following = body_span.map_or(0, |it| it.start);
|
||||
write!(
|
||||
f,
|
||||
format_trailing_comments(statement.span(), innermost.name_span(), following)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(span) = body_span else {
|
||||
return write!(f, OptionalSemicolon);
|
||||
};
|
||||
match !f.is_quiet() && comments_stay_between_head_and_body(f) {
|
||||
true => write_head_body_separator(span.start, f),
|
||||
false => write!(f, space()),
|
||||
}
|
||||
format_node(
|
||||
span,
|
||||
|| node,
|
||||
f,
|
||||
|f| {
|
||||
write!(f, "{");
|
||||
match is_empty_block(innermost.body()) {
|
||||
// Prettier's `printBlock` does not know Flow's `declare namespace`.
|
||||
true if f.file().is_flow()
|
||||
&& !module.uses_module_keyword()
|
||||
&& !f.comments().has_comment_in_span(span) =>
|
||||
{
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
true => write!(f, format_dangling_comments(span).with_block_indent()),
|
||||
false => write!(f, block_indent(&FormatStatements(innermost.body()))),
|
||||
let body = AstNodes::TSModuleBlock(statement);
|
||||
write_space_before_body(body, f);
|
||||
format_node(body, f, |f| {
|
||||
write!(f, "{");
|
||||
match is_empty_block(innermost.body()) {
|
||||
// Prettier's `printBlock` does not know Flow's `declare namespace`.
|
||||
true if f.file().is_flow()
|
||||
&& !module.uses_module_keyword()
|
||||
&& !f.comments().has_comment_in_span(span) =>
|
||||
{
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
write!(f, "}");
|
||||
},
|
||||
);
|
||||
true => write!(f, dangling_comments(body).with_block_indent()),
|
||||
false => write!(f, block_indent(&FormatStatements(innermost.body()))),
|
||||
}
|
||||
write!(f, "}");
|
||||
});
|
||||
}
|
||||
|
||||
/// `require(/* comment */ "a")` is on three lines for oxfmt, whatever the comment is.
|
||||
@@ -663,19 +584,19 @@ fn require_with_comment_breaks(f: &Formatter<'_>) -> bool {
|
||||
}
|
||||
|
||||
/// `import a = require("a")`, `import a = b.c`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_import_equals_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
import: ImportEquals<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let node = AstNodes::TSImportEqualsDeclaration(statement);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
"import",
|
||||
space(),
|
||||
import.flags().contains(Flags::TYPE_ONLY).then_some("type "),
|
||||
identifier(import.name(), node),
|
||||
identifier(import.name()),
|
||||
space(),
|
||||
"=",
|
||||
space()
|
||||
@@ -686,34 +607,25 @@ pub(crate) fn write_ts_import_equals_declaration<'a>(
|
||||
import.require_span(),
|
||||
statement.module_specifier_span(),
|
||||
) {
|
||||
(ImportEqualsTarget::Entity(name), ..) => write!(f, entity_name(name, node)),
|
||||
(ImportEqualsTarget::Entity(name), ..) => write!(f, entity_name(name)),
|
||||
(ImportEqualsTarget::Require(_), Some(require_span), Some(span)) => {
|
||||
format_node(
|
||||
require_span,
|
||||
|| node,
|
||||
f,
|
||||
|f| {
|
||||
let expression = FormatModuleSpecifier {
|
||||
span,
|
||||
parent: node,
|
||||
call_span: require_span,
|
||||
};
|
||||
match f.comments().has_comment_in_span(require_span) {
|
||||
true if require_with_comment_breaks(f) => {
|
||||
write!(f, ["require(", block_indent(&expression), ")"])
|
||||
}
|
||||
true => write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
"require(",
|
||||
soft_block_indent(&expression),
|
||||
")"
|
||||
))
|
||||
),
|
||||
false => write!(f, ["require(", expression, ")"]),
|
||||
format_ignorable_name(require_span, f, |f| {
|
||||
let expression = FormatStringLiteral { span };
|
||||
match f.comments().has_comment_in_span(require_span) {
|
||||
true if require_with_comment_breaks(f) => {
|
||||
write!(f, ["require(", block_indent(&expression), ")"])
|
||||
}
|
||||
},
|
||||
);
|
||||
true => write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
"require(",
|
||||
soft_block_indent(&expression),
|
||||
")"
|
||||
))
|
||||
),
|
||||
false => write!(f, ["require(", expression, ")"]),
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -721,17 +633,17 @@ pub(crate) fn write_ts_import_equals_declaration<'a>(
|
||||
}
|
||||
|
||||
/// `export = a`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_export_assignment<'a>(expression: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
write!(f, ["export = ", expression, OptionalSemicolon]);
|
||||
}
|
||||
|
||||
/// `export as namespace a`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_namespace_export_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let id = statement
|
||||
.namespace_export_name()
|
||||
.map(|it| identifier(it, AstNodes::TSNamespaceExportDeclaration(statement)));
|
||||
let id = statement.namespace_export_name().map(|it| identifier(it));
|
||||
write!(f, ["export as namespace ", id, OptionalSemicolon]);
|
||||
}
|
||||
+335
-255
@@ -4,44 +4,26 @@ use super::function_type::{
|
||||
write_ts_call_signature_declaration, write_ts_construct_signature_declaration,
|
||||
write_ts_method_signature,
|
||||
};
|
||||
use super::import_declaration::FormatStringLiteral;
|
||||
use super::object_like::ObjectLike;
|
||||
use super::type_parameters::type_arguments;
|
||||
use super::type_parameters::{FormatDanglingCommentsForInline, type_arguments};
|
||||
use super::union_type::union_writes_comments_behind_colon;
|
||||
use crate::cursor::extend_node;
|
||||
use crate::js::format::{
|
||||
FormatTypeOfPredicate, identifier, no_comment_trails_what_is_before_another,
|
||||
write_trailing_comments_of,
|
||||
};
|
||||
use crate::js::format::{FormatTypeOfPredicate, format_node, identifier};
|
||||
use crate::js::utils::conditional::ConditionalLike;
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::number::format_number_token;
|
||||
use crate::js::utils::object::{format_computed_or_property_key, key_requires_quotes};
|
||||
use crate::js::utils::string::{FormatLiteralStringToken, StringLiteralParentKind};
|
||||
use crate::js::utils::suppressed::{FormatSuppressedNode, FormatTemplateText};
|
||||
use crate::js::utils::typescript::{end_of_line_comments, without_lone_operator};
|
||||
use crate::js::utils::typescript::without_lone_operator;
|
||||
use crate::prelude::*;
|
||||
use crate::{best_fitting, format_args, write};
|
||||
use smallvec::SmallVec;
|
||||
use std::cell::Cell;
|
||||
|
||||
/// `A.B.C`. `parent`: what it is a name in.
|
||||
pub(crate) fn entity_name<'a>(name: EntityName<'a>, parent: AstNodes<'a>) -> impl Format<'a> {
|
||||
/// `A.B.C`
|
||||
pub(crate) fn entity_name<'a>(name: EntityName<'a>) -> impl Format<'a> {
|
||||
format_with(move |f: &mut Formatter<'a>| {
|
||||
if f.is_quiet() {
|
||||
f.join_with(".")
|
||||
.entries(name.parts().map(|part| identifier(part, parent)));
|
||||
return;
|
||||
}
|
||||
// The next name is what follows a name, which `parent` does not tell.
|
||||
let mut parts = name.parts().peekable();
|
||||
while let Some(part) = parts.next() {
|
||||
let part = identifier(part, parent);
|
||||
let Some(next) = parts.peek() else {
|
||||
return write!(f, part);
|
||||
};
|
||||
let comments = format_trailing_comments(name.span(), part.span(), next.span().start);
|
||||
write!(f, [FormatNodeWithoutTrailingComments(&part), comments, "."]);
|
||||
}
|
||||
f.join_with(".")
|
||||
.entries(name.parts().map(|part| identifier(part)));
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,10 +54,7 @@ pub(crate) fn write_ts_type_reference<'a>(
|
||||
{
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
heritage_name(name, node),
|
||||
type_arguments(args, Node::Type(ty))
|
||||
]
|
||||
[heritage_name(name), type_arguments(args, Node::Type(ty))]
|
||||
);
|
||||
}
|
||||
if args.is_empty() && f.file().is_javascript() {
|
||||
@@ -84,28 +63,17 @@ pub(crate) fn write_ts_type_reference<'a>(
|
||||
return write!(f, "boolean");
|
||||
}
|
||||
// `A<>`
|
||||
if let Some(last) = name.get(name.len().wrapping_sub(1))
|
||||
&& ty.text().ends_with(b">")
|
||||
{
|
||||
let brackets = Span::after(last.span(), ty.span().end);
|
||||
let has_line_comment = f
|
||||
.comments()
|
||||
.comments_in(brackets)
|
||||
.iter()
|
||||
.any(|it| it.is_line());
|
||||
match name.len() {
|
||||
1 => write!(
|
||||
f,
|
||||
FormatNodeWithoutTrailingComments(&identifier(last, node))
|
||||
),
|
||||
_ => write!(f, entity_name(name, node)),
|
||||
}
|
||||
write!(f, "<");
|
||||
match has_line_comment {
|
||||
true => write!(f, format_dangling_comments(brackets).with_block_indent()),
|
||||
false => write!(f, format_dangling_comments(brackets)),
|
||||
}
|
||||
return write!(f, ">");
|
||||
if ty.text().ends_with(b">") {
|
||||
let arguments = AstNodes::TSTypeParameterInstantiation(Node::Type(ty));
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
entity_name(name),
|
||||
"<",
|
||||
FormatDanglingCommentsForInline(arguments),
|
||||
">"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
let wrap =
|
||||
@@ -114,7 +82,7 @@ pub(crate) fn write_ts_type_reference<'a>(
|
||||
f,
|
||||
[
|
||||
wrap.then_some("("),
|
||||
entity_name(name, node),
|
||||
entity_name(name),
|
||||
type_arguments(args, Node::Type(ty)),
|
||||
wrap.then_some(")")
|
||||
]
|
||||
@@ -128,10 +96,10 @@ fn heritage_name_breaks_before_its_dots(f: &Formatter<'_>) -> bool {
|
||||
|
||||
/// `A.B.C` after `extends` or `implements`. There it is a member expression in ESTree, which can
|
||||
/// break before its dots (Prettier's `printMemberExpression`).
|
||||
fn heritage_name<'a>(name: EntityName<'a>, parent: AstNodes<'a>) -> impl Format<'a> {
|
||||
fn heritage_name<'a>(name: EntityName<'a>) -> impl Format<'a> {
|
||||
format_with(move |f: &mut Formatter<'a>| {
|
||||
for (index, part) in name.parts().enumerate() {
|
||||
let part = identifier(part, parent);
|
||||
let part = identifier(part);
|
||||
match index {
|
||||
0 => write!(f, part),
|
||||
_ => write!(
|
||||
@@ -205,14 +173,16 @@ pub(crate) fn write_ts_literal_type<'a>(ty: TypeNode<'a>, f: &mut Formatter<'a>)
|
||||
return write!(f, format_digits(text));
|
||||
};
|
||||
// `- /* comment */ 1`: like a unary expression whose operand has a comment.
|
||||
let comments = f.comments().comments_before(ty.span().end);
|
||||
let comments = match f.is_quiet() {
|
||||
true => CommentList::empty(),
|
||||
false => f.comments().of_node(ty.as_ast_nodes()).dangling,
|
||||
};
|
||||
let Some(last) = comments.last() else {
|
||||
return write!(f, ["-", format_digits(rest.trim_ascii_start())]);
|
||||
};
|
||||
let digits = f
|
||||
.source_text()
|
||||
.text_for(&Span::after(last.span, ty.span().end))
|
||||
.trim_ascii_start();
|
||||
.text_for(&Span::new(last.next_token_start(), ty.span().end));
|
||||
let operand = format_args!(
|
||||
FormatLeadingComments::Comments(comments),
|
||||
format_digits(digits)
|
||||
@@ -268,19 +238,25 @@ fn write_signatures<'a>(members: List<'a, Member<'a>>, is_interface: bool, f: &m
|
||||
f.context_mut().push_quote_needed(quote_needed);
|
||||
}
|
||||
|
||||
let (is_flow, text) = (f.file().is_flow(), f.source_text().as_bytes());
|
||||
let mut joiner = f.join_nodes_with_soft_line();
|
||||
let mut iter = members.iter().peekable();
|
||||
let mut previous: Option<Member<'a>> = None;
|
||||
while let Some(signature) = iter.next() {
|
||||
let content = FormatTSSignature {
|
||||
signature,
|
||||
next_signature: iter.peek().copied(),
|
||||
is_interface,
|
||||
};
|
||||
// Prettier has no node for the `...`.
|
||||
match super::flow::is_inexact_mark(signature)
|
||||
&& !joiner.has_comment_before(signature.span().start)
|
||||
{
|
||||
// `a: 1⏎⏎// comment⏎; b: 2`: a member in Flow ends before its separator, and Prettier's `isNextLineEmpty` starts there.
|
||||
let is_after_empty_line = is_flow
|
||||
&& previous.replace(signature).is_some_and(|previous| {
|
||||
crate::text::is_next_line_empty(text, previous.span().end as usize)
|
||||
});
|
||||
// Prettier has no node for the `...`. A comment before it trails the member before it.
|
||||
match super::flow::is_inexact_mark(signature) {
|
||||
true => joiner.entry_without_empty_line(&content),
|
||||
false if is_after_empty_line => joiner.entry_after_empty_line(&content),
|
||||
false => joiner.entry(signature.span(), &content),
|
||||
}
|
||||
}
|
||||
@@ -302,48 +278,20 @@ struct FormatTSSignature<'a> {
|
||||
is_interface: bool,
|
||||
}
|
||||
|
||||
/// A member, with the span that the comments after it are after.
|
||||
struct FormatMemberIn<'a>(Member<'a>, Span);
|
||||
|
||||
impl<'a> Format<'a> for FormatMemberIn<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
self.0.fmt(f);
|
||||
}
|
||||
}
|
||||
|
||||
impl Spanned for FormatMemberIn<'_> {
|
||||
fn span(&self) -> Span {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatTSSignature<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let (signature, mut span) = (self.signature, self.signature.span());
|
||||
let signature = self.signature;
|
||||
let node = signature.as_ast_nodes();
|
||||
if !f.is_quiet() {
|
||||
if f.comments().is_suppressed(span.start) {
|
||||
if f.comments().is_ignored(node) {
|
||||
write!(f, signature);
|
||||
// It is not part of a member of Flow's.
|
||||
if f.file().is_flow() {
|
||||
// In Flow the separator is not part of the member. In an interface or a declared class Prettier writes none after an ignored
|
||||
// member, which is a syntax error before another member.
|
||||
if f.file().is_flow() && !(self.is_interface && self.next_signature.is_none()) {
|
||||
self.write_separator(f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if f.comments().has_trailing_suppression_comment(span.end) {
|
||||
write!(
|
||||
f,
|
||||
[format_leading_comments(span), FormatSuppressedNode(span)]
|
||||
);
|
||||
return write_trailing_comments_of(signature.as_ast_nodes(), f);
|
||||
}
|
||||
// Prettier's `handleTSFunctionTrailingComments`: `a() /* comment */;`
|
||||
if matches!(signature.as_ast_nodes(), AstNodes::TSMethodSignature(_))
|
||||
&& signature
|
||||
.func()
|
||||
.is_some_and(|func| func.return_type().is_none())
|
||||
{
|
||||
span = f.comments().without_semicolon(span);
|
||||
}
|
||||
// The `,` of Flow's is behind the comments that trail the member.
|
||||
if (f.file().is_flow() && !self.is_interface) || separator_is_behind_comments(f) {
|
||||
write!(f, signature);
|
||||
@@ -351,18 +299,11 @@ impl<'a> Format<'a> for FormatTSSignature<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// The separator is not part of the member. The comments after the member are behind it.
|
||||
write!(
|
||||
f,
|
||||
FormatNodeWithoutTrailingComments(&FormatMemberIn(signature, span))
|
||||
);
|
||||
// The separator is not part of the member. The comments that trail the member are behind it.
|
||||
write!(f, FormatNodeWithoutTrailingComments(&signature));
|
||||
self.write_separator(f);
|
||||
extend_node(signature.span(), f);
|
||||
if !f.is_quiet()
|
||||
&& !(self.next_signature.is_some() && no_comment_trails_what_is_before_another(f))
|
||||
{
|
||||
write_trailing_comments_of(signature.as_ast_nodes(), f);
|
||||
}
|
||||
write!(f, trailing_comments(node));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,58 +375,21 @@ pub(crate) fn write_ts_signature<'a>(member: Member<'a>, f: &mut Formatter<'a>)
|
||||
let Some(ty) = member.ty() else {
|
||||
return write!(f, member.flags().contains(Flags::OPTIONAL).then_some("?"));
|
||||
};
|
||||
// Prettier does not attach comments to the `: T` of a property signature, only to the
|
||||
// name and the type.
|
||||
if !f.is_quiet() {
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments_after_property_name(ty, f))
|
||||
);
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
member.flags().contains(Flags::OPTIONAL).then_some("?"),
|
||||
":",
|
||||
space(),
|
||||
ty
|
||||
]
|
||||
[member.flags().contains(Flags::OPTIONAL).then_some("?"), ":"]
|
||||
);
|
||||
write_trailing_comments_of(AstNodes::TSTypeAnnotation(ty), f);
|
||||
write!(f, space());
|
||||
// `a: // comment ⏎ B` stays as it is for oxfmt. The comment dangles from the signature.
|
||||
if !f.is_quiet() && !union_writes_comments_behind_colon(without_lone_operator(ty), f) {
|
||||
let comments_behind_colon = f.comments().of_node(node).dangling;
|
||||
write!(f, FormatLeadingComments::Comments(comments_behind_colon));
|
||||
}
|
||||
write!(f, ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `a: // comment ⏎ B;` stays as it is for oxfmt. For Prettier it is `a: B; // comment`.
|
||||
fn comments_behind_colon_of_property_signature_stay(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Of the comments between the name of a property signature and its type `ty`, those that trail
|
||||
/// the name: those on its line that are before the `:`, or at the end of the line, unless a union
|
||||
/// or an intersection follows (Prettier's `handlePropertySignatureComments`).
|
||||
pub(super) fn comments_after_property_name<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> &'a [Comment] {
|
||||
let comments = f.comments().comments_before(ty.span().start);
|
||||
let colon = ty.annotation_span().start;
|
||||
let before_colon = comments
|
||||
.iter()
|
||||
.take_while(|comment| comment.span.start < colon && !comment.preceded_by_newline())
|
||||
.count();
|
||||
let (_, rest) = comments.split_at(before_colon);
|
||||
let takes_comments = comments_behind_colon_of_property_signature_stay(f)
|
||||
|| matches!(
|
||||
without_lone_operator(ty).kind(),
|
||||
TypeKind::Union(_) | TypeKind::Intersection(_)
|
||||
);
|
||||
match takes_comments {
|
||||
true => &comments[..before_colon],
|
||||
false => &comments[..before_colon + end_of_line_comments(rest).len()],
|
||||
}
|
||||
}
|
||||
|
||||
/// `A extends B ? C : D`
|
||||
pub(crate) fn write_ts_conditional_type<'a>(ty: TypeNode<'a>, f: &mut Formatter<'a>) {
|
||||
ConditionalLike::TSConditionalType(ty).fmt(f);
|
||||
@@ -498,24 +402,36 @@ pub(crate) fn write_ts_type_query<'a>(
|
||||
args: List<'a, TypeNode<'a>>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if f.file().is_flow() && super::flow::write_declared_predicate(ty, expr, args, f) {
|
||||
if f.file().is_flow() && super::flow::write_declared_predicate(ty, f) {
|
||||
return;
|
||||
}
|
||||
// The name is ESTree's `TSQualifiedName`, which does not break like a member expression.
|
||||
let mut names: SmallVec<[Ident<'a>; 4]> = SmallVec::new();
|
||||
let mut leftmost = expr;
|
||||
while let ExprKind::Dot { obj, name, .. } = leftmost.kind() {
|
||||
names.push(name);
|
||||
leftmost = obj;
|
||||
}
|
||||
write!(f, ["typeof ", leftmost]);
|
||||
for &name in names.iter().rev() {
|
||||
write!(f, [".", identifier(name, AstNodes::TSTypeQuery(ty))]);
|
||||
}
|
||||
write!(f, "typeof ");
|
||||
write_qualified_name(expr, f);
|
||||
write!(f, type_arguments(args, Node::Type(ty)));
|
||||
}
|
||||
|
||||
/// `a.b.c` after `typeof`. It is ESTree's `TSQualifiedName`, which does not break like a member expression.
|
||||
fn write_qualified_name<'a>(name: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let ExprKind::Dot {
|
||||
obj: left,
|
||||
name: right,
|
||||
..
|
||||
} = name.kind()
|
||||
else {
|
||||
return write!(f, name);
|
||||
};
|
||||
// Not by way of `Expr`.
|
||||
if !f.context_mut().has_stack_left() {
|
||||
return;
|
||||
}
|
||||
format_node(name.as_chain_element(), f, |f| {
|
||||
write_qualified_name(left, f);
|
||||
write!(f, [".", identifier(right)]);
|
||||
});
|
||||
}
|
||||
|
||||
/// `import("a").B<C>`, `typeof import("a")`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_import_type<'a>(ty: TypeNode<'a>, f: &mut Formatter<'a>) {
|
||||
let TypeKind::Import {
|
||||
name,
|
||||
@@ -529,31 +445,35 @@ pub(crate) fn write_ts_import_type<'a>(ty: TypeNode<'a>, f: &mut Formatter<'a>)
|
||||
let Some(source) = ty.import_source_span() else {
|
||||
return write!(f, FormatSuppressedNode(ty.span()));
|
||||
};
|
||||
let node = AstNodes::TSImportType(ty);
|
||||
let format_source = FormatStringLiteral {
|
||||
span: source,
|
||||
parent: node,
|
||||
let options = ty.import_attributes();
|
||||
let comments = match f.is_quiet() {
|
||||
true => ImportTypeComments::default(),
|
||||
false => ImportTypeComments::new(ty, source, options, f),
|
||||
};
|
||||
let format_source = comments.format_source(source);
|
||||
|
||||
// Up to what follows the `)`, they lead or trail the module specifier.
|
||||
let has_comment = !f.is_quiet() && {
|
||||
let following = name
|
||||
.first()
|
||||
.map(|it| it.span())
|
||||
.or_else(|| args.angle_brackets_span());
|
||||
let span = ty.span();
|
||||
f.comments()
|
||||
.has_comment_in_range(span.start, following.map_or(span.end, |it| it.start))
|
||||
};
|
||||
|
||||
write!(f, is_typeof.then_some("typeof "));
|
||||
match ty.import_attributes() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
is_typeof.then_some("typeof "),
|
||||
FormatLeadingComments::Comments(comments.leading_import)
|
||||
]
|
||||
);
|
||||
if let Some(import) = ty.import_span()
|
||||
&& has_ignore_comment(&[comments.leading_import], CommentList::empty(), f)
|
||||
{
|
||||
return write!(f, FormatSuppressedNode(import));
|
||||
}
|
||||
match options {
|
||||
Some(options) => {
|
||||
let arguments = format_with(|f| write_import_type_arguments(ty, source, options, f));
|
||||
let arguments =
|
||||
format_with(|f| write_import_type_arguments(source, options, &comments, f));
|
||||
write!(f, group(&format_args!("import", arguments)));
|
||||
}
|
||||
// A long module name does not break.
|
||||
None if !has_comment => write!(f, ["import(", format_source, ")"]),
|
||||
None if comments.leading_source.is_empty() && comments.trailing_source.is_empty() => {
|
||||
write!(f, ["import(", format_source, ")"])
|
||||
}
|
||||
None => write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
@@ -565,48 +485,148 @@ pub(crate) fn write_ts_import_type<'a>(ty: TypeNode<'a>, f: &mut Formatter<'a>)
|
||||
}
|
||||
|
||||
if !name.is_empty() {
|
||||
write!(f, [".", entity_name(name, node)]);
|
||||
write!(f, [".", entity_name(name)]);
|
||||
}
|
||||
write!(f, type_arguments(args, Node::Type(ty)));
|
||||
}
|
||||
|
||||
/// The module specifier in `import("a")` or `require("a")`. Only the comments before the `)` trail it.
|
||||
pub(crate) struct FormatModuleSpecifier<'a> {
|
||||
pub(crate) span: Span,
|
||||
pub(crate) parent: AstNodes<'a>,
|
||||
/// From `import` or `require` to the `)`.
|
||||
pub(crate) call_span: Span,
|
||||
/// The comments in `typeof import("a", { with: { .. } })`, by the ESTree node that Prettier attaches them to. Of those nodes only
|
||||
/// the module specifier and the inner object can own comments here, so they also own the comments of the import type after
|
||||
/// `typeof`, of the outer object and of its property.
|
||||
#[derive(Default)]
|
||||
struct ImportTypeComments<'a> {
|
||||
/// `typeof /* comment */ import("a")`
|
||||
leading_import: CommentList<'a>,
|
||||
leading_source: CommentList<'a>,
|
||||
trailing_source: CommentList<'a>,
|
||||
/// Before the `{` of `{ with: { .. } }`: the comments that the module specifier owns, then those that the inner object owns.
|
||||
leading_options: [CommentList<'a>; 2],
|
||||
/// Before `with`: the comments that the module specifier owns, then those that the inner object owns.
|
||||
leading_property: [CommentList<'a>; 2],
|
||||
/// `with // comment ⏎: { .. }`. With Prettier's `handlePropertyComments` it leads the property.
|
||||
at_end_of_line_before_colon: CommentList<'a>,
|
||||
/// `with: // comment ⏎ { .. }`, likewise.
|
||||
at_end_of_line_after_colon: CommentList<'a>,
|
||||
/// `with /* comment */: { .. }`
|
||||
trailing_keyword: CommentList<'a>,
|
||||
/// Before the `{` of `{ .. }`.
|
||||
leading_attributes: CommentList<'a>,
|
||||
/// Before the `}` of `{ with: { .. } }`.
|
||||
trailing_property: CommentList<'a>,
|
||||
trailing_options: CommentList<'a>,
|
||||
}
|
||||
|
||||
struct FormatModuleSpecifierWithoutComments<'a>(Span, AstNodes<'a>);
|
||||
|
||||
impl Spanned for FormatModuleSpecifierWithoutComments<'_> {
|
||||
fn span(&self) -> Span {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatModuleSpecifierWithoutComments<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
FormatStringLiteral {
|
||||
span: self.0,
|
||||
parent: self.1,
|
||||
impl<'a> ImportTypeComments<'a> {
|
||||
fn new(
|
||||
ty: TypeNode<'a>,
|
||||
source: Span,
|
||||
options: Option<ImportAttributes<'a>>,
|
||||
f: &Formatter<'a>,
|
||||
) -> Self {
|
||||
/// Splits off the comments at the start of `comments` whose next token starts at or before `token_start`.
|
||||
fn take_up_to<'a>(comments: &mut CommentList<'a>, token_start: u32) -> CommentList<'a> {
|
||||
let count = (comments.iter())
|
||||
.take_while(|comment| comment.next_token_start() <= token_start)
|
||||
.count();
|
||||
let (taken, rest) = comments.split_at(count);
|
||||
*comments = rest;
|
||||
taken
|
||||
}
|
||||
.fmt(f);
|
||||
let of_source = f.comments().of_name(source.start);
|
||||
let end_of_typeof = ty.span().start + "typeof".len() as u32;
|
||||
let is_typeof = matches!(
|
||||
ty.kind(),
|
||||
TypeKind::Import {
|
||||
is_typeof: true,
|
||||
..
|
||||
}
|
||||
);
|
||||
let behind_typeof = (of_source.leading.iter())
|
||||
.take_while(|comment| is_typeof && comment.prev_token_end() == end_of_typeof)
|
||||
.count();
|
||||
let (leading_import, leading_source) = of_source.leading.split_at(behind_typeof);
|
||||
let Some(options) = options else {
|
||||
return ImportTypeComments {
|
||||
leading_import,
|
||||
leading_source,
|
||||
trailing_source: of_source.trailing,
|
||||
..ImportTypeComments::default()
|
||||
};
|
||||
};
|
||||
let outer = options.options_span();
|
||||
let of_attributes = f.comments().of_node(options.object().as_ast_nodes());
|
||||
let (mut after_source, mut before_attributes, mut after_attributes) = (
|
||||
of_source.trailing,
|
||||
of_attributes.leading,
|
||||
of_attributes.trailing,
|
||||
);
|
||||
// `"a" /* 1 */, // 2 ⏎ { .. }`: before the `,`, or after it up to the end of its line.
|
||||
let before_comma = take_up_to(&mut after_source, outer.start.saturating_sub(1));
|
||||
let (on_line_of_comma, after_comma) =
|
||||
take_up_to(&mut after_source, outer.start).split_at_end_of_line();
|
||||
let (keyword, attributes) = (options.keyword_span().start, options.braces_span().start);
|
||||
let leading_options = [after_comma, take_up_to(&mut before_attributes, outer.start)];
|
||||
let leading_property = [
|
||||
take_up_to(&mut after_source, keyword),
|
||||
take_up_to(&mut before_attributes, keyword),
|
||||
];
|
||||
let (at_end_of_line_before_colon, trailing_keyword) =
|
||||
take_up_to(&mut after_source, attributes.saturating_sub(1)).split_at_end_of_line();
|
||||
ImportTypeComments {
|
||||
leading_import,
|
||||
leading_source,
|
||||
trailing_source: (of_source.trailing)
|
||||
.slice(0, before_comma.len() + on_line_of_comma.len()),
|
||||
leading_options,
|
||||
leading_property,
|
||||
at_end_of_line_before_colon,
|
||||
at_end_of_line_after_colon: after_source,
|
||||
trailing_keyword,
|
||||
leading_attributes: before_attributes,
|
||||
trailing_property: take_up_to(&mut after_attributes, outer.end.saturating_sub(1)),
|
||||
trailing_options: after_attributes,
|
||||
}
|
||||
}
|
||||
|
||||
/// The module specifier at `source`, with its comments.
|
||||
fn format_source(&self, source: Span) -> impl Format<'a> {
|
||||
let (leading, trailing) = (self.leading_source, self.trailing_source);
|
||||
format_with(move |f: &mut Formatter<'a>| {
|
||||
write!(f, FormatLeadingComments::Comments(leading));
|
||||
match has_ignore_comment(&[leading], trailing, f) {
|
||||
true => write!(f, FormatSuppressedNode(source)),
|
||||
false => write!(
|
||||
f,
|
||||
FormatLiteralStringToken::new(
|
||||
f.source_text().text_for(&source),
|
||||
false,
|
||||
StringLiteralParentKind::Expression
|
||||
)
|
||||
),
|
||||
}
|
||||
write!(f, FormatTrailingComments::Comments(trailing));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatModuleSpecifier<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let specifier = FormatModuleSpecifierWithoutComments(self.span, self.parent);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&specifier),
|
||||
format_trailing_comments(self.call_span, self.span, 0)
|
||||
]
|
||||
);
|
||||
}
|
||||
/// `import('a' /* prettier-ignore */)` is `import("a" /* prettier-ignore */)` for oxfmt: a `prettier-ignore` comment after a node
|
||||
/// applies to it only if it ends its line. For Prettier every comment of a node counts.
|
||||
fn trailing_ignore_comment_has_to_end_its_line(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Whether a `prettier-ignore` comment applies to the ESTree node that has the comments `leading` and `trailing`.
|
||||
fn has_ignore_comment<'a>(
|
||||
leading: &[CommentList<'a>],
|
||||
trailing: CommentList<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
let is_ignore_comment = |comment: Comment<'a>| f.comments().is_suppression_comment(comment);
|
||||
leading.iter().any(|it| it.iter().any(is_ignore_comment))
|
||||
|| trailing.iter().any(|comment| {
|
||||
is_ignore_comment(comment)
|
||||
&& (comment.has_newline_after() || !trailing_ignore_comment_has_to_end_its_line(f))
|
||||
})
|
||||
}
|
||||
|
||||
/// ```ts
|
||||
@@ -623,37 +643,42 @@ fn options_of_import_type_are_on_a_line_of_their_own(f: &Formatter<'_>) -> bool
|
||||
|
||||
/// `("a", { with: { type: "json" } })`. Prettier's `printCallArguments` for a string and an object.
|
||||
fn write_import_type_arguments<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
source: Span,
|
||||
options: ImportAttributes<'a>,
|
||||
comments: &ImportTypeComments<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let span = options.options_span();
|
||||
let format_source = format_with(|f| {
|
||||
let source = FormatModuleSpecifierWithoutComments(source, AstNodes::TSImportType(ty));
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&source),
|
||||
format_trailing_comments(ty.span(), source.span(), span.start)
|
||||
]
|
||||
);
|
||||
})
|
||||
.memoized();
|
||||
let format_source = comments.format_source(source).memoized();
|
||||
let does_source_break = format_source.inspect(f).will_break();
|
||||
|
||||
let has_comments = Cell::new(f.comments().has_comment_before(span.start));
|
||||
let has_comments = !(comments.leading_options.iter().all(|it| it.is_empty())
|
||||
&& comments.trailing_options.is_empty());
|
||||
let format_options = format_with(|f| {
|
||||
write!(f, format_leading_comments(span));
|
||||
write_import_type_options(options, f);
|
||||
let comments = f.comments().comments_before_character(span.end, b')');
|
||||
has_comments.set(has_comments.get() || !comments.is_empty());
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
for comments in comments.leading_options {
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
match has_ignore_comment(&comments.leading_options, comments.trailing_options, f) {
|
||||
true => write!(f, FormatSuppressedNode(options.options_span())),
|
||||
false => write_import_type_options(options, comments, f),
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments.trailing_options)
|
||||
);
|
||||
})
|
||||
.memoized();
|
||||
let do_options_break = format_options.inspect(f).will_break();
|
||||
|
||||
let head = format_with(|f| write!(f, [format_source, ",", soft_line_break_or_space()]));
|
||||
let first_comment = comments.leading_options.into_iter().flatten().next();
|
||||
let has_empty_line = f.source_text().get_lines_before(
|
||||
options.options_span(),
|
||||
first_comment.map(|comment| comment.span()),
|
||||
true,
|
||||
) > 1;
|
||||
let head = format_with(|f| match has_empty_line {
|
||||
true => write!(f, [format_source, ",", empty_line()]),
|
||||
false => write!(f, [format_source, ",", soft_line_break_or_space()]),
|
||||
});
|
||||
let (head, format_options) = (&head, &format_options);
|
||||
let all_broken_out = |should_expand: bool| {
|
||||
format_with(move |f: &mut Formatter<'a>| {
|
||||
@@ -665,10 +690,10 @@ fn write_import_type_arguments<'a>(
|
||||
);
|
||||
})
|
||||
};
|
||||
if has_comments.get() {
|
||||
if has_comments {
|
||||
return write!(f, all_broken_out(does_source_break || do_options_break));
|
||||
}
|
||||
if does_source_break {
|
||||
if does_source_break || has_empty_line {
|
||||
return write!(f, all_broken_out(true));
|
||||
}
|
||||
if !do_options_break && options_of_import_type_are_on_a_line_of_their_own(f) {
|
||||
@@ -705,9 +730,25 @@ fn write_import_type_arguments<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// ```ts
|
||||
/// type A = import("a", { type A = import("a", {
|
||||
/// with: // comment // comment
|
||||
/// { type: "json" }, with: { type: "json" },
|
||||
/// }); });
|
||||
/// ```
|
||||
///
|
||||
/// oxfmt on the left, Prettier on the right.
|
||||
fn comments_at_end_of_line_stay_after_colon(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `{ with: { type: "json" } }`. It is an object literal, of which the HIR has only the properties
|
||||
/// of the inner one.
|
||||
fn write_import_type_options<'a>(options: ImportAttributes<'a>, f: &mut Formatter<'a>) {
|
||||
fn write_import_type_options<'a>(
|
||||
options: ImportAttributes<'a>,
|
||||
comments: &ImportTypeComments<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let (outer, keyword, inner) = (
|
||||
options.options_span(),
|
||||
options.keyword_span(),
|
||||
@@ -719,14 +760,21 @@ fn write_import_type_options<'a>(options: ImportAttributes<'a>, f: &mut Formatte
|
||||
};
|
||||
let has_space = f.options().bracket_spacing.value();
|
||||
|
||||
let attributes = options.object().as_ast_nodes();
|
||||
let format_inner = format_with(|f| {
|
||||
write!(f, format_leading_comments(inner));
|
||||
write!(
|
||||
f,
|
||||
FormatLeadingComments::Comments(comments.leading_attributes)
|
||||
);
|
||||
if has_ignore_comment(&[comments.leading_attributes], CommentList::empty(), f) {
|
||||
return write!(f, FormatSuppressedNode(inner));
|
||||
}
|
||||
let Some(first) = entries.first() else {
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
"{",
|
||||
format_dangling_comments(inner).with_soft_block_indent(),
|
||||
dangling_comments(attributes).with_soft_block_indent(),
|
||||
"}"
|
||||
]
|
||||
);
|
||||
@@ -747,21 +795,61 @@ fn write_import_type_options<'a>(options: ImportAttributes<'a>, f: &mut Formatte
|
||||
);
|
||||
});
|
||||
let format_property = format_with(|f| {
|
||||
for comments in comments.leading_property {
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
if has_ignore_comment(&comments.leading_property, comments.trailing_property, f) {
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
FormatSuppressedNode(keyword.to(inner)),
|
||||
FormatTrailingComments::Comments(comments.trailing_property),
|
||||
FormatTrailingCommas::ES5
|
||||
]
|
||||
);
|
||||
}
|
||||
let at_end_of_line = [
|
||||
comments.at_end_of_line_before_colon,
|
||||
comments.at_end_of_line_after_colon,
|
||||
];
|
||||
let stay_after_colon = comments_at_end_of_line_stay_after_colon(f);
|
||||
if !stay_after_colon {
|
||||
for comments in at_end_of_line {
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
format_leading_comments(keyword),
|
||||
source_text(keyword),
|
||||
":",
|
||||
space(),
|
||||
format_inner
|
||||
FormatTrailingComments::Comments(comments.trailing_keyword),
|
||||
":"
|
||||
]
|
||||
);
|
||||
let comments = f.comments().comments_before(outer.end.saturating_sub(1));
|
||||
if stay_after_colon {
|
||||
for comments in at_end_of_line {
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
// The layout `break-after-operator` of Prettier's `printAssignment`.
|
||||
let breaks_after_colon = (comments.leading_attributes.iter())
|
||||
.any(|comment| comment.has_newline_after() || comment.is_indentable_block())
|
||||
|| (stay_after_colon && at_end_of_line.iter().any(|it| !it.is_empty()));
|
||||
match breaks_after_colon {
|
||||
true => write!(
|
||||
f,
|
||||
group(&indent(&format_args!(
|
||||
soft_line_break_or_space(),
|
||||
format_inner
|
||||
)))
|
||||
.should_expand(stay_after_colon)
|
||||
),
|
||||
false => write!(f, [space(), format_inner]),
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatTrailingComments::Comments(comments),
|
||||
FormatTrailingComments::Comments(comments.trailing_property),
|
||||
FormatTrailingCommas::ES5
|
||||
]
|
||||
);
|
||||
@@ -790,23 +878,15 @@ pub(crate) fn write_ts_type_predicate<'a>(ty: TypeNode<'a>, f: &mut Formatter<'a
|
||||
if f.file().is_flow() && super::flow::write_checks_predicate(ty, f) {
|
||||
return;
|
||||
}
|
||||
let parameter = ty
|
||||
.predicate_param()
|
||||
.map(|it| identifier(it, AstNodes::TSTypePredicate(ty)));
|
||||
let parameter = ty.predicate_param().map(|it| identifier(it));
|
||||
write!(f, asserts.then(|| super::flow::predicate_prefix(ty)));
|
||||
let Some(type_annotation) = type_annotation else {
|
||||
return write!(f, parameter);
|
||||
};
|
||||
write!(f, parameter.as_ref().map(FormatNodeWithoutTrailingComments));
|
||||
if let Some(parameter) = parameter {
|
||||
write!(
|
||||
f,
|
||||
format_trailing_comments(ty.span(), parameter.span(), type_annotation.span().start)
|
||||
);
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
parameter,
|
||||
space(),
|
||||
"is",
|
||||
space(),
|
||||
|
||||
@@ -16,7 +16,7 @@ pub(crate) fn write_ts_tuple_type<'a>(
|
||||
if elements.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
format_dangling_comments(ty.span()).with_soft_block_indent()
|
||||
dangling_comments(AstNodes::TSTupleType(ty)).with_soft_block_indent()
|
||||
);
|
||||
} else {
|
||||
let element_types = format_with(|f| {
|
||||
@@ -66,7 +66,7 @@ pub(crate) fn write_ts_tuple_element<'a>(element: TupleElem<'a>, f: &mut Formatt
|
||||
Some(label) => write!(
|
||||
f,
|
||||
[
|
||||
identifier(label, AstNodes::TSNamedTupleMember(element)),
|
||||
identifier(label),
|
||||
element.is_optional().then_some("?"),
|
||||
":",
|
||||
space(),
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
use crate::js::format::{format_node, identifier};
|
||||
use crate::js::utils::call_expression::is_test_call_expression_in_flavor;
|
||||
use crate::js::utils::typescript::{is_object_like_type, is_simple_type, should_hug_type};
|
||||
use crate::js::utils::typescript::{
|
||||
is_object_like_type, is_simple_type, should_hug_type, without_lone_operator,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -22,10 +24,7 @@ pub(crate) fn write_ts_type_parameter<'a>(param: TypeParam<'a>, f: &mut Formatte
|
||||
}
|
||||
}
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
identifier(param.name(), AstNodes::TSTypeParameter(param))
|
||||
);
|
||||
write!(f, identifier(param.name()));
|
||||
|
||||
match param.constraint() {
|
||||
Some(constraint) if is_flow => {
|
||||
@@ -109,49 +108,73 @@ impl<'a> FormatTSTypeParameters<'a> {
|
||||
/// Without the comments around the `<..>`.
|
||||
pub(crate) fn write_without_comments(self, f: &mut Formatter<'a>) {
|
||||
let Self { params, owner, .. } = self;
|
||||
if params.is_empty() && self.options.is_type_or_interface_decl {
|
||||
return write!(f, "<>");
|
||||
}
|
||||
let node = AstNodes::TSTypeParameterDeclaration(owner);
|
||||
if params.is_empty() && self.options.is_type_or_interface_decl {
|
||||
return write!(f, ["<", FormatDanglingCommentsForInline(node), ">"]);
|
||||
}
|
||||
let is_in_test_call = matches!(node.parent().parent(), AstNodes::CallExpression(call) if is_test_call_expression_in_flavor(call, f));
|
||||
if is_in_test_call
|
||||
&& !(params.iter())
|
||||
.any(|param| has_comment_that_breaks(AstNodes::TSTypeParameter(param), f))
|
||||
{
|
||||
let format_params = format_with(|f| {
|
||||
f.join_nodes_with_space().entries_with_trailing_separator(
|
||||
params.iter(),
|
||||
",",
|
||||
TrailingSeparator::Omit,
|
||||
);
|
||||
});
|
||||
return write!(
|
||||
f,
|
||||
group(&format_args!("<", format_params, ">")).with_group_id(self.options.group_id)
|
||||
);
|
||||
}
|
||||
let trailing_separator =
|
||||
match should_force_trailing_comma_for_arrow_function(params, owner, f) {
|
||||
true => TrailingSeparator::Mandatory,
|
||||
false => FormatTrailingCommas::ES5.trailing_separator(f.options()),
|
||||
};
|
||||
let format_params = format_with(|f| {
|
||||
f.join_with(soft_line_break_or_space())
|
||||
.entries_with_trailing_separator(params.iter(), ",", trailing_separator);
|
||||
});
|
||||
write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
"<",
|
||||
format_with(|f| {
|
||||
if matches!(node.parent().parent(), AstNodes::CallExpression(call) if is_test_call_expression_in_flavor(call, f)) {
|
||||
f.join_nodes_with_space().entries_with_trailing_separator(
|
||||
params.iter(),
|
||||
",",
|
||||
TrailingSeparator::Omit,
|
||||
);
|
||||
} else {
|
||||
let trailing_separator = match should_force_trailing_comma_for_arrow_function(params, owner, f)
|
||||
{
|
||||
true => TrailingSeparator::Mandatory,
|
||||
false => FormatTrailingCommas::ES5.trailing_separator(f.options()),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
soft_block_indent(&format_with(|f| {
|
||||
f.join_with(soft_line_break_or_space()).entries_with_trailing_separator(
|
||||
params.iter(),
|
||||
",",
|
||||
trailing_separator,
|
||||
);
|
||||
}))
|
||||
);
|
||||
}
|
||||
if !f.is_quiet() {
|
||||
format_dangling_comments(node.span()).with_soft_block_indent().fmt(f);
|
||||
}
|
||||
}),
|
||||
">"
|
||||
))
|
||||
.with_group_id(self.options.group_id)
|
||||
group(&format_args!("<", soft_block_indent(&format_params), ">"))
|
||||
.with_group_id(self.options.group_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `printDanglingCommentsForInline`: the comments in `</* comment */>`.
|
||||
pub(crate) struct FormatDanglingCommentsForInline<'a>(pub(crate) AstNodes<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatDanglingCommentsForInline<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let comments = f.comments().of_node(self.0).dangling;
|
||||
let indent = match comments.iter().any(|comment| comment.is_line()) {
|
||||
true => DanglingIndentMode::Block,
|
||||
false => DanglingIndentMode::None,
|
||||
};
|
||||
write!(f, FormatDanglingComments::Comments { comments, indent });
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a type parameter or type argument has a comment that does not fit between `<` and `>` on one line: a line
|
||||
/// comment, or a last comment that ends its line.
|
||||
fn has_comment_that_breaks<'a>(node: AstNodes<'a>, f: &Formatter<'a>) -> bool {
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
let owned = f.comments().of_node(node);
|
||||
(owned.leading.iter().chain(owned.trailing)).any(|comment| comment.is_line())
|
||||
|| (owned.trailing.last().or_else(|| owned.leading.last()))
|
||||
.is_some_and(|comment| comment.has_newline_after())
|
||||
}
|
||||
|
||||
impl Spanned for FormatTSTypeParameters<'_> {
|
||||
fn span(&self) -> Span {
|
||||
AstNodes::TSTypeParameterDeclaration(self.owner).span()
|
||||
@@ -167,12 +190,7 @@ impl<'a> Format<'a> for FormatTSTypeParameters<'a> {
|
||||
return self.write_without_comments(f);
|
||||
}
|
||||
let node = AstNodes::TSTypeParameterDeclaration(self.owner);
|
||||
format_node(
|
||||
node.span(),
|
||||
|| node.parent(),
|
||||
f,
|
||||
|f| self.write_without_comments(f),
|
||||
);
|
||||
format_node(node, f, |f| self.write_without_comments(f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +228,7 @@ impl<'a> FormatTypeArguments<'a> {
|
||||
let params = self.params;
|
||||
let should_inline = params.len() == 1
|
||||
&& params.first().is_some_and(|first| {
|
||||
should_hug_single_type(first, f) && !self.has_comment_on_own_line(first, f)
|
||||
should_hug_single_type(first, f) && !has_comment_that_unhugs(first, f)
|
||||
})
|
||||
&& !is_arrow_function_variable_type_argument(self);
|
||||
|
||||
@@ -234,27 +252,14 @@ impl<'a> FormatTypeArguments<'a> {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a comment around `only`, the only type argument, is a line comment, or the last one
|
||||
/// ends its line.
|
||||
fn has_comment_on_own_line(self, only: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
let (outer, inner) = (
|
||||
AstNodes::TSTypeParameterInstantiation(self.owner).span(),
|
||||
only.span(),
|
||||
);
|
||||
let before = f.comments().comments_in(Span::before(outer.start, inner));
|
||||
let after = f.comments().comments_in(Span::after(inner, outer.end));
|
||||
if only_comment_behind_type_argument_counts(f) {
|
||||
return !after.is_empty();
|
||||
}
|
||||
before.iter().chain(after).any(|comment| comment.is_line())
|
||||
|| after
|
||||
.last()
|
||||
.or_else(|| before.last())
|
||||
.is_some_and(|comment| comment.followed_by_newline())
|
||||
/// Whether a comment of `only`, the only type argument, keeps it from being written directly between `<` and `>`.
|
||||
fn has_comment_that_unhugs<'a>(only: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
let node = without_lone_operator(only).as_ast_nodes();
|
||||
match only_comment_behind_type_argument_counts(f) {
|
||||
true => !f.is_quiet() && !f.comments().of_node(node).trailing.is_empty(),
|
||||
false => has_comment_that_breaks(node, f),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,12 +272,7 @@ impl<'a> Format<'a> for FormatTypeArguments<'a> {
|
||||
return self.write_without_comments(f);
|
||||
}
|
||||
let node = AstNodes::TSTypeParameterInstantiation(self.owner);
|
||||
format_node(
|
||||
node.span(),
|
||||
|| node.parent(),
|
||||
f,
|
||||
|f| self.write_without_comments(f),
|
||||
);
|
||||
format_node(node, f, |f| self.write_without_comments(f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ fn should_hug_single_type<'a>(ty: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
is_simple_type(ty)
|
||||
|| is_object_like_type(ty)
|
||||
// `A<B | null | undefined>`
|
||||
|| matches!(ty.kind(), TypeKind::Union(types) if should_hug_type(ty, types, f))
|
||||
|| matches!(ty.kind(), TypeKind::Union(types) if should_hug_type(types, f))
|
||||
// Flow's `?T`
|
||||
|| (ty.tag() == TypeTag::JSDoc && f.file().is_flow())
|
||||
}
|
||||
|
||||
+254
-346
@@ -1,14 +1,14 @@
|
||||
use crate::js::format::write_trailing_comments_of;
|
||||
use super::ts_declarations::{
|
||||
comments_behind_type_alias_operator, comments_trailing_type_alias_left_side,
|
||||
};
|
||||
use crate::js::parentheses::ts_type::{effective_parent, needs_parentheses};
|
||||
use crate::js::siblings::following_span_start_in;
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::utils::suppressed::FormatSuppressedNode;
|
||||
use crate::js::utils::typescript::{end_of_line_comments, should_hug_type, union_leading_comments};
|
||||
use crate::js::utils::typescript::{should_hug_type, without_lone_operator};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
use std::cell::Cell;
|
||||
|
||||
/// `A | B`. The comments before it are not written yet.
|
||||
/// `A | B`
|
||||
pub(crate) fn write_ts_union_type<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
types: List<'a, TypeNode<'a>>,
|
||||
@@ -17,6 +17,42 @@ pub(crate) fn write_ts_union_type<'a>(
|
||||
write_ts_union_type_in(ty, types, false, f);
|
||||
}
|
||||
|
||||
/// `[A, B | C]`
|
||||
fn is_one_of_several_tuple_elements(parent: AstNodes<'_>) -> bool {
|
||||
matches!(parent, AstNodes::TSTupleType(tuple)
|
||||
if matches!(tuple.kind(), TypeKind::Tuple(elements) if elements.len() > 1))
|
||||
}
|
||||
|
||||
/// Prettier's `shouldUnionTypePrintOwnComments`: [`write_ts_union_type_in`] writes the leading and trailing comments of the
|
||||
/// union `ty`, inside its indentation and its parentheses. False if `ty` is not a union.
|
||||
///
|
||||
/// ```ts
|
||||
/// let a:
|
||||
/// // comment
|
||||
/// | A
|
||||
/// | B;
|
||||
/// ```
|
||||
pub(crate) fn union_prints_own_comments<'a>(ty: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
let TypeKind::Union(types) = ty.kind() else {
|
||||
return false;
|
||||
};
|
||||
if types.len() <= 1 || should_hug_type(types, f) {
|
||||
return false;
|
||||
}
|
||||
let parent = effective_parent(ty.ast_parent());
|
||||
!is_one_of_several_tuple_elements(parent)
|
||||
&& !matches!(
|
||||
parent,
|
||||
AstNodes::TSUnionType(_) | AstNodes::TSIntersectionType(_)
|
||||
)
|
||||
&& !(comments_stay_outside_of_parentheses(f) && needs_parentheses(ty, f))
|
||||
}
|
||||
|
||||
/// `keyof /* comment */ (A | B)` stays as it is for oxfmt. Prettier writes the comments of a union in its parentheses.
|
||||
fn comments_stay_outside_of_parentheses(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier's `printUnionType`.
|
||||
///
|
||||
/// ```ts
|
||||
@@ -27,8 +63,9 @@ pub(crate) fn write_ts_union_type<'a>(
|
||||
/// | C
|
||||
/// ```
|
||||
///
|
||||
/// `is_indented_by_parent`: it is the right side of a type alias, which has a line break and an
|
||||
/// indentation after the `=`.
|
||||
/// `is_indented_by_parent`: `ty` is the right side of a type alias, which has written a line break and an indentation
|
||||
/// after the `=`.
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_ts_union_type_in<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
types: List<'a, TypeNode<'a>>,
|
||||
@@ -38,90 +75,65 @@ pub(crate) fn write_ts_union_type_in<'a>(
|
||||
if types.len() == 1 {
|
||||
return write!(f, types.first());
|
||||
}
|
||||
let node = AstNodes::TSUnionType(ty);
|
||||
let prints_own_comments = !f.is_quiet() && union_prints_own_comments(ty, f);
|
||||
let skips_trailing_comments =
|
||||
prints_own_comments && f.context_mut().take_skip_trailing_comments();
|
||||
let own_leading_comments = prints_own_comments.then(|| leading_comments(node));
|
||||
let own_trailing_comments =
|
||||
(prints_own_comments && !skips_trailing_comments).then(|| trailing_comments(node));
|
||||
|
||||
let (leading_comments, _) = union_leading_comments(ty, f);
|
||||
let format_leading_comments = FormatLeadingComments::Comments(leading_comments);
|
||||
|
||||
// A `prettier-ignore` comment on a line of its own is about the first type only.
|
||||
let suppression = leading_comments
|
||||
.iter()
|
||||
.find(|comment| f.comments().is_suppression_comment(comment));
|
||||
let is_suppressed = match prettier_ignore_before_union_is_about_all_of_it(f) {
|
||||
true => !union_prints_itself(ty, f) || follows_printed_suppression_comment(ty, f),
|
||||
false => suppression.is_some_and(|comment| !comment.preceded_by_newline()),
|
||||
};
|
||||
if is_suppressed {
|
||||
if is_ignored_by_comment_of_first_or_last_type(ty, f) {
|
||||
let comments_of_last_type = types.last().map_or(CommentList::empty(), |last| {
|
||||
let last = without_lone_operator(last).as_ast_nodes();
|
||||
f.comments().of_node(last).trailing
|
||||
});
|
||||
write!(
|
||||
f,
|
||||
[format_leading_comments, FormatSuppressedNode(ty.span())]
|
||||
[
|
||||
own_leading_comments,
|
||||
FormatSuppressedNode(ty.span()),
|
||||
FormatTrailingComments::Comments(comments_of_last_type),
|
||||
own_trailing_comments
|
||||
]
|
||||
);
|
||||
// oxc's `write_suppressed_expression`: the comments before the `)` stay there, and a line comment has the `)` on
|
||||
// the next line.
|
||||
if prettier_ignore_before_union_is_about_all_of_it(f) && needs_parentheses(ty, f) {
|
||||
let comments = f.comments().comments_before_character(ty.span().end, b')');
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
if comments.last().is_some_and(|comment| comment.is_line()) {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
// The `)` goes on the next line.
|
||||
if needs_parentheses(ty, f) && comments_of_last_type.last().is_some_and(Comment::is_line) {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let parent = effective_parent(ty.ast_parent());
|
||||
// For oxfmt a comment that ends its line is above the type of a type alias, and both are indented.
|
||||
let has_comment_above = union_breaks_one_per_line(f)
|
||||
&& !f.is_quiet()
|
||||
&& matches!(parent, AstNodes::TSTypeAliasDeclaration(_))
|
||||
&& f.comments().has_leading_own_line_comment(ty.span().start);
|
||||
// `{ a: string } | null | void` is written like the object type alone.
|
||||
if !has_comment_above && should_hug_type(ty, types, f) {
|
||||
write!(f, format_leading_comments);
|
||||
if should_hug_type(types, f) {
|
||||
f.join_with(" | ").entries(types.iter());
|
||||
return;
|
||||
}
|
||||
|
||||
let is_one_of_several_tuple_elements = matches!(parent, AstNodes::TSTupleType(tuple) if matches!(tuple.kind(), TypeKind::Tuple(it) if it.len() > 1));
|
||||
// Prettier's `shouldUnionTypePrintOwnComments`. Otherwise they are outside of the parentheses.
|
||||
let prints_own_comments = !is_one_of_several_tuple_elements
|
||||
&& !matches!(
|
||||
parent,
|
||||
AstNodes::TSUnionType(_) | AstNodes::TSIntersectionType(_)
|
||||
);
|
||||
|
||||
let members = UnionMembers {
|
||||
ty,
|
||||
types,
|
||||
parent,
|
||||
is_first_type_suppressed: suppression.is_some()
|
||||
|| follows_printed_suppression_comment(ty, f),
|
||||
};
|
||||
if union_breaks_one_per_line(f) {
|
||||
return write_union_one_per_line(&members, is_one_of_several_tuple_elements, f);
|
||||
return write_union_one_per_line(
|
||||
ty,
|
||||
types,
|
||||
parent,
|
||||
prints_own_comments,
|
||||
own_trailing_comments,
|
||||
f,
|
||||
);
|
||||
}
|
||||
|
||||
let members = UnionMembers {
|
||||
types,
|
||||
comments_behind_first_operator: CommentList::empty(),
|
||||
};
|
||||
let printed = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
prints_own_comments.then_some(format_leading_comments),
|
||||
group(&format_args!(if_group_breaks(&"| "), members))
|
||||
own_leading_comments,
|
||||
group(&format_args!(if_group_breaks(&"| "), members)),
|
||||
own_trailing_comments
|
||||
]
|
||||
);
|
||||
if prints_own_comments {
|
||||
write_trailing_comments_of(AstNodes::TSUnionType(ty), f);
|
||||
// Prettier does not attach comments to the `: T` of a property signature.
|
||||
if f.comments().next_start() != u32::MAX
|
||||
&& matches!(parent, AstNodes::TSTypeAnnotation(_))
|
||||
&& matches!(parent.parent(), AstNodes::TSPropertySignature(_))
|
||||
{
|
||||
write_trailing_comments_of(parent, f);
|
||||
}
|
||||
}
|
||||
});
|
||||
if !prints_own_comments {
|
||||
write!(f, format_leading_comments);
|
||||
}
|
||||
|
||||
if needs_parentheses(ty, f) {
|
||||
return write!(
|
||||
f,
|
||||
@@ -131,7 +143,7 @@ pub(crate) fn write_ts_union_type_in<'a>(
|
||||
))
|
||||
);
|
||||
}
|
||||
if is_one_of_several_tuple_elements {
|
||||
if is_one_of_several_tuple_elements(parent) {
|
||||
return write!(
|
||||
f,
|
||||
group(&format_args!(
|
||||
@@ -159,41 +171,27 @@ pub(crate) fn write_ts_union_type_in<'a>(
|
||||
write!(f, group(&indent(&format_args!(line, printed))));
|
||||
}
|
||||
|
||||
/// For oxfmt a `prettier-ignore` comment before a union is about the union, as before anything else.
|
||||
/// For Prettier one on a line of its own is about the first type.
|
||||
pub(crate) fn prettier_ignore_before_union_is_about_all_of_it(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `A /* prettier-ignore */ | B`: for oxfmt a comment behind a type is only about it if it ends the line.
|
||||
fn prettier_ignore_behind_type_ends_its_line(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Whether what the union `ty` is in has written a `prettier-ignore` comment that leads it, outside
|
||||
/// of its parentheses.
|
||||
fn follows_printed_suppression_comment<'a>(ty: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
if !f.comments().has_suppression_comments() {
|
||||
/// For oxfmt a `prettier-ignore` comment before a union applies to the whole union, as before any other node, and so does
|
||||
/// one after its last type that ends the line. For Prettier one on its own line before the union applies to the first type
|
||||
/// only, and one after the last type to that type.
|
||||
fn is_ignored_by_comment_of_first_or_last_type<'a>(ty: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
if !f.options().flavor.is_oxfmt() || f.is_quiet() || !f.comments().has_suppression_comments() {
|
||||
return false;
|
||||
}
|
||||
let mut end = ty.span().start;
|
||||
for comment in f.comments().printed_comments().iter().rev() {
|
||||
let is_adjacent = !comment.is_moved()
|
||||
&& comment.span.end <= end
|
||||
&& f.source_text()
|
||||
.all_bytes(Span::after(comment.span, end), |b| {
|
||||
b.is_ascii_whitespace() || b == b'('
|
||||
});
|
||||
if !is_adjacent {
|
||||
return false;
|
||||
}
|
||||
if f.comments().is_suppression_comment(comment) {
|
||||
// At the end of a line it trails what is before it.
|
||||
return comment.preceded_by_newline();
|
||||
}
|
||||
end = comment.span.start;
|
||||
}
|
||||
false
|
||||
let TypeKind::Union(types) = ty.kind() else {
|
||||
return false;
|
||||
};
|
||||
let is_ignore_comment = |comment: Comment<'a>| f.comments().is_suppression_comment(comment);
|
||||
(f.comments()
|
||||
.of_node(AstNodes::TSUnionType(ty))
|
||||
.leading
|
||||
.iter())
|
||||
.any(is_ignore_comment)
|
||||
|| types.last().is_some_and(|last| {
|
||||
let last = without_lone_operator(last).as_ast_nodes();
|
||||
(f.comments().of_node(last).trailing.iter())
|
||||
.any(|comment| is_ignore_comment(comment) && comment.has_newline_after())
|
||||
})
|
||||
}
|
||||
|
||||
/// oxfmt follows Prettier 3.8: a union that does not fit where it is has each type on a line of its
|
||||
@@ -208,51 +206,89 @@ pub(crate) fn union_breaks_one_per_line(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// oxc's `TSUnionType::write`, which is `printUnionType` of Prettier 3.8, for a union that is not
|
||||
/// written like an object type.
|
||||
/// The comments that the union `ty`, whose parent is `parent`, writes before its first type, in source order:
|
||||
/// - the comments behind the `:` of a property signature, which dangle from it for oxfmt (`a: // comment ⏎ | B | C`),
|
||||
/// - the leading comments of the union,
|
||||
/// - the leading comments of its first type that are before its first `|`.
|
||||
fn comments_before_first_type<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
parent: AstNodes<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> [CommentList<'a>; 3] {
|
||||
let TypeKind::Union(types) = ty.kind() else {
|
||||
return [CommentList::empty(); 3];
|
||||
};
|
||||
if f.is_quiet() {
|
||||
return [CommentList::empty(); 3];
|
||||
}
|
||||
let behind_colon = match (parent, parent.parent()) {
|
||||
(AstNodes::TSTypeAnnotation(_), signature @ AstNodes::TSPropertySignature(_)) => {
|
||||
f.comments().of_node(signature).dangling
|
||||
}
|
||||
_ => CommentList::empty(),
|
||||
};
|
||||
let of_first_type = types.first().map_or(CommentList::empty(), |first| {
|
||||
let first = without_lone_operator(first).as_ast_nodes();
|
||||
let leading_comments = f.comments().of_node(first).leading;
|
||||
let before_union = (leading_comments.iter())
|
||||
.take_while(|comment| comment.span().start < ty.span().start)
|
||||
.count();
|
||||
leading_comments.slice(0, before_union)
|
||||
});
|
||||
let of_union = f.comments().of_node(AstNodes::TSUnionType(ty)).leading;
|
||||
[behind_colon, of_union, of_first_type]
|
||||
}
|
||||
|
||||
/// oxc's `TSUnionType::write`, which is `printUnionType` of Prettier 3.8, for a union that is not written like an object
|
||||
/// type.
|
||||
fn write_union_one_per_line<'a>(
|
||||
members: &UnionMembers<'a>,
|
||||
is_one_of_several_tuple_elements: bool,
|
||||
ty: TypeNode<'a>,
|
||||
types: List<'a, TypeNode<'a>>,
|
||||
parent: AstNodes<'a>,
|
||||
prints_own_comments: bool,
|
||||
own_trailing_comments: Option<FormatTrailingComments<'a>>,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let leading_comments = match f.is_quiet() {
|
||||
true => &[][..],
|
||||
false => f.comments().comments_before(members.ty.span().start),
|
||||
let needs_parentheses = needs_parentheses(ty, f);
|
||||
let comments = match (prints_own_comments, needs_parentheses) {
|
||||
(true, _) => comments_before_first_type(ty, parent, f),
|
||||
// `( // comment ⏎ A | B)[]`. The comments of the union are outside of the parentheses.
|
||||
(false, true) => {
|
||||
let [_, _, of_first_type] = comments_before_first_type(ty, parent, f);
|
||||
[CommentList::empty(), CommentList::empty(), of_first_type]
|
||||
}
|
||||
(false, false) => [CommentList::empty(); 3],
|
||||
};
|
||||
let is_after_code_at_end_of_line = |comment: &Comment| {
|
||||
comment.is_block() && !comment.preceded_by_newline() && comment.followed_by_newline()
|
||||
let [_, of_union, _] = comments;
|
||||
let leading_comments = || comments.into_iter().flatten();
|
||||
let is_after_code_at_end_of_line = |comment: Comment<'_>| {
|
||||
comment.is_block() && !comment.has_newline_before() && comment.has_newline_after()
|
||||
};
|
||||
let is_in_type_alias = matches!(members.parent, AstNodes::TSTypeAliasDeclaration(_));
|
||||
let has_end_of_line_comment = leading_comments
|
||||
.iter()
|
||||
.any(|comment| comment.followed_by_newline());
|
||||
// Comments go behind the `|` that is written before the first type, which writes them, wherever they are in the
|
||||
// text. Only if one of them ends its line do they all stay before it: there would be a line break between the
|
||||
// `|` and the type.
|
||||
let is_in_type_alias = matches!(parent, AstNodes::TSTypeAliasDeclaration(_));
|
||||
let has_end_of_line_comment = leading_comments().any(|comment| comment.has_newline_after());
|
||||
// Comments go behind the `|` that is written before the first type, wherever they are in the text. Only if one of
|
||||
// them ends its line do they all stay before it: there would be a line break between the `|` and the type.
|
||||
//
|
||||
// ```ts
|
||||
// type A = /* comment */ B | C; type A =
|
||||
// | /* comment */ B
|
||||
// | C;
|
||||
// ```
|
||||
let comments_before_operator = match has_end_of_line_comment {
|
||||
true => leading_comments,
|
||||
false => &[][..],
|
||||
};
|
||||
let has_comments_before_operator = has_end_of_line_comment;
|
||||
// After the `?` or `:` of a conditional type the comments are behind the operator, and what follows is in line
|
||||
// with them.
|
||||
let is_branch_of_conditional_type = matches!(members.parent, AstNodes::TSConditionalType(it) if matches!(it.kind(), TypeKind::Cond { yes, no, .. } if yes.span().contains(members.ty.span()) || no.span().contains(members.ty.span())));
|
||||
let is_branch_of_conditional_type = matches!(parent, AstNodes::TSConditionalType(it) if matches!(it.kind(), TypeKind::Cond { yes, no, .. } if yes.span().contains(ty.span()) || no.span().contains(ty.span())));
|
||||
|
||||
let should_indent = match members.parent {
|
||||
let should_indent = match parent {
|
||||
AstNodes::TSTypeAssertion(_)
|
||||
| AstNodes::TSTupleType(_)
|
||||
| AstNodes::TSTypeParameterInstantiation(_) => false,
|
||||
// The line break after the `=`, which a comment forces, comes with an indentation.
|
||||
AstNodes::TSTypeAliasDeclaration(statement) => {
|
||||
!type_alias_union_breaks_after_operator(statement, leading_comments, f)
|
||||
!type_alias_union_breaks_after_operator(statement, ty, f)
|
||||
}
|
||||
AstNodes::TSConditionalType(_) if is_branch_of_conditional_type => {
|
||||
comments_before_operator.is_empty()
|
||||
!has_comments_before_operator
|
||||
}
|
||||
// A line comment before `as` is written behind it, with a line break and an indentation.
|
||||
AstNodes::TSAsExpression(cast) | AstNodes::TSSatisfiesExpression(cast) => {
|
||||
@@ -260,8 +296,14 @@ fn write_union_one_per_line<'a>(
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
let needs_parentheses = needs_parentheses(members.ty, f);
|
||||
let starts_with_line_break = should_indent && comments_before_operator.is_empty();
|
||||
let starts_with_line_break = should_indent && !has_comments_before_operator;
|
||||
let members = UnionMembers {
|
||||
types,
|
||||
comments_behind_first_operator: match has_comments_before_operator {
|
||||
true => CommentList::empty(),
|
||||
false => of_union,
|
||||
},
|
||||
};
|
||||
let types = format_with(|f| {
|
||||
let first_separator = format_args!(
|
||||
starts_with_line_break.then_some(soft_line_break_or_space()),
|
||||
@@ -272,7 +314,7 @@ fn write_union_one_per_line<'a>(
|
||||
let content = format_with(|f| {
|
||||
if needs_parentheses {
|
||||
write!(f, [indent(&types), soft_line_break()]);
|
||||
} else if is_one_of_several_tuple_elements {
|
||||
} else if is_one_of_several_tuple_elements(parent) {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
@@ -290,74 +332,69 @@ fn write_union_one_per_line<'a>(
|
||||
});
|
||||
|
||||
// `| (A | B)`
|
||||
let is_only_type = matches!(members.ty.ast_parent(), AstNodes::TSUnionType(_))
|
||||
&& !matches!(members.parent, AstNodes::TSUnionType(_));
|
||||
let has_own_line_comment = leading_comments
|
||||
.iter()
|
||||
.any(|comment| comment.preceded_by_newline())
|
||||
let is_only_type = matches!(ty.ast_parent(), AstNodes::TSUnionType(_))
|
||||
&& !matches!(parent, AstNodes::TSUnionType(_));
|
||||
let has_own_line_comment = leading_comments().any(|comment| comment.has_newline_before())
|
||||
|| (is_in_type_alias
|
||||
&& leading_comments
|
||||
.iter()
|
||||
.any(|it| is_after_code_at_end_of_line(it) && !is_jsdoc(it, f)));
|
||||
&& leading_comments().any(|it| is_after_code_at_end_of_line(it) && !is_jsdoc(it)));
|
||||
let breaks_before_comments = (has_own_line_comment
|
||||
|| (has_end_of_line_comment && is_only_type))
|
||||
&& !is_branch_of_conditional_type;
|
||||
let inner = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
breaks_before_comments.then_some(soft_line_break()),
|
||||
FormatLeadingComments::Comments(comments_before_operator),
|
||||
group(&content)
|
||||
]
|
||||
);
|
||||
write!(f, breaks_before_comments.then_some(soft_line_break()));
|
||||
if has_comments_before_operator {
|
||||
for comments in comments {
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
}
|
||||
}
|
||||
write!(f, group(&content));
|
||||
});
|
||||
match should_indent && !needs_parentheses {
|
||||
true => write!(f, group(&indent(&inner))),
|
||||
false => write!(f, group(&inner)),
|
||||
}
|
||||
write!(f, own_trailing_comments);
|
||||
}
|
||||
|
||||
fn is_jsdoc(comment: &Comment, f: &Formatter<'_>) -> bool {
|
||||
f.source_text().text_for(comment).starts_with(b"/**")
|
||||
fn is_jsdoc(comment: Comment<'_>) -> bool {
|
||||
comment.text().starts_with(b"/**")
|
||||
}
|
||||
|
||||
/// oxc's `alias_union_breaks_after_operator`: whether the union that is the type of the type alias `statement` leaves
|
||||
/// the line break and the indentation to the `=`. `leading_comments`: those that are left before the union.
|
||||
/// oxc's `alias_union_breaks_after_operator`: whether the union `ty`, which is the type of the type alias `statement`,
|
||||
/// leaves the line break and the indentation to the `=`.
|
||||
///
|
||||
/// - A JSDoc comment behind code ends its line, or one over several lines has its stars lined up.
|
||||
/// - A comment that has been written behind the left side ends the line of the `=`.
|
||||
/// - A comment that trails the left side ends the line of the `=`.
|
||||
pub(crate) fn type_alias_union_breaks_after_operator<'a>(
|
||||
statement: Stmt<'a>,
|
||||
leading_comments: &[Comment],
|
||||
ty: TypeNode<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
let StmtKind::TypeAlias(alias) = statement.kind() else {
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
};
|
||||
let left_end = alias
|
||||
.type_params()
|
||||
.angle_brackets_span()
|
||||
.map_or_else(|| alias.name().span().end, |it| it.end);
|
||||
leading_comments.iter().any(|comment| {
|
||||
}
|
||||
let comments = comments_before_first_type(ty, AstNodes::TSTypeAliasDeclaration(statement), f);
|
||||
comments.into_iter().flatten().any(|comment| {
|
||||
comment.is_indentable_block()
|
||||
|| (is_jsdoc(comment, f)
|
||||
&& !comment.preceded_by_newline()
|
||||
&& comment.followed_by_newline())
|
||||
}) || f
|
||||
.comments()
|
||||
.printed_comments()
|
||||
.last()
|
||||
.is_some_and(|comment| comment.followed_by_newline() && comment.span.start > left_end)
|
||||
|| (is_jsdoc(comment) && !comment.has_newline_before() && comment.has_newline_after())
|
||||
}) || (comments_behind_type_alias_operator(statement, f).last())
|
||||
.or_else(|| comments_trailing_type_alias_left_side(statement, f).last())
|
||||
.is_some_and(|comment| comment.has_newline_after())
|
||||
}
|
||||
|
||||
/// oxc's `union_prints_itself`: a union that a `prettier-ignore` comment is about is written as it is, and what it is
|
||||
/// in treats it like any other type.
|
||||
/// oxc's `union_prints_itself`: a union that a `prettier-ignore` comment applies to is copied from the source, and its parent
|
||||
/// treats it like any other type.
|
||||
pub(crate) fn union_prints_itself<'a>(ty: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
matches!(ty.kind(), TypeKind::Union(_))
|
||||
&& (f.is_quiet()
|
||||
|| !(f.comments().is_suppressed(ty.span().start)
|
||||
|| f.comments().has_trailing_suppression_comment(ty.span().end)))
|
||||
|| !(f.comments().is_ignored(AstNodes::TSUnionType(ty))
|
||||
|| is_ignored_by_comment_of_first_or_last_type(ty, f)))
|
||||
}
|
||||
|
||||
/// Whether `ty`, the type of a property signature, is a union that writes the comments behind the `:` of the signature. See
|
||||
/// [`comments_before_first_type`].
|
||||
pub(crate) fn union_writes_comments_behind_colon<'a>(ty: TypeNode<'a>, f: &Formatter<'a>) -> bool {
|
||||
union_breaks_one_per_line(f) && union_prints_own_comments(ty, f) && union_prints_itself(ty, f)
|
||||
}
|
||||
|
||||
/// Prettier's `shouldIndentUnionType`.
|
||||
@@ -373,190 +410,61 @@ fn should_indent_union_type<'a>(ty: TypeNode<'a>, parent: AstNodes<'a>) -> bool
|
||||
}
|
||||
}
|
||||
|
||||
/// The types of a union that is not on one line with an object type, and the `|` between them.
|
||||
/// For oxfmt the comments after a type of a union are not aligned with it, unless it is a union in parentheses. For Prettier
|
||||
/// they are if there are comments before it.
|
||||
fn trailing_comments_are_outside_of_alignment(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// The types of a union that is not written like an object type, and the `|` between them.
|
||||
#[derive(Copy, Clone)]
|
||||
struct UnionMembers<'a> {
|
||||
ty: TypeNode<'a>,
|
||||
types: List<'a, TypeNode<'a>>,
|
||||
/// What the union is in.
|
||||
parent: AstNodes<'a>,
|
||||
/// A `prettier-ignore` comment is before the union.
|
||||
is_first_type_suppressed: bool,
|
||||
/// Leading comments of the union that are written with the first type.
|
||||
comments_behind_first_operator: CommentList<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for UnionMembers<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() && !self.is_first_type_suppressed {
|
||||
for (index, member) in self.types.iter().enumerate() {
|
||||
if index > 0 {
|
||||
write!(f, [soft_line_break_or_space(), "| "]);
|
||||
}
|
||||
write!(f, align(2, &member));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let is_suppressed = Cell::new(self.is_first_type_suppressed);
|
||||
let mut iter = self.types.iter().peekable();
|
||||
while let Some(member) = iter.next() {
|
||||
let next = iter.peek().copied();
|
||||
let (leading_comments, _) = comments_before_member(member, f);
|
||||
let is_suppression = |comment: &Comment| f.comments().is_suppression_comment(comment);
|
||||
// A `prettier-ignore` comment leads it, or trails it with the `|` behind it.
|
||||
let is_member_suppressed = is_suppressed.get()
|
||||
|| leading_comments.iter().any(is_suppression)
|
||||
|| (!prettier_ignore_behind_type_ends_its_line(f)
|
||||
&& next.is_some_and(|next| {
|
||||
let comments = f.comments().comments_in(member.span().between(next.span()));
|
||||
let trailing =
|
||||
comments.len() - count_leading_comments(comments, next.span().start, f);
|
||||
comments[..trailing].iter().any(|comment| {
|
||||
is_suppression(comment) && !comment.preceded_by_newline()
|
||||
})
|
||||
}));
|
||||
|
||||
let format_member = format_with(|f| write_member(member, is_member_suppressed, f));
|
||||
// The comments between it and the next type that do not lead that one.
|
||||
let format_trailing_comments = format_with(|f| {
|
||||
let comments = match next {
|
||||
Some(next) => {
|
||||
let (comments, start) = comments_before_member(next, f);
|
||||
// Prettier's `handleUnionTypeComments`: on a line of its own it is about the next type.
|
||||
is_suppressed.set(comments.iter().any(|comment| {
|
||||
f.comments().is_suppression_comment(comment)
|
||||
&& comment.preceded_by_newline()
|
||||
}));
|
||||
&comments[..comments.len() - count_leading_comments(comments, start, f)]
|
||||
}
|
||||
None => {
|
||||
// `A | (B /* comment */)`
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(
|
||||
f.comments().comments_before(self.ty.span().end)
|
||||
)
|
||||
);
|
||||
self.comments_after_last_member(f)
|
||||
}
|
||||
};
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
});
|
||||
|
||||
// The comments after a type are only aligned with it if there are comments before it. Never for oxfmt.
|
||||
if leading_comments.is_empty() {
|
||||
write!(f, [align(2, &format_member), format_trailing_comments]);
|
||||
} else if union_breaks_one_per_line(f) {
|
||||
let leading_comments = FormatLeadingComments::Comments(leading_comments);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
align(2, &format_args!(leading_comments, format_member)),
|
||||
format_trailing_comments
|
||||
]
|
||||
);
|
||||
} else {
|
||||
let leading_comments = FormatLeadingComments::Comments(leading_comments);
|
||||
write!(
|
||||
f,
|
||||
align(
|
||||
2,
|
||||
&format_args!(leading_comments, format_member, format_trailing_comments)
|
||||
)
|
||||
);
|
||||
}
|
||||
if next.is_some() {
|
||||
for (index, member) in self.types.iter().enumerate() {
|
||||
if index > 0 {
|
||||
write!(f, [soft_line_break_or_space(), "| "]);
|
||||
}
|
||||
if f.is_quiet() {
|
||||
write!(f, align(2, &member));
|
||||
continue;
|
||||
}
|
||||
// Only the type is aligned, so that a comment on its own line after it lines up with the `|`:
|
||||
//
|
||||
// ```ts
|
||||
// | A
|
||||
// // comment
|
||||
// | B
|
||||
// ```
|
||||
let member = without_lone_operator(member);
|
||||
let node = member.as_ast_nodes();
|
||||
let has_leading_comments = !f.comments().of_node(node).leading.is_empty();
|
||||
let are_trailing_comments_aligned = match trailing_comments_are_outside_of_alignment(f)
|
||||
{
|
||||
// `| (A | B /* comment */)`
|
||||
true => index > 0 && matches!(member.kind(), TypeKind::Union(_)),
|
||||
false => has_leading_comments,
|
||||
};
|
||||
if are_trailing_comments_aligned {
|
||||
write!(f, align(2, &member));
|
||||
continue;
|
||||
}
|
||||
let comments_of_union = (index == 0).then_some(FormatLeadingComments::Comments(
|
||||
self.comments_behind_first_operator,
|
||||
));
|
||||
let member = FormatNodeWithoutTrailingComments(&member);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
align(2, &format_args!(comments_of_union, member)),
|
||||
trailing_comments(node)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> UnionMembers<'a> {
|
||||
/// Prettier's `handleLastUnionElementInExpression`: a comment at the end of a line after a union
|
||||
/// in parentheses is written after its last type.
|
||||
///
|
||||
/// ```ts
|
||||
/// type A = (
|
||||
/// | "a" // comment
|
||||
/// | "b" // comment
|
||||
/// )[];
|
||||
/// ```
|
||||
fn comments_after_last_member(&self, f: &Formatter<'a>) -> &'a [Comment] {
|
||||
if !matches!(
|
||||
self.parent,
|
||||
AstNodes::TSArrayType(_) | AstNodes::TSUnionType(_) | AstNodes::TSIntersectionType(_)
|
||||
) {
|
||||
return &[];
|
||||
}
|
||||
// Only those that are in the parent.
|
||||
let (span, parent_end) = (self.ty.span(), self.parent.span().end);
|
||||
let end = match following_span_start_in(span, self.parent, f.options().flavor) {
|
||||
0 => parent_end,
|
||||
following => following.min(parent_end),
|
||||
};
|
||||
end_of_line_comments(f.comments().comments_in(Span::after(span, end)))
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments before `member` that are not written by `member` itself, and where the rest of
|
||||
/// them, or `member`, starts.
|
||||
fn comments_before_member<'a>(member: TypeNode<'a>, f: &Formatter<'a>) -> (&'a [Comment], u32) {
|
||||
let start = member.span().start;
|
||||
match member.kind() {
|
||||
TypeKind::Union(types) if types.len() > 1 => {
|
||||
let (comments, first_type_comments) = union_leading_comments(member, f);
|
||||
(
|
||||
comments,
|
||||
first_type_comments
|
||||
.first()
|
||||
.map_or(start, |comment| comment.span.start),
|
||||
)
|
||||
}
|
||||
_ => (f.comments().comments_before(start), start),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many of `comments`, which are between two types of a union, lead the second one, which
|
||||
/// starts at `start`: those that are after the `|` on the line of the type. All others trail the
|
||||
/// first type (Prettier's `handleUnionTypeComments`).
|
||||
fn count_leading_comments(comments: &[Comment], start: u32, f: &Formatter<'_>) -> usize {
|
||||
let mut end = start;
|
||||
let mut count = 0;
|
||||
for comment in comments.iter().rev() {
|
||||
let is_adjacent = !comment.followed_by_newline()
|
||||
&& f.source_text()
|
||||
.all_bytes(Span::after(comment.span, end), |b| {
|
||||
b.is_ascii_whitespace() || b == b'('
|
||||
});
|
||||
if !is_adjacent {
|
||||
break;
|
||||
}
|
||||
if comment.preceded_by_newline() {
|
||||
return 0;
|
||||
}
|
||||
count += 1;
|
||||
end = comment.span.start;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// A type of a union. The comments before it are written, those after it are not.
|
||||
fn write_member<'a>(member: TypeNode<'a>, is_suppressed: bool, f: &mut Formatter<'a>) {
|
||||
let span = member.span();
|
||||
if !is_suppressed && !f.comments().has_trailing_suppression_comment(span.end) {
|
||||
return match member.kind() {
|
||||
// It takes some of the comments after it: see `comments_after_last_member`.
|
||||
TypeKind::Union(types) if types.len() > 1 => write!(f, member),
|
||||
_ => write!(f, FormatNodeWithoutTrailingComments(&member)),
|
||||
};
|
||||
}
|
||||
let needs_parentheses = needs_parentheses(member, f);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
needs_parentheses.then_some("("),
|
||||
FormatSuppressedNode(span),
|
||||
needs_parentheses.then_some(")")
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ fn body_of_for_loop_is_written_like_its_head(f: &Formatter<'_>) -> bool {
|
||||
}
|
||||
|
||||
/// `const a = 1, b = 2;`
|
||||
#[inline(never)]
|
||||
pub(crate) fn write_variable_declaration<'a>(
|
||||
statement: Stmt<'a>,
|
||||
declarations: List<'a, VarDecl<'a>>,
|
||||
@@ -99,11 +100,12 @@ impl<'a> Format<'a> for FormatVariableDeclarators<'a> {
|
||||
return;
|
||||
};
|
||||
|
||||
if length == 1
|
||||
&& !f
|
||||
.comments()
|
||||
.has_comment_before(first_declarator.span().start)
|
||||
{
|
||||
// Only oxfmt has dangling comments here: the ones that stay behind the `=`. They do not count.
|
||||
let has_comments = |declarator: VarDecl<'a>| {
|
||||
let comments = (f.comments()).of_node(AstNodes::VariableDeclarator(declarator));
|
||||
!comments.leading.is_empty() || !comments.trailing.is_empty()
|
||||
};
|
||||
if length == 1 && !self.declarations.first().is_some_and(has_comments) {
|
||||
return write!(f, first_declarator);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,719 +0,0 @@
|
||||
//! Where the next sibling of a node starts.
|
||||
//!
|
||||
//! [`Comments::get_trailing_comments`](super::comments::Comments::get_trailing_comments) needs it
|
||||
//! to tell the comments that trail a node from those that lead the next one. In oxc every node
|
||||
//! carries it around. Here it is only computed for a node that has a comment after it.
|
||||
//!
|
||||
//! The rules, which are those of oxc's generated `AstNode` accessors:
|
||||
//! - It is the start of the next child of the parent that is there, in the order of the fields.
|
||||
//! - After the last element of a list it is 0, "there is none", even if other fields follow. The
|
||||
//! lists that a rest element can follow, and directives, are the exception.
|
||||
//! - After the last child, it is that of the parent. If the parent is a statement, a pattern or a
|
||||
//! list of parameters, it is 0.
|
||||
|
||||
use super::ast_nodes::{
|
||||
AsAstNodes, AstNodes, ExpressionStatement, type_arguments_of, type_parameters_of,
|
||||
};
|
||||
use crate::options::Flavor;
|
||||
use bun_lint::ast::{
|
||||
ExprKind, FnBody, Func, Handle, ImportEqualsTarget, Key, List, Member, ModuleName, Node,
|
||||
PatKind, StmtKind, TypeKind,
|
||||
};
|
||||
use bun_lint::span::{Span, Spanned};
|
||||
|
||||
/// Goes through the children of a node in order, looking for the one after `me`.
|
||||
struct Finder {
|
||||
me: Span,
|
||||
is_found: bool,
|
||||
following: Option<u32>,
|
||||
/// oxfmt has a node for what Prettier has none: see [`Finder::transparent`].
|
||||
has_all_nodes: bool,
|
||||
}
|
||||
|
||||
impl Finder {
|
||||
/// A child that is there or not.
|
||||
fn one(&mut self, child: Option<Span>) -> &mut Self {
|
||||
if self.following.is_some() {
|
||||
return self;
|
||||
}
|
||||
match child {
|
||||
Some(child) if self.is_found => self.following = Some(child.start),
|
||||
Some(child) if child.contains(self.me) => self.is_found = true,
|
||||
_ => {}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// A child that Prettier has no node for, like the parentheses around parameters. What is before
|
||||
/// it is followed by the first thing in it, which starts at `first`.
|
||||
fn transparent(&mut self, child: Option<Span>, first: Option<u32>) -> &mut Self {
|
||||
if self.following.is_none()
|
||||
&& self.is_found
|
||||
&& child.is_some()
|
||||
&& first.is_some()
|
||||
&& !self.has_all_nodes
|
||||
{
|
||||
self.following = first;
|
||||
}
|
||||
self.one(child)
|
||||
}
|
||||
|
||||
/// A list of children in source order. `is_open`: what follows the list follows its last
|
||||
/// element.
|
||||
fn list<'a, T: Handle<'a> + Spanned>(&mut self, list: List<'a, T>, is_open: bool) -> &mut Self {
|
||||
self.list_of(list, is_open, |it| Some(it.span()))
|
||||
}
|
||||
|
||||
/// The same. `span_of`: where an element is. `None` if it is no child, like a hole in an array.
|
||||
fn list_of<'a, T: Handle<'a> + Spanned>(
|
||||
&mut self,
|
||||
list: List<'a, T>,
|
||||
is_open: bool,
|
||||
span_of: impl Fn(T) -> Option<Span>,
|
||||
) -> &mut Self {
|
||||
if self.following.is_some() {
|
||||
return self;
|
||||
}
|
||||
if self.is_found {
|
||||
self.following = list.iter().find_map(&span_of).map(|first| first.start);
|
||||
return self;
|
||||
}
|
||||
// A binary search for the first element that does not end before `me` does.
|
||||
let (mut low, mut high) = (0, list.len());
|
||||
while low < high {
|
||||
let middle = low + (high - low) / 2;
|
||||
match list.get(middle) {
|
||||
Some(it) if span_of(it).unwrap_or_else(|| it.span()).end < self.me.end => {
|
||||
low = middle + 1;
|
||||
}
|
||||
_ => high = middle,
|
||||
}
|
||||
}
|
||||
if (list.get(low).and_then(&span_of)).is_some_and(|it| it.contains(self.me)) {
|
||||
self.is_found = true;
|
||||
let next = (low + 1..list.len()).find_map(|at| list.get(at).and_then(&span_of));
|
||||
self.following = match next {
|
||||
Some(next) => Some(next.start),
|
||||
None if is_open => None,
|
||||
None => Some(0),
|
||||
};
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// The same for what is not a [`List`].
|
||||
fn spans(&mut self, spans: impl Iterator<Item = Span>, is_open: bool) -> &mut Self {
|
||||
if self.following.is_some() {
|
||||
return self;
|
||||
}
|
||||
let was_found = self.is_found;
|
||||
for span in spans {
|
||||
if self.is_found {
|
||||
self.following = Some(span.start);
|
||||
return self;
|
||||
}
|
||||
self.is_found = span.contains(self.me);
|
||||
}
|
||||
if self.is_found && !was_found && !is_open {
|
||||
self.following = Some(0);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn key_span<'a>(key: Option<Key<'a>>, node: Node<'a>) -> Option<Span> {
|
||||
key.map(|key| key.span(node.file()))
|
||||
}
|
||||
|
||||
/// The fields of everything that has parameters: type parameters, `this`, parameters, return type
|
||||
/// and body.
|
||||
fn function_fields(finder: &mut Finder, func: Func<'_>) {
|
||||
let first_parameter = func.params().first().map(|it| it.span().start);
|
||||
finder
|
||||
.one(func.type_params().angle_brackets_span())
|
||||
.one(func.this_param().map(|it| it.span()));
|
||||
// `this` is in the parentheses. If it is the only parameter, nothing follows it there.
|
||||
if finder.is_found
|
||||
&& finder.following.is_none()
|
||||
&& func
|
||||
.this_param()
|
||||
.is_some_and(|it| it.span().contains(finder.me))
|
||||
{
|
||||
finder.following = Some(first_parameter.unwrap_or(0));
|
||||
}
|
||||
finder
|
||||
.transparent(func.params_span(), first_parameter)
|
||||
.one(func.return_type().map(|it| it.annotation_span()))
|
||||
.one(match func.body() {
|
||||
FnBody::None => None,
|
||||
FnBody::Block(_) => func.body_span(),
|
||||
FnBody::Expr(e) => Some(e.span()),
|
||||
});
|
||||
}
|
||||
|
||||
/// The function of a method, after its key. Babel, which Prettier parses JavaScript with, has no
|
||||
/// node for it: the parameters are children of the method.
|
||||
fn method_function(finder: &mut Finder, func: Func<'_>) {
|
||||
let first_parameter = func.params().first().filter(|it| it.file().is_javascript());
|
||||
finder.transparent(
|
||||
Some(func.estree_span()),
|
||||
first_parameter.map(|it| it.span().start),
|
||||
);
|
||||
}
|
||||
|
||||
fn member_fields(finder: &mut Finder, member: Member<'_>) {
|
||||
finder
|
||||
.spans(
|
||||
member.decorators().map(|it| AstNodes::Decorator(it).span()),
|
||||
false,
|
||||
)
|
||||
.one(match member.constructor_keyword() {
|
||||
Some(keyword) => Some(keyword.span()),
|
||||
None => key_span(member.key(), Node::Member(member)),
|
||||
});
|
||||
match member.func() {
|
||||
// The `Function` of a method of a class is one child.
|
||||
Some(func) if matches!(func.as_ast_nodes(), AstNodes::Function(_)) => {
|
||||
method_function(finder, func)
|
||||
}
|
||||
Some(func) => function_fields(finder, func),
|
||||
None => {
|
||||
finder
|
||||
.one(member.ty().map(|it| it.annotation_span()))
|
||||
.one(member.init().map(|it| it.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the next sibling of the child of `parent` at `span` starts, or 0 if there is none. The child
|
||||
/// can be a name or something else that there is no [`AstNodes`] for.
|
||||
pub(crate) fn following_span_start_in(
|
||||
mut span: Span,
|
||||
mut parent: AstNodes<'_>,
|
||||
flavor: Flavor,
|
||||
) -> u32 {
|
||||
loop {
|
||||
match following_span_start_among_siblings(span, parent, flavor) {
|
||||
Some(following) => return following,
|
||||
None => (span, parent) = (parent.span(), parent.parent()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `None`: the child is the last one, and what follows `parent` follows it.
|
||||
fn following_span_start_among_siblings(
|
||||
span: Span,
|
||||
parent: AstNodes<'_>,
|
||||
flavor: Flavor,
|
||||
) -> Option<u32> {
|
||||
use AstNodes as N;
|
||||
let mut finder = Finder {
|
||||
me: span,
|
||||
is_found: false,
|
||||
following: None,
|
||||
has_all_nodes: flavor.is_oxfmt(),
|
||||
};
|
||||
let f = &mut finder;
|
||||
let span_of = |e: bun_lint::ast::Expr<'_>| Some(e.span());
|
||||
// Whether what follows the parent follows its last child.
|
||||
let mut inherits = true;
|
||||
|
||||
match parent {
|
||||
N::Program(file) => {
|
||||
inherits = false;
|
||||
f.list(file.0.body(), false);
|
||||
}
|
||||
N::ArrayExpression(e) | N::ArrayAssignmentTarget(e) => {
|
||||
let is_target = matches!(parent, N::ArrayAssignmentTarget(_));
|
||||
inherits = !is_target;
|
||||
if let ExprKind::Array(elements) = e.kind() {
|
||||
f.list_of(elements, is_target, |it| {
|
||||
(!it.is_missing()).then(|| it.span())
|
||||
});
|
||||
}
|
||||
}
|
||||
N::ObjectExpression(e) | N::ObjectAssignmentTarget(e) => {
|
||||
let is_target = matches!(parent, N::ObjectAssignmentTarget(_));
|
||||
inherits = !is_target;
|
||||
if let ExprKind::Object(props) = e.kind() {
|
||||
f.list(props, is_target);
|
||||
}
|
||||
}
|
||||
N::ObjectProperty(prop)
|
||||
| N::AssignmentTargetPropertyIdentifier(prop)
|
||||
| N::AssignmentTargetPropertyProperty(prop)
|
||||
| N::JSXAttribute(prop) => {
|
||||
f.one(key_span(prop.key(), Node::Prop(prop)));
|
||||
match prop.func() {
|
||||
Some(func) => method_function(f, func),
|
||||
None => {
|
||||
f.one(
|
||||
prop.value()
|
||||
.map(|it| it.jsx_container_span().unwrap_or_else(|| it.span())),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
N::TemplateLiteral(e) => {
|
||||
if let ExprKind::Template(template) = e.kind() {
|
||||
f.list(template.exprs(), false);
|
||||
}
|
||||
}
|
||||
N::TaggedTemplateExpression(e) | N::CallExpression(e) | N::NewExpression(e) => {
|
||||
if let ExprKind::TaggedTemplate(call) | ExprKind::Call(call) | ExprKind::New(call) =
|
||||
e.kind()
|
||||
{
|
||||
f.one(span_of(call.callee()))
|
||||
.one(call.type_args().angle_brackets_span())
|
||||
.one(call.template().map(|it| it.span()));
|
||||
if call.template().is_none() {
|
||||
f.list(call.args(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
N::ImportExpression(e) => {
|
||||
if let ExprKind::ImportCall { args } = e.kind() {
|
||||
f.list(args, true);
|
||||
}
|
||||
}
|
||||
N::StaticMemberExpression(e)
|
||||
| N::PrivateFieldExpression(e)
|
||||
| N::ComputedMemberExpression(e) => {
|
||||
match e.kind() {
|
||||
ExprKind::Dot { obj, name, .. } => f.one(span_of(obj)).one(Some(name.span())),
|
||||
ExprKind::Index { obj, index, .. } => f.one(span_of(obj)).one(span_of(index)),
|
||||
_ => f,
|
||||
};
|
||||
}
|
||||
N::BinaryExpression(e) | N::LogicalExpression(e) | N::PrivateInExpression(e) => {
|
||||
if let ExprKind::Binary { left, right, .. } = e.kind() {
|
||||
f.one(span_of(left)).one(span_of(right));
|
||||
}
|
||||
}
|
||||
N::SequenceExpression(e) => {
|
||||
f.spans(e.sequence().into_iter().map(|it| it.span()), false);
|
||||
}
|
||||
N::AssignmentExpression(e) | N::AssignmentTargetWithDefault(e) => {
|
||||
if let ExprKind::Assign { target, value, .. } = e.kind() {
|
||||
f.one(span_of(target)).one(span_of(value));
|
||||
}
|
||||
}
|
||||
N::ConditionalExpression(e) => {
|
||||
if let ExprKind::Cond { test, yes, no } = e.kind() {
|
||||
f.one(span_of(test)).one(span_of(yes)).one(span_of(no));
|
||||
}
|
||||
}
|
||||
N::TSAsExpression(e) | N::TSSatisfiesExpression(e) | N::TypeCastExpression(e) => {
|
||||
match e.kind() {
|
||||
ExprKind::As { expr, ty } | ExprKind::Satisfies { expr, ty } => {
|
||||
// Flow's `(a: T)`: Prettier's `TypeAnnotation` starts at the `:`.
|
||||
f.one(span_of(expr)).one(Some(match parent {
|
||||
N::TypeCastExpression(_) => ty.annotation_span(),
|
||||
_ => ty.span(),
|
||||
}));
|
||||
}
|
||||
ExprKind::AsConst(expr) => {
|
||||
f.one(span_of(expr)).one(e.const_keyword_span());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
N::TSTypeAssertion(e) => match e.kind() {
|
||||
ExprKind::As { expr, ty } => {
|
||||
f.one(Some(ty.span())).one(span_of(expr));
|
||||
}
|
||||
ExprKind::AsConst(expr) => {
|
||||
f.one(e.const_keyword_span()).one(span_of(expr));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
N::TSInstantiationExpression(e) => {
|
||||
if let ExprKind::Instantiation { expr, type_args } = e.kind() {
|
||||
f.one(span_of(expr)).one(type_args.angle_brackets_span());
|
||||
}
|
||||
}
|
||||
N::JSXElement(e) | N::JSXFragment(e) => {
|
||||
if let ExprKind::Jsx(jsx) = e.kind() {
|
||||
f.one(Some(jsx.opening_span()))
|
||||
.list_of(jsx.children(), false, |it| {
|
||||
Some(it.jsx_container_span().unwrap_or_else(|| it.span()))
|
||||
})
|
||||
.one(jsx.closing_span());
|
||||
}
|
||||
}
|
||||
N::JSXOpeningElement(e) => {
|
||||
if let ExprKind::Jsx(jsx) = e.kind() {
|
||||
// The `a` of `<a.b>` is followed by `b`.
|
||||
let mut name = jsx.tag();
|
||||
while let Some(ExprKind::Dot {
|
||||
obj,
|
||||
name: property,
|
||||
..
|
||||
}) = name.map(|it| it.kind())
|
||||
{
|
||||
if obj.span() == span {
|
||||
return Some(property.span().start);
|
||||
}
|
||||
name = Some(obj);
|
||||
}
|
||||
f.one(jsx.tag().map(|it| it.span()))
|
||||
.one(jsx.type_args().angle_brackets_span())
|
||||
.list(jsx.attrs(), false);
|
||||
}
|
||||
}
|
||||
|
||||
N::Function(func) | N::ArrowFunctionExpression(func) => {
|
||||
f.one(func.name().map(|it| it.span()));
|
||||
function_fields(f, func);
|
||||
}
|
||||
N::FunctionBody(func) => {
|
||||
if let FnBody::Block(statements) = func.body() {
|
||||
f.list(statements, false);
|
||||
}
|
||||
}
|
||||
N::FormalParameters(func) => {
|
||||
inherits = false;
|
||||
f.list(func.params(), true);
|
||||
}
|
||||
N::FormalParameter(param) | N::FormalParameterRest(param) | N::TSThisParameter(param) => {
|
||||
f.spans(param.decorators().map(|it| N::Decorator(it).span()), false)
|
||||
.one(Some(param.pat().span()))
|
||||
.one(param.ty().map(|it| it.annotation_span()))
|
||||
.one(param.default().map(|it| it.span()));
|
||||
}
|
||||
N::Class(class) => {
|
||||
f.spans(class.decorators().map(|it| N::Decorator(it).span()), false)
|
||||
.one(class.name().map(|it| it.span()))
|
||||
.one(class.type_params().angle_brackets_span())
|
||||
.one(class.extends().map(|it| it.span()))
|
||||
.one(class.extends_args().angle_brackets_span())
|
||||
.list(class.implements(), false)
|
||||
.one(Some(class.body_span()));
|
||||
}
|
||||
N::ClassBody(class) => {
|
||||
f.list(class.members(), false);
|
||||
}
|
||||
N::MethodDefinition(member)
|
||||
| N::PropertyDefinition(member)
|
||||
| N::AccessorProperty(member)
|
||||
| N::TSIndexSignature(member)
|
||||
| N::TSPropertySignature(member)
|
||||
| N::TSMethodSignature(member)
|
||||
| N::TSCallSignatureDeclaration(member)
|
||||
| N::TSConstructSignatureDeclaration(member) => member_fields(f, member),
|
||||
N::StaticBlock(member) => {
|
||||
if let Some(FnBody::Block(statements)) = member.func().map(Func::body) {
|
||||
f.list(statements, false);
|
||||
}
|
||||
}
|
||||
|
||||
N::VariableDeclarator(declaration) | N::CatchParameter(declaration) => {
|
||||
f.one(Some(declaration.pat().span()))
|
||||
.one(declaration.ty().map(|it| it.annotation_span()))
|
||||
.one(declaration.init().map(|it| it.span()));
|
||||
}
|
||||
N::ObjectPattern(pat) | N::ArrayPattern(pat) => {
|
||||
inherits = false;
|
||||
match pat.kind() {
|
||||
PatKind::Object(props) => f.list(props, true),
|
||||
PatKind::Array(elements) => {
|
||||
f.list_of(elements, true, |it| it.pat().is_some().then(|| it.span()))
|
||||
}
|
||||
_ => f,
|
||||
};
|
||||
}
|
||||
N::BindingProperty(prop) => {
|
||||
f.one(key_span(prop.key(), Node::PatProp(prop)))
|
||||
.one(Some(match prop.default() {
|
||||
Some(default) => prop.value().span().to(default.span()),
|
||||
None => prop.value().span(),
|
||||
}));
|
||||
}
|
||||
N::AssignmentPattern(it) => {
|
||||
let (pat, default) = match it {
|
||||
Node::PatProp(prop) => (Some(prop.value()), prop.default()),
|
||||
Node::PatElem(element) => (element.pat(), element.default()),
|
||||
_ => (None, None),
|
||||
};
|
||||
f.one(pat.map(|it| it.span()))
|
||||
.one(default.map(|it| it.span()));
|
||||
}
|
||||
|
||||
N::SwitchCase(case) => {
|
||||
f.one(case.test().map(|it| it.span()))
|
||||
.list(case.body(), false);
|
||||
}
|
||||
N::CatchClause(statement) => {
|
||||
if let StmtKind::Try { param, handler, .. } = statement.kind() {
|
||||
f.one(param.map(|it| it.span()))
|
||||
.one(handler.map(|it| it.span()));
|
||||
}
|
||||
}
|
||||
N::TSModuleBlock(statement) => {
|
||||
if let StmtKind::Module(module) = statement.kind() {
|
||||
f.list(module.innermost().body(), false);
|
||||
}
|
||||
}
|
||||
N::TSInterfaceBody(statement) => {
|
||||
if let StmtKind::Interface(interface) = statement.kind() {
|
||||
f.list(interface.members(), false);
|
||||
}
|
||||
}
|
||||
N::TSEnumBody(statement) => {
|
||||
if let StmtKind::Enum(it) = statement.kind() {
|
||||
f.list(it.members(), false);
|
||||
}
|
||||
}
|
||||
N::TSEnumMember(member) => {
|
||||
f.one(key_span(member.key(), Node::EnumMember(member)))
|
||||
.one(member.init().map(|it| it.span()));
|
||||
}
|
||||
N::ImportSpecifier(spec) => {
|
||||
f.one(Some(spec.imported().span()))
|
||||
.one(Some(spec.local().span()));
|
||||
}
|
||||
N::ExportSpecifier(spec) => {
|
||||
f.one(Some(spec.local().span()))
|
||||
.one(Some(spec.exported().span()));
|
||||
}
|
||||
|
||||
N::ExpressionStatement(ExpressionStatement::ArrowBody(_)) => {}
|
||||
N::ExpressionStatement(ExpressionStatement::Stmt(_)) | N::Directive(_) => inherits = false,
|
||||
N::ExportNamedDeclaration(statement) | N::ExportDefaultDeclaration(statement) => {
|
||||
inherits = false;
|
||||
if let StmtKind::ExportNamed(export) = statement.kind() {
|
||||
f.list(export.items(), false).one(export.spec_span());
|
||||
}
|
||||
}
|
||||
N::BlockStatement(statement)
|
||||
| N::EmptyStatement(statement)
|
||||
| N::DebuggerStatement(statement)
|
||||
| N::VariableDeclaration(statement)
|
||||
| N::ReturnStatement(statement)
|
||||
| N::IfStatement(statement)
|
||||
| N::ForStatement(statement)
|
||||
| N::ForInStatement(statement)
|
||||
| N::ForOfStatement(statement)
|
||||
| N::WhileStatement(statement)
|
||||
| N::DoWhileStatement(statement)
|
||||
| N::WithStatement(statement)
|
||||
| N::SwitchStatement(statement)
|
||||
| N::TryStatement(statement)
|
||||
| N::ThrowStatement(statement)
|
||||
| N::BreakStatement(statement)
|
||||
| N::ContinueStatement(statement)
|
||||
| N::LabeledStatement(statement)
|
||||
| N::ImportDeclaration(statement)
|
||||
| N::ExportAllDeclaration(statement)
|
||||
| N::TSInterfaceDeclaration(statement)
|
||||
| N::TSTypeAliasDeclaration(statement)
|
||||
| N::TSEnumDeclaration(statement)
|
||||
| N::TSModuleDeclaration(statement)
|
||||
| N::TSGlobalDeclaration(statement)
|
||||
| N::TSImportEqualsDeclaration(statement)
|
||||
| N::TSExportAssignment(statement)
|
||||
| N::TSNamespaceExportDeclaration(statement) => {
|
||||
inherits = false;
|
||||
// What is in the head of a `for`: the declaration, or the expression.
|
||||
let head = |it: bun_lint::ast::Stmt<'_>| match it.kind() {
|
||||
StmtKind::Expr(e) => e.span(),
|
||||
_ => it.span(),
|
||||
};
|
||||
match statement.kind() {
|
||||
StmtKind::Block(statements) => {
|
||||
f.list(statements, false);
|
||||
}
|
||||
StmtKind::Var(declarations) => {
|
||||
f.list(declarations, false);
|
||||
}
|
||||
StmtKind::If { test, yes, no } => {
|
||||
f.one(span_of(test))
|
||||
.one(Some(yes.span()))
|
||||
.one(no.map(|it| it.span()));
|
||||
}
|
||||
StmtKind::For {
|
||||
init,
|
||||
test,
|
||||
update,
|
||||
body,
|
||||
} => {
|
||||
f.one(init.map(head))
|
||||
.one(test.map(|it| it.span()))
|
||||
.one(update.map(|it| it.span()))
|
||||
.one(Some(body.span()));
|
||||
}
|
||||
StmtKind::ForIn { left, expr, body }
|
||||
| StmtKind::ForOf {
|
||||
left, expr, body, ..
|
||||
} => {
|
||||
f.one(Some(head(left)))
|
||||
.one(span_of(expr))
|
||||
.one(Some(body.span()));
|
||||
}
|
||||
StmtKind::While { test, body } => {
|
||||
f.one(span_of(test)).one(Some(body.span()));
|
||||
}
|
||||
StmtKind::DoWhile { body, test } => {
|
||||
f.one(Some(body.span())).one(span_of(test));
|
||||
}
|
||||
StmtKind::With { object, body } => {
|
||||
f.one(span_of(object)).one(Some(body.span()));
|
||||
}
|
||||
StmtKind::Switch { expr, cases } => {
|
||||
f.one(span_of(expr)).list(cases, false);
|
||||
}
|
||||
StmtKind::Try {
|
||||
block, finalizer, ..
|
||||
} => {
|
||||
f.one(Some(block.span()))
|
||||
.one(statement.catch_clause_span())
|
||||
.one(finalizer.map(|it| it.span()));
|
||||
}
|
||||
StmtKind::Labeled { body, .. } => {
|
||||
f.one(statement.label().map(|it| it.span()))
|
||||
.one(Some(body.span()));
|
||||
}
|
||||
StmtKind::Import(import) => {
|
||||
f.one(import.default().map(|it| it.span()))
|
||||
.one(import.namespace_span())
|
||||
.list(import.named(), false)
|
||||
.one(import.spec_span());
|
||||
}
|
||||
StmtKind::Interface(interface) => {
|
||||
f.one(Some(interface.name().span()))
|
||||
.one(interface.type_params().angle_brackets_span())
|
||||
.list(interface.extends(), false)
|
||||
.one(Some(interface.body_span()));
|
||||
}
|
||||
StmtKind::TypeAlias(alias) => {
|
||||
f.one(Some(alias.name().span()))
|
||||
.one(alias.type_params().angle_brackets_span())
|
||||
.one(Some(alias.ty().span()));
|
||||
}
|
||||
StmtKind::Enum(it) => {
|
||||
f.one(Some(it.name().span())).one(Some(it.body_span()));
|
||||
}
|
||||
StmtKind::Module(module) => {
|
||||
let name = match module.name() {
|
||||
ModuleName::Ident(name) | ModuleName::String(name) => Some(name.span()),
|
||||
ModuleName::Global => None,
|
||||
};
|
||||
f.one(name).one(module.innermost().body_span());
|
||||
}
|
||||
StmtKind::ImportEquals(import) => {
|
||||
f.one(Some(import.name().span()))
|
||||
.one(match import.target() {
|
||||
ImportEqualsTarget::Require(_) => import.require_span(),
|
||||
ImportEqualsTarget::Entity(name) => Some(name.span()),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
N::TSConditionalType(ty) => {
|
||||
if let TypeKind::Cond {
|
||||
check,
|
||||
extends,
|
||||
yes,
|
||||
no,
|
||||
} = ty.kind()
|
||||
{
|
||||
f.one(Some(check.span()))
|
||||
.one(Some(extends.span()))
|
||||
.one(Some(yes.span()))
|
||||
.one(Some(no.span()));
|
||||
}
|
||||
}
|
||||
N::TSUnionType(ty) | N::TSIntersectionType(ty) | N::TSTemplateLiteralType(ty) => {
|
||||
if let TypeKind::Union(types)
|
||||
| TypeKind::Intersection(types)
|
||||
| TypeKind::Template(types) = ty.kind()
|
||||
{
|
||||
f.list(types, false);
|
||||
}
|
||||
}
|
||||
N::TSTupleType(ty) => {
|
||||
if let TypeKind::Tuple(elements) = ty.kind() {
|
||||
f.list(elements, false);
|
||||
}
|
||||
}
|
||||
N::TSNamedTupleMember(element) => {
|
||||
f.one(element.name().map(|it| it.span()))
|
||||
.one(Some(element.ty().span()));
|
||||
}
|
||||
N::TSIndexedAccessType(ty) => {
|
||||
if let TypeKind::IndexedAccess { obj, index } = ty.kind() {
|
||||
f.one(Some(obj.span())).one(Some(index.span()));
|
||||
}
|
||||
}
|
||||
N::TSTypeReference(ty)
|
||||
| N::TSClassImplements(ty)
|
||||
| N::TSInterfaceHeritage(ty)
|
||||
| N::TSTypeQuery(ty)
|
||||
| N::TSImportType(ty) => {
|
||||
let name = match ty.kind() {
|
||||
TypeKind::Ref { name, .. } => Some(name.span()),
|
||||
TypeKind::Heritage { expr, .. } | TypeKind::Typeof { expr, .. } => {
|
||||
Some(expr.span())
|
||||
}
|
||||
TypeKind::Import { .. } => ty.import_source_span(),
|
||||
_ => None,
|
||||
};
|
||||
f.one(name)
|
||||
.one(type_arguments_of(Node::Type(ty)).and_then(|it| it.angle_brackets_span()));
|
||||
}
|
||||
N::TSTypeParameterInstantiation(owner) => {
|
||||
if let Some(arguments) = type_arguments_of(owner) {
|
||||
f.list(arguments, false);
|
||||
}
|
||||
}
|
||||
N::TSTypeParameterDeclaration(owner) => {
|
||||
if let Some(parameters) = type_parameters_of(owner) {
|
||||
f.list(parameters, false);
|
||||
}
|
||||
}
|
||||
N::TSTypeParameter(param) => {
|
||||
// Flow's `<T: U>`: as above.
|
||||
let is_annotation =
|
||||
Node::TypeParam(param).file().is_flow() && param.flow_token_after_name() == b":";
|
||||
f.one(Some(param.name().span()))
|
||||
.one(param.constraint().map(|it| match is_annotation {
|
||||
true => it.annotation_span(),
|
||||
false => it.span(),
|
||||
}))
|
||||
.one(param.default().map(|it| it.span()));
|
||||
}
|
||||
N::TSTypeLiteral(ty) => {
|
||||
if let TypeKind::Object(members) = ty.kind() {
|
||||
f.list(members, false);
|
||||
}
|
||||
}
|
||||
N::TSFunctionType(ty) | N::TSConstructorType(ty) => {
|
||||
if let TypeKind::Fn(func) = ty.kind() {
|
||||
function_fields(f, func);
|
||||
}
|
||||
}
|
||||
N::TSMappedType(ty) => {
|
||||
if let TypeKind::Mapped(mapped) = ty.kind() {
|
||||
f.one(Some(mapped.param().name().span()))
|
||||
.one(mapped.param().constraint().map(|it| it.span()))
|
||||
.one(mapped.name_type().map(|it| it.span()))
|
||||
.one(mapped.ty().map(|it| it.span()));
|
||||
}
|
||||
}
|
||||
N::TSTypePredicate(ty) => {
|
||||
if let TypeKind::Predicate { ty: asserted, .. } = ty.kind() {
|
||||
f.one(ty.predicate_param().map(|it| it.span()))
|
||||
.one(asserted.map(|it| it.annotation_span()));
|
||||
}
|
||||
}
|
||||
// One child, or none.
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match finder.following {
|
||||
None if !inherits => Some(0),
|
||||
following => following,
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,8 @@ impl<'a> Written<'a> {
|
||||
source: f.file().slice(source),
|
||||
has_comment_for_default: false,
|
||||
};
|
||||
if f.comments().has_comment_before(source.start) {
|
||||
let before_source = Span::new(statement.span().start, source.start);
|
||||
if f.comments().has_comment_in_span(before_source) {
|
||||
let keyword_end = statement.span().start + "import".len() as u32;
|
||||
// Without a `from`, everything is after it.
|
||||
let mut is_after_from = import.is_side_effect();
|
||||
@@ -74,18 +75,18 @@ impl<'a> Written<'a> {
|
||||
};
|
||||
let comment = f
|
||||
.comments()
|
||||
.comments_in(Span::before(position, source))
|
||||
.comments_in_span(Span::before(position, source))
|
||||
.iter()
|
||||
.find(|comment| {
|
||||
is_after_from |= bun_core::strings::contains(
|
||||
f.file().slice(Span::before(position, comment.span)),
|
||||
f.file().slice(Span::before(position, comment.span())),
|
||||
b"from",
|
||||
);
|
||||
position = comment.span.end;
|
||||
position = comment.span().end;
|
||||
is_after_from
|
||||
});
|
||||
if let Some(comment) = comment {
|
||||
let text = f.file().slice(comment.span);
|
||||
let text = f.file().slice(comment.span());
|
||||
written.source = bun_core::strings::index_of_char_usize(text, b'\n')
|
||||
.map_or(text, |end| text[..end].trim_ascii_end());
|
||||
}
|
||||
@@ -94,7 +95,7 @@ impl<'a> Written<'a> {
|
||||
.filter(|_| import.default().is_none());
|
||||
written.has_comment_for_default = star.is_some_and(|star| {
|
||||
!f.comments()
|
||||
.comments_in(Span::before(keyword_end, star))
|
||||
.comments_in_span(Span::before(keyword_end, star))
|
||||
.is_empty()
|
||||
});
|
||||
}
|
||||
@@ -474,10 +475,8 @@ impl<'a> ImportRun<'a> {
|
||||
|
||||
/// An import that is written as it is in the source ends a run.
|
||||
fn is_sortable(statement: Stmt<'a>, f: &Formatter<'a>) -> bool {
|
||||
let span = statement.span();
|
||||
matches!(statement.kind(), StmtKind::Import(_))
|
||||
&& !f.comments().is_suppressed(span.start)
|
||||
&& !f.comments().has_trailing_suppression_comment(span.end)
|
||||
&& !f.comments().is_ignored(statement.as_ast_nodes())
|
||||
}
|
||||
|
||||
/// To be called before the line break between the previous statement and `next` is written.
|
||||
@@ -571,21 +570,24 @@ impl<'a> ImportRun<'a> {
|
||||
self.line_start = f.elements().len();
|
||||
|
||||
// One line at a time. What is left for the statement to write is its own line.
|
||||
let mut comments = f.comments().comments_before(statement.span().start);
|
||||
let mut comments = f.comments().of_node(statement.as_ast_nodes()).leading;
|
||||
while let Some(first) = comments.first() {
|
||||
// Comments without anything between them belong together.
|
||||
let mut end = first.span.end;
|
||||
let count = 1 + comments[1..]
|
||||
let mut end = first.span().end;
|
||||
let count = 1 + comments
|
||||
.slice(1, usize::MAX)
|
||||
.iter()
|
||||
.take_while(|next| std::mem::replace(&mut end, next.span.end) == next.span.start)
|
||||
.take_while(|next| {
|
||||
std::mem::replace(&mut end, next.span().end) == next.span().start
|
||||
})
|
||||
.count();
|
||||
let (now, later) = comments.split_at(count);
|
||||
comments = later;
|
||||
write!(f, FormatLeadingComments::Comments(now));
|
||||
// oxc does not see the line break after a block comment that the file starts with.
|
||||
let starts_file = |it: &Comment| {
|
||||
let starts_file = |it: Comment<'_>| {
|
||||
it.is_block()
|
||||
&& f.file().text()[..it.span.start as usize]
|
||||
&& f.file().text()[..it.span().start as usize]
|
||||
.trim_ascii()
|
||||
.is_empty()
|
||||
};
|
||||
|
||||
@@ -53,21 +53,6 @@ impl<'a> SourceText<'a> {
|
||||
self.text.get(position as usize).copied()
|
||||
}
|
||||
|
||||
pub(crate) fn next_non_whitespace_byte_is(self, position: u32, expected: u8) -> bool {
|
||||
let mut rest = self.from(position).trim_ascii_start();
|
||||
while rest.first().is_some_and(|b| !b.is_ascii()) {
|
||||
match js_whitespace_len(rest) {
|
||||
0 => break,
|
||||
len => rest = rest[len..].trim_ascii_start(),
|
||||
}
|
||||
}
|
||||
rest.first() == Some(&expected)
|
||||
}
|
||||
|
||||
pub(crate) fn contains_byte(self, span: Span, byte: u8) -> bool {
|
||||
bun_core::strings::contains_char(self.text_for(&span), byte)
|
||||
}
|
||||
|
||||
/// White space that is not ASCII counts as a space, and so does the vertical tab, which `u8::is_ascii_whitespace` does not
|
||||
/// know. U+2028 and U+2029 count as `\n`.
|
||||
pub(crate) fn all_bytes(self, span: Span, predicate: impl Fn(u8) -> bool) -> bool {
|
||||
|
||||
+106
-158
@@ -1,15 +1,17 @@
|
||||
//! How comments are written.
|
||||
//!
|
||||
//! [`Comments`](super::comments::Comments) tells which comments belong where. This writes them,
|
||||
//! with the spaces and line breaks around them, and marks them as printed.
|
||||
//! [`Comments`](super::comments::Comments) says which comments a node owns. The functions here write them, with the spaces and
|
||||
//! line breaks around them, and mark them as printed. They are Prettier's `printLeadingComments`, `printTrailingComments` and
|
||||
//! `printDanglingComments`.
|
||||
|
||||
use super::comments::{Comment, CommentKind};
|
||||
use super::ast_nodes::AstNodes;
|
||||
use super::comments::{Comment, CommentExt, CommentList};
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
|
||||
/// 0 if something is before `comment` on its line, 1 if nothing is, 2 if the line before is empty
|
||||
/// as well: Prettier's `hasNewline(.., { backwards: true })` and `isPreviousLineEmpty`.
|
||||
fn lines_before(comment: &Comment, f: &Formatter<'_>) -> usize {
|
||||
fn lines_before(comment: Comment<'_>, f: &Formatter<'_>) -> usize {
|
||||
/// Without the blanks at the end of `text` and the line break before them, if there is one.
|
||||
fn without_last_line(text: &[u8]) -> Option<&[u8]> {
|
||||
let text = bun_core::strings::trim_right(text, b" \t");
|
||||
@@ -18,48 +20,50 @@ fn lines_before(comment: &Comment, f: &Formatter<'_>) -> usize {
|
||||
len => Some(&text[..text.len() - len]),
|
||||
}
|
||||
}
|
||||
let before = f.source_text().text_for(&Span::before(0, comment.span));
|
||||
let before = f.source_text().text_for(&Span::before(0, comment.span()));
|
||||
match without_last_line(before) {
|
||||
None => 0,
|
||||
Some(before) => 1 + usize::from(without_last_line(before).is_some()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments before the node at `span`.
|
||||
/// The leading comments that `node` owns.
|
||||
#[inline]
|
||||
pub(crate) const fn format_leading_comments<'a>(span: Span) -> FormatLeadingComments<'a> {
|
||||
FormatLeadingComments::Node(span)
|
||||
pub(crate) const fn leading_comments<'a>(node: AstNodes<'a>) -> FormatLeadingComments<'a> {
|
||||
FormatLeadingComments::Owner(node)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum FormatLeadingComments<'a> {
|
||||
Node(Span),
|
||||
Comments(&'a [Comment]),
|
||||
Owner(AstNodes<'a>),
|
||||
Comments(CommentList<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatLeadingComments<'a> {
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let (comments, node_start) = match *self {
|
||||
Self::Node(span) if f.comments().next_start() > span.start => return,
|
||||
Self::Node(span) => (f.comments().comments_leading_node(span), span.start),
|
||||
Self::Comments(comments) => (comments, u32::MAX),
|
||||
let comments = match *self {
|
||||
Self::Owner(_) if f.is_quiet() => return,
|
||||
Self::Owner(node) => f.comments().of_node(node).leading,
|
||||
Self::Comments(comments) => comments,
|
||||
};
|
||||
write_leading_comments(comments, node_start, f);
|
||||
if !comments.is_empty() {
|
||||
write_leading_comments(comments, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `a ⏎ /* comment */; ⏎ b`: the `;`, which is that of `a`, is written before the comment. For oxfmt
|
||||
/// the comment stays on a line of its own, so the line breaks after it are counted from behind the `;`.
|
||||
/// the comment stays on a line of its own, so the line breaks after it are counted from the end of the `;`.
|
||||
/// For Prettier it is on the line of `b`.
|
||||
pub(crate) fn comment_before_semicolon_keeps_its_line(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Where `comment`, which leads something, ends. See [`comment_before_semicolon_keeps_its_line`].
|
||||
fn end_with_semicolon_of_statement_before(comment: &Comment, f: &Formatter<'_>) -> u32 {
|
||||
let end = comment.span.end;
|
||||
if !comment.preceded_by_newline() || !comment_before_semicolon_keeps_its_line(f) {
|
||||
fn end_with_semicolon_of_statement_before(comment: Comment<'_>, f: &Formatter<'_>) -> u32 {
|
||||
let end = comment.span().end;
|
||||
if !comment.has_newline_before() || !comment_before_semicolon_keeps_its_line(f) {
|
||||
return end;
|
||||
}
|
||||
let rest = f.source_text().as_bytes().get(end as usize..);
|
||||
@@ -72,66 +76,62 @@ fn end_with_semicolon_of_statement_before(comment: &Comment, f: &Formatter<'_>)
|
||||
}
|
||||
}
|
||||
|
||||
/// `node_start`: where the node starts that they lead.
|
||||
/// Prettier's `mergeNestledJsdocComments`: `/** .. *//** .. */`, which JSDoc uses for the overloads of a function. The two
|
||||
/// comments stay together, without a space between them.
|
||||
fn is_nestled_jsdoc_comment(comment: Comment<'_>, next: Option<Comment<'_>>) -> bool {
|
||||
next.is_some_and(|next| {
|
||||
next.span().start == comment.span().end
|
||||
&& comment.is_indentable_block()
|
||||
&& next.is_indentable_block()
|
||||
})
|
||||
}
|
||||
|
||||
fn is_nestled_jsdoc_comment_after(previous: Option<Comment<'_>>, comment: Comment<'_>) -> bool {
|
||||
previous.is_some_and(|previous| is_nestled_jsdoc_comment(previous, Some(comment)))
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn write_leading_comments<'a>(comments: &'a [Comment], node_start: u32, f: &mut Formatter<'a>) {
|
||||
// A comment that has been moved out of a node that is written as it is in the source is in that
|
||||
// text.
|
||||
let is_ignored = comments
|
||||
.iter()
|
||||
.any(|comment| comment.is_moved() && f.comments().is_suppression_comment(comment));
|
||||
for comment in comments {
|
||||
f.comments_mut().increment_printed_count();
|
||||
if is_ignored && comment.is_moved() && comment.span.start >= node_start {
|
||||
fn write_leading_comments<'a>(comments: CommentList<'a>, f: &mut Formatter<'a>) {
|
||||
for (index, comment) in comments.iter().enumerate() {
|
||||
if !f.comments_mut().mark_printed(comment) {
|
||||
continue;
|
||||
}
|
||||
write!(f, comment);
|
||||
if is_nestled_jsdoc_comment(comment, comments.get(index + 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lines_after = (f.source_text().lines_after(comment.span.end)).max(
|
||||
let lines_after = (f.source_text().lines_after(comment.span().end)).max(
|
||||
f.source_text()
|
||||
.lines_after(end_with_semicolon_of_statement_before(comment, f)),
|
||||
);
|
||||
match comment.kind {
|
||||
CommentKind::SingleLineBlock | CommentKind::MultiLineBlock => match lines_after {
|
||||
0 => write!(f, space()),
|
||||
1 if lines_before(comment, f) == 0 => write!(f, soft_line_break_or_space()),
|
||||
1 => write!(f, hard_line_break()),
|
||||
_ => write!(f, empty_line()),
|
||||
},
|
||||
CommentKind::Line => match lines_after {
|
||||
0 | 1 => write!(f, hard_line_break()),
|
||||
_ => write!(f, empty_line()),
|
||||
},
|
||||
match (comment.is_block(), lines_after) {
|
||||
(true, 0) => write!(f, space()),
|
||||
(true, 1) if lines_before(comment, f) == 0 => write!(f, soft_line_break_or_space()),
|
||||
(_, 0 | 1) => write!(f, hard_line_break()),
|
||||
_ => write!(f, empty_line()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments after the node at `preceding_span`, whose parent is at `enclosing_span` and whose
|
||||
/// next sibling starts at `following_span_start`, which is 0 if there is none.
|
||||
/// The trailing comments that `node` owns.
|
||||
#[inline]
|
||||
pub(crate) const fn format_trailing_comments<'a>(
|
||||
enclosing_span: Span,
|
||||
preceding_span: Span,
|
||||
following_span_start: u32,
|
||||
) -> FormatTrailingComments<'a> {
|
||||
FormatTrailingComments::Node((enclosing_span, preceding_span, following_span_start))
|
||||
pub(crate) const fn trailing_comments<'a>(node: AstNodes<'a>) -> FormatTrailingComments<'a> {
|
||||
FormatTrailingComments::Owner(node)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum FormatTrailingComments<'a> {
|
||||
/// `(enclosing_span, preceding_span, following_span_start)`
|
||||
Node((Span, Span, u32)),
|
||||
Comments(&'a [Comment]),
|
||||
Owner(AstNodes<'a>),
|
||||
Comments(CommentList<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatTrailingComments<'a> {
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let comments = match *self {
|
||||
Self::Node((enclosing, ..)) if f.comments().next_start() >= enclosing.end => return,
|
||||
Self::Node((enclosing, preceding, following)) => f
|
||||
.comments()
|
||||
.get_trailing_comments(enclosing, preceding, following),
|
||||
Self::Owner(_) if f.is_quiet() => return,
|
||||
Self::Owner(node) => f.comments().of_node(node).trailing,
|
||||
Self::Comments(comments) => comments,
|
||||
};
|
||||
if !comments.is_empty() {
|
||||
@@ -141,22 +141,33 @@ impl<'a> Format<'a> for FormatTrailingComments<'a> {
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn write_trailing_comments<'a>(comments: &'a [Comment], f: &mut Formatter<'a>) {
|
||||
fn write_trailing_comments<'a>(comments: CommentList<'a>, f: &mut Formatter<'a>) {
|
||||
let mut total_lines_before = 0;
|
||||
// `a; // b` and `; // c` on the next line: the one before the empty statement has been written by itself.
|
||||
let mut previous_comment: Option<&Comment> = comments.first().and_then(|first| {
|
||||
let previous = f.comments().printed_comments().last()?;
|
||||
if first.span.start < previous.span.end {
|
||||
let mut previous_comment: Option<Comment<'a>> = comments.first().and_then(|first| {
|
||||
let previous = f.comments().last_printed()?;
|
||||
if first.span().start < previous.span().end {
|
||||
return None;
|
||||
}
|
||||
let between = previous.span.between(first.span);
|
||||
let between = previous.span().between(first.span());
|
||||
let is_only_empty_statements =
|
||||
(f.source_text()).all_bytes(between, |b| b.is_ascii_whitespace() || b == b';');
|
||||
(previous.is_line() && is_only_empty_statements).then_some(previous)
|
||||
});
|
||||
|
||||
for comment in comments {
|
||||
f.comments_mut().increment_printed_count();
|
||||
if !f.comments_mut().mark_printed(comment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_nestled_jsdoc_comment_after(previous_comment, comment) {
|
||||
match total_lines_before > 0 {
|
||||
true => write!(f, line_suffix(&comment)),
|
||||
false => write!(f, comment),
|
||||
}
|
||||
previous_comment = Some(comment);
|
||||
continue;
|
||||
}
|
||||
|
||||
let lines_before = lines_before(comment, f);
|
||||
total_lines_before += lines_before;
|
||||
@@ -190,7 +201,7 @@ fn write_trailing_comments<'a>(comments: &'a [Comment], f: &mut Formatter<'a>) {
|
||||
} else {
|
||||
// Nothing but a byte order mark is before it.
|
||||
let is_first_in_file =
|
||||
f.source_text().text_for(&Span::before(0, comment.span)) == b"\xEF\xBB\xBF";
|
||||
f.source_text().text_for(&Span::before(0, comment.span())) == b"\xEF\xBB\xBF";
|
||||
let content = format_with(|f| write!(f, [maybe_space(!is_first_in_file), comment]));
|
||||
if comment.is_line() {
|
||||
write!(f, [line_suffix(&content), expand_parent()]);
|
||||
@@ -203,22 +214,22 @@ fn write_trailing_comments<'a>(comments: &'a [Comment], f: &mut Formatter<'a>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments that are left in the node at `span`, which has nothing else in it: `{ /* a */ }`.
|
||||
/// The dangling comments that `node` owns.
|
||||
#[inline]
|
||||
pub(crate) const fn format_dangling_comments<'a>(span: Span) -> FormatDanglingComments<'a> {
|
||||
FormatDanglingComments::Node {
|
||||
span,
|
||||
pub(crate) const fn dangling_comments<'a>(node: AstNodes<'a>) -> FormatDanglingComments<'a> {
|
||||
FormatDanglingComments::Owner {
|
||||
node,
|
||||
indent: DanglingIndentMode::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum FormatDanglingComments<'a> {
|
||||
Node {
|
||||
span: Span,
|
||||
Owner {
|
||||
node: AstNodes<'a>,
|
||||
indent: DanglingIndentMode,
|
||||
},
|
||||
Comments {
|
||||
comments: &'a [Comment],
|
||||
comments: CommentList<'a>,
|
||||
indent: DanglingIndentMode,
|
||||
},
|
||||
}
|
||||
@@ -246,7 +257,7 @@ impl FormatDanglingComments<'_> {
|
||||
|
||||
fn with_indent_mode(mut self, mode: DanglingIndentMode) -> Self {
|
||||
match &mut self {
|
||||
FormatDanglingComments::Node { indent, .. }
|
||||
FormatDanglingComments::Owner { indent, .. }
|
||||
| FormatDanglingComments::Comments { indent, .. } => *indent = mode,
|
||||
}
|
||||
self
|
||||
@@ -257,8 +268,8 @@ impl<'a> Format<'a> for FormatDanglingComments<'a> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let (comments, indent) = match *self {
|
||||
Self::Node { span, .. } if f.comments().next_start() >= span.end => return,
|
||||
Self::Node { span, indent } => (f.comments().comments_before_end_of(span), indent),
|
||||
Self::Owner { .. } if f.is_quiet() => return,
|
||||
Self::Owner { node, indent } => (f.comments().of_node(node).dangling, indent),
|
||||
Self::Comments { comments, indent } => (comments, indent),
|
||||
};
|
||||
if !comments.is_empty() {
|
||||
@@ -269,21 +280,19 @@ impl<'a> Format<'a> for FormatDanglingComments<'a> {
|
||||
|
||||
#[cold]
|
||||
fn write_dangling_comments<'a>(
|
||||
comments: &'a [Comment],
|
||||
comments: CommentList<'a>,
|
||||
indent: DanglingIndentMode,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
let content = format_with(|f: &mut Formatter<'a>| {
|
||||
let mut previous_comment: Option<&Comment> = None;
|
||||
let mut previous_comment: Option<Comment<'a>> = None;
|
||||
for comment in comments {
|
||||
f.comments_mut().increment_printed_count();
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
previous_comment.is_some().then_some(hard_line_break()),
|
||||
comment
|
||||
]
|
||||
);
|
||||
if !f.comments_mut().mark_printed(comment) {
|
||||
continue;
|
||||
}
|
||||
let needs_line_break = previous_comment.is_some()
|
||||
&& !is_nestled_jsdoc_comment_after(previous_comment, comment);
|
||||
write!(f, [needs_line_break.then_some(hard_line_break()), comment]);
|
||||
previous_comment = Some(comment);
|
||||
}
|
||||
if matches!(indent, DanglingIndentMode::Soft)
|
||||
@@ -312,88 +321,24 @@ pub(crate) fn comments_stay_between_head_and_body(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// See [`comments_stay_between_head_and_body`]: what is between a head and its body, which starts at
|
||||
/// `body_start`: a space, or a line break if a comment starts the next line, and the comments.
|
||||
pub(crate) fn write_head_body_separator(body_start: u32, f: &mut Formatter<'_>) {
|
||||
let comments = f.comments().comments_before(body_start);
|
||||
match comments
|
||||
.first()
|
||||
.is_some_and(|comment| comment.preceded_by_newline())
|
||||
{
|
||||
true => write!(f, hard_line_break()),
|
||||
false => write!(f, space()),
|
||||
}
|
||||
FormatLeadingComments::Comments(comments).fmt(f);
|
||||
}
|
||||
|
||||
/// See [`comments_stay_between_head_and_body`]: `comments` are between a `}` and a keyword, or between
|
||||
/// what is called and the `(`. Those on the line of what is before them trail it, the others keep
|
||||
/// their lines. Returns whether what separates the two if there are no comments is still to be written.
|
||||
pub(crate) fn write_comments_between_blocks<'a>(
|
||||
comments: &'a [Comment],
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
let same_line_count = comments
|
||||
.iter()
|
||||
.take_while(|comment| !comment.preceded_by_newline())
|
||||
.count();
|
||||
let (same_line, own_line) = comments.split_at(same_line_count);
|
||||
FormatTrailingComments::Comments(same_line).fmt(f);
|
||||
if let Some(first) = own_line.first() {
|
||||
match lines_before(first, f) > 1 {
|
||||
true => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
}
|
||||
FormatLeadingComments::Comments(own_line).fmt(f);
|
||||
false
|
||||
} else if same_line.last().is_some_and(|comment| comment.is_line()) {
|
||||
write!(f, hard_line_break());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_stay_between_head_and_body`]: writes the comments after `start` that are before
|
||||
/// `character`, which the caller writes next. Returns whether a line break has to follow that, because a
|
||||
/// line comment is waiting for the end of the line.
|
||||
pub(crate) fn write_trailing_comments_before(
|
||||
start: u32,
|
||||
character: u8,
|
||||
f: &mut Formatter<'_>,
|
||||
) -> bool {
|
||||
let comments = f.comments().comments_before_character(start, character);
|
||||
let same_line_count = comments
|
||||
.iter()
|
||||
.take_while(|comment| !comment.preceded_by_newline())
|
||||
.count();
|
||||
let (same_line, own_line) = comments.split_at(same_line_count);
|
||||
FormatTrailingComments::Comments(same_line).fmt(f);
|
||||
for comment in own_line {
|
||||
match lines_before(comment, f) > 1 {
|
||||
true => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
}
|
||||
f.comments_mut().increment_printed_count();
|
||||
write!(f, [comment, comment.is_line().then_some(hard_line_break())]);
|
||||
}
|
||||
own_line.is_empty() && same_line.last().is_some_and(|comment| comment.is_line())
|
||||
}
|
||||
|
||||
/// oxfmt hands its document for a script of a Vue file to Prettier, whose printer takes away the blanks before a line
|
||||
/// break.
|
||||
fn is_printed_by_prettier_for_oxfmt(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt() && f.options().in_html.root != HtmlRoot::None
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for Comment {
|
||||
impl<'a> Format<'a> for Comment<'a> {
|
||||
/// Prettier's `printComment`.
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let content = f.source_text().text_for(&self.span);
|
||||
let content = self.text();
|
||||
if self.is_line() {
|
||||
// `-->` starts a comment at the start of a line only. oxfmt prints `var --> comment`.
|
||||
if content.starts_with(b"-->") {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
return write!(f, text(bun_core::strings::trim_js_whitespace_end(content)));
|
||||
}
|
||||
if super::jsdoc::write_comment(self, f) {
|
||||
if super::jsdoc::write_comment(*self, f) {
|
||||
return;
|
||||
}
|
||||
if self.is_indentable_block() {
|
||||
@@ -403,6 +348,9 @@ impl<'a> Format<'a> for Comment {
|
||||
&& !is_printed_by_prettier_for_oxfmt(f);
|
||||
let mut lines = bun_core::strings::split_crlf_lines(content).peekable();
|
||||
let first = lines.next().unwrap_or_default();
|
||||
// A comment that waits for the end of the line would be written at the first line break in this one. Prettier
|
||||
// prints `a; /**⏎// b⏎ * c⏎ */` for `a // b⏎; /**⏎ * c⏎ */`.
|
||||
write!(f, line_suffix_boundary());
|
||||
write!(f, text(bun_core::strings::trim_js_whitespace_end(first)));
|
||||
while let Some(line) = lines.next() {
|
||||
let trimmed = bun_core::strings::trim_js_whitespace(line);
|
||||
|
||||
@@ -6,16 +6,15 @@ use super::member_chain::is_member_call_chain;
|
||||
use super::object::{FormatKey, format_computed_or_property_key, write_member_name};
|
||||
use super::string::{FormatLiteralStringToken, StringLiteralParentKind};
|
||||
use super::suppressed::FormatSuppressedNode;
|
||||
use super::typecast::is_cast_target;
|
||||
use super::typecast::{
|
||||
comments_of_outermost_node, has_comments, has_leading_own_line_comment, is_cast_target,
|
||||
};
|
||||
use crate::js::format::{ExprOptions, FormatExpr, FormatTypeAnnotation};
|
||||
use crate::js::parentheses::expression::expression_needs_parentheses;
|
||||
use crate::js::print::arrow_function_expression::FormatJsArrowFunctionExpressionOptions;
|
||||
use crate::js::print::binary_like_expression::BinaryLikeExpression;
|
||||
use crate::js::print::decorators::FormatDecorators;
|
||||
use crate::js::print::expressions::unary_argument_has_comments;
|
||||
use crate::js::print::jsx::ignored_jsx_gets_no_parentheses;
|
||||
use crate::js::print::patterns::FormatBindingPropertyValue;
|
||||
use crate::js::print::sequence_expression::write_comments_before_closing_parenthesis;
|
||||
use crate::js::print::type_parameters::type_arguments;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
@@ -50,43 +49,6 @@ pub(crate) enum AssignmentLikeLayout {
|
||||
ChainTailArrowFunction,
|
||||
}
|
||||
|
||||
/// The comments between the left side, which is written and ends at `start`, and `right` that trail
|
||||
/// the left side.
|
||||
fn format_left_trailing_comments<'a>(start: u32, right: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
match comments_stay_around_operator(f) {
|
||||
true => write_comments_before_operator(start, f),
|
||||
false => FormatTrailingComments::Comments(
|
||||
f.comments().comments_trailing_left_side(right.span().start),
|
||||
)
|
||||
.fmt(f),
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_stay_around_operator`]: the comments after the left side, which ends at `start`, that
|
||||
/// trail it. Those behind the operator are hidden.
|
||||
pub(crate) fn write_comments_before_operator(start: u32, f: &mut Formatter<'_>) {
|
||||
let end_of_line_comments = f.comments().end_of_line_comments_after(start);
|
||||
let comments = if end_of_line_comments.is_empty() {
|
||||
let comments = f.comments().comments_before_character(start, b'=');
|
||||
if comments.iter().any(|comment| comment.preceded_by_newline()) {
|
||||
&[]
|
||||
} else {
|
||||
comments
|
||||
}
|
||||
} else if end_of_line_comments
|
||||
.last()
|
||||
.is_some_and(|comment| comment.is_multiline_block())
|
||||
{
|
||||
&[]
|
||||
} else {
|
||||
end_of_line_comments
|
||||
};
|
||||
FormatTrailingComments::Comments(comments).fmt(f);
|
||||
}
|
||||
|
||||
/// For oxfmt a comment stays on the side of the `=` or the `:` that it is on, and a line comment on
|
||||
/// the line of the operator stays there, with what follows on the next line. Prettier attaches it to
|
||||
/// one of the two sides, and writes it where the line ends that this side ends on.
|
||||
@@ -95,24 +57,12 @@ pub(crate) fn write_comments_before_operator(start: u32, f: &mut Formatter<'_>)
|
||||
/// const a = // comment const a = 1; // comment
|
||||
/// 1;
|
||||
/// ```
|
||||
pub(crate) fn comments_stay_around_operator(f: &Formatter<'_>) -> bool {
|
||||
///
|
||||
/// The comments that stay right behind the operator are attached to the assignment as dangling comments.
|
||||
fn comments_stay_around_operator(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// The comments behind the operator, which ends at `operator_end`, up to a line comment that ends its
|
||||
/// line. None if a comment from before the operator is left: then all lead the right side.
|
||||
pub(crate) fn operator_line_run<'a>(operator_end: u32, f: &Formatter<'a>) -> &'a [Comment] {
|
||||
if f.comments().has_comment_before(operator_end) {
|
||||
return &[];
|
||||
}
|
||||
let run = f.comments().end_of_line_comments_after(operator_end);
|
||||
if run.last().is_some_and(|comment| comment.is_line()) {
|
||||
run
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
/// A name is short if it is less than this much wider than the indentation: breaking after the
|
||||
/// operator would gain next to nothing.
|
||||
const MIN_OVERLAP_FOR_BREAK: u32 = 3;
|
||||
@@ -129,16 +79,10 @@ fn computed_key_width<'a>(key: Key<'a>, f: &Formatter<'a>) -> Option<usize> {
|
||||
let span = key.span(f.file());
|
||||
if let KeyKind::Computed(e) = key.kind() {
|
||||
// A comment in the brackets is a piece of its own.
|
||||
let inner = e.outer_span();
|
||||
let is_blank = |start: u32, end: u32| {
|
||||
(f.source_text().text_for(&Span::new(start.min(end), end)))
|
||||
.trim_ascii()
|
||||
.is_empty()
|
||||
return match !f.is_quiet() && has_comments(e, f) {
|
||||
true => None,
|
||||
false => plain_expression_width(e, f).map(|width| width + 2),
|
||||
};
|
||||
return (is_blank(span.start + 1, inner.start)
|
||||
&& is_blank(inner.end, span.end.saturating_sub(1)))
|
||||
.then(|| plain_expression_width(e, f))?
|
||||
.map(|width| width + 2);
|
||||
}
|
||||
// A template is written with a `lineSuffixBoundary`.
|
||||
let is_template = f
|
||||
@@ -149,6 +93,16 @@ fn computed_key_width<'a>(key: Key<'a>, f: &Formatter<'a>) -> Option<usize> {
|
||||
(!is_template).then(|| f.source_text().span_width(span))
|
||||
}
|
||||
|
||||
/// `{ a /* comment */: b }`: for Prettier a name with a comment is more than one piece of text, so it is not short. oxfmt only
|
||||
/// looks at the name.
|
||||
fn comments_make_name_long<'a>(key: Key<'a>, f: &Formatter<'a>) -> bool {
|
||||
if f.is_quiet() || f.options().flavor.is_oxfmt() {
|
||||
return false;
|
||||
}
|
||||
let comments = f.comments().of_name(key.inner_span(f.file()).start);
|
||||
!comments.leading.is_empty() || !comments.trailing.is_empty()
|
||||
}
|
||||
|
||||
/// See [`computed_key_width`]: `a`, `1`, `-1`, `!a`, `typeof a`, `a++`, `a!`, `await a`, `a as T`,
|
||||
/// `a<T>`. Not `a.b`, `a()`, `` `a` ``, `[]`.
|
||||
fn plain_expression_width<'a>(e: Expr<'a>, f: &Formatter<'a>) -> Option<usize> {
|
||||
@@ -173,9 +127,7 @@ fn plain_expression_width<'a>(e: Expr<'a>, f: &Formatter<'a>) -> Option<usize> {
|
||||
ExprKind::ImportMeta => return Some(width + "import.meta".len()),
|
||||
ExprKind::NewTarget => return Some(width + "new.target".len()),
|
||||
// An operand with comments is in a group.
|
||||
ExprKind::Unary { operand, .. } if unary_argument_has_comments(current, operand, f) => {
|
||||
return None;
|
||||
}
|
||||
ExprKind::Unary { operand, .. } if has_comments(operand, f) => return None,
|
||||
ExprKind::Unary { op, operand } => {
|
||||
width += op.as_str().len() + usize::from(op.is_keyword());
|
||||
operand
|
||||
@@ -311,6 +263,24 @@ fn write_property_definition_left<'a>(member: Member<'a>, f: &mut Formatter<'a>)
|
||||
);
|
||||
}
|
||||
|
||||
/// `let a /* comment */⏎: T`: for oxfmt the comment trails `a`, and stays before the type annotation. For Prettier it leads the
|
||||
/// type annotation.
|
||||
fn comments_between_name_and_type<'a>(
|
||||
declarator: VarDecl<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> CommentList<'a> {
|
||||
let Some(ty) = declarator.ty().filter(|_| !f.is_quiet()) else {
|
||||
return CommentList::empty();
|
||||
};
|
||||
let comments = (f.comments())
|
||||
.of_node(declarator.pat().as_ast_nodes())
|
||||
.trailing;
|
||||
let count = (comments.iter())
|
||||
.take_while(|comment| comment.span().start < ty.span().start)
|
||||
.count();
|
||||
comments.slice(0, count)
|
||||
}
|
||||
|
||||
/// A left side that is a name without comments. See [`AssignmentLike::name_on_the_left`].
|
||||
#[derive(Copy, Clone)]
|
||||
enum NameOnTheLeft<'a> {
|
||||
@@ -381,38 +351,38 @@ impl<'a> AssignmentLike<'a> {
|
||||
AssignmentLike::VariableDeclarator(declarator) => {
|
||||
let (id, ty) = (declarator.pat(), declarator.ty());
|
||||
let definite = declarator.is_definite().then_some("!");
|
||||
let Some(init) = declarator.init() else {
|
||||
write!(f, [id, definite, ty.map(FormatTypeAnnotation)]);
|
||||
return false;
|
||||
};
|
||||
// For Prettier the type annotation is part of the name. So it is copied along with an ignored name, and the
|
||||
// trailing comments of the name come after it.
|
||||
match ty {
|
||||
// `a: T = // prettier-ignore`: the comment trails the name, which the type is part of.
|
||||
Some(ty)
|
||||
if !f.is_quiet()
|
||||
&& f.comments().has_trailing_suppression_comment(ty.span().end) =>
|
||||
{
|
||||
Some(ty) if !f.is_quiet() && f.comments().is_ignored(id.as_ast_nodes()) => {
|
||||
let span = Span::new(id.span().start, ty.span().end);
|
||||
write!(
|
||||
f,
|
||||
[format_leading_comments(span), FormatSuppressedNode(span)]
|
||||
[
|
||||
leading_comments(id.as_ast_nodes()),
|
||||
FormatSuppressedNode(span)
|
||||
]
|
||||
);
|
||||
}
|
||||
Some(ty) => {
|
||||
let type_annotation = WithSpan(FormatTypeAnnotation(ty), ty.span());
|
||||
write!(f, [FormatNodeWithoutTrailingComments(&id), definite]);
|
||||
write!(f, FormatNodeWithoutTrailingComments(&type_annotation));
|
||||
}
|
||||
None => write!(f, [FormatNodeWithoutTrailingComments(&id), definite]),
|
||||
_ => write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&id),
|
||||
FormatTrailingComments::Comments(comments_between_name_and_type(
|
||||
declarator, f
|
||||
)),
|
||||
definite,
|
||||
ty.map(FormatTypeAnnotation)
|
||||
]
|
||||
),
|
||||
}
|
||||
if !f.is_quiet() {
|
||||
write!(f, trailing_comments(id.as_ast_nodes()));
|
||||
}
|
||||
format_left_trailing_comments(id.span().end, init, f);
|
||||
false
|
||||
}
|
||||
AssignmentLike::AssignmentExpression(assignment) => {
|
||||
let (Some(target), Some(value)) = (assignment.left(), assignment.right()) else {
|
||||
return false;
|
||||
};
|
||||
write!(f, FormatNodeWithoutTrailingComments(&target));
|
||||
format_left_trailing_comments(target.span().end, value, f);
|
||||
write!(f, assignment.left());
|
||||
false
|
||||
}
|
||||
AssignmentLike::ObjectProperty(property) => match property.key() {
|
||||
@@ -426,6 +396,7 @@ impl<'a> AssignmentLike<'a> {
|
||||
}
|
||||
Some(key) => {
|
||||
write_member_name(key, || property.as_ast_nodes(), f) < text_width_for_break
|
||||
&& !comments_make_name_long(key, f)
|
||||
}
|
||||
None => false,
|
||||
},
|
||||
@@ -440,7 +411,10 @@ impl<'a> AssignmentLike<'a> {
|
||||
write!(f, ["[", FormatKey::new(key, node), "]"]);
|
||||
computed_key_width(key, f).is_some_and(|width| width < text_width_for_break)
|
||||
}
|
||||
Some(key) => write_member_name(key, || node, f) < text_width_for_break,
|
||||
Some(key) => {
|
||||
write_member_name(key, || node, f) < text_width_for_break
|
||||
&& !comments_make_name_long(key, f)
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -485,8 +459,13 @@ impl<'a> AssignmentLike<'a> {
|
||||
&& right.tag() == ExprTag::Cond
|
||||
&& layout != AssignmentLikeLayout::BreakAfterOperator
|
||||
&& !matches!(self, Self::AccessorProperty(_))
|
||||
&& !is_cast_target(right, f) =>
|
||||
&& !is_cast_target(right, f)
|
||||
&& (f.is_quiet() || !f.comments().is_ignored(right.as_ast_nodes())) =>
|
||||
{
|
||||
// The conditional writes the indentation itself, so its comments are outside of it.
|
||||
if !f.is_quiet() {
|
||||
write!(f, leading_comments(right.as_ast_nodes()));
|
||||
}
|
||||
match super::experimental_ternary::should_break(right, f) {
|
||||
true => write!(f, indent(&right)),
|
||||
false => {
|
||||
@@ -518,9 +497,6 @@ impl<'a> AssignmentLike<'a> {
|
||||
Some(right) => write!(f, right),
|
||||
None => {}
|
||||
}
|
||||
if let Self::AssignmentExpression(assignment) = *self {
|
||||
write_comments_before_closing_parenthesis(assignment, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,7 +513,6 @@ impl<'a> AssignmentLike<'a> {
|
||||
) -> AssignmentLikeLayout {
|
||||
// For Prettier the right side is a `ParenthesizedExpression` then, whatever is in it.
|
||||
let is_type_cast = right_expression.is_some_and(|e| is_cast_target(e, f));
|
||||
let mut starts_with_type_cast = false;
|
||||
if let Some(e) = right_expression {
|
||||
if let Some(layout) = self.chain_formatting_layout(e, is_type_cast, f) {
|
||||
return layout;
|
||||
@@ -552,10 +527,10 @@ impl<'a> AssignmentLike<'a> {
|
||||
{
|
||||
return AssignmentLikeLayout::BreakAfterOperator;
|
||||
}
|
||||
match leading_comments_of_right_side(e, f) {
|
||||
LeadingComments::None | LeadingComments::TypeCast => {}
|
||||
LeadingComments::Break => return AssignmentLikeLayout::BreakAfterOperator,
|
||||
LeadingComments::TypeCastOfLeftEdge => starts_with_type_cast = true,
|
||||
if has_leading_comment_that_breaks_after_operator(e, f)
|
||||
|| !self.comments_behind_operator(f).is_empty()
|
||||
{
|
||||
return AssignmentLikeLayout::BreakAfterOperator;
|
||||
}
|
||||
if e.tag() == ExprTag::Call
|
||||
&& !is_type_cast
|
||||
@@ -570,21 +545,24 @@ impl<'a> AssignmentLike<'a> {
|
||||
// The right side of a property of a pattern is not an expression.
|
||||
if let Self::BindingProperty(property) = *self
|
||||
&& !f.is_quiet()
|
||||
&& (f
|
||||
.comments()
|
||||
.comments_before_iter(property.value().span().start))
|
||||
.any(|comment| comment.followed_by_newline() || comment.is_indentable_block())
|
||||
{
|
||||
return AssignmentLikeLayout::BreakAfterOperator;
|
||||
let value = match property.default() {
|
||||
Some(_) => AstNodes::AssignmentPattern(Node::PatProp(property)),
|
||||
None => property.value().as_ast_nodes(),
|
||||
};
|
||||
if (f.comments().of_node(value).leading.iter())
|
||||
.any(|comment| comment.has_newline_after() || comment.is_indentable_block())
|
||||
{
|
||||
return AssignmentLikeLayout::BreakAfterOperator;
|
||||
}
|
||||
}
|
||||
|
||||
if self.should_break_left_hand_side(left_may_break && !is_type_cast) {
|
||||
return AssignmentLikeLayout::BreakLeftHandSide;
|
||||
}
|
||||
if !is_type_cast
|
||||
&& right_expression.is_some_and(|right| {
|
||||
should_break_after_operator(right, is_left_short, starts_with_type_cast, f)
|
||||
})
|
||||
&& right_expression
|
||||
.is_some_and(|right| should_break_after_operator(right, is_left_short, f))
|
||||
{
|
||||
return AssignmentLikeLayout::BreakAfterOperator;
|
||||
}
|
||||
@@ -749,71 +727,34 @@ impl<T> Spanned for WithSpan<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the comments before the right side of an assignment mean for its layout.
|
||||
enum LeadingComments {
|
||||
None,
|
||||
/// One ends its line, or is a block whose lines all start with `*`: the right side starts on
|
||||
/// its own line.
|
||||
Break,
|
||||
/// `/** @type {T} */ (e)`: the right side is in parentheses.
|
||||
TypeCast,
|
||||
/// `/** @type {T} */ (a).b`, `/** @type {T} */ (a) || b`: what the right side starts with is.
|
||||
TypeCastOfLeftEdge,
|
||||
}
|
||||
|
||||
/// `a = /** @type {T} */ (⏎/**⏎ * comment⏎ */⏎b)`: oxfmt looks at all comments before `b`.
|
||||
fn block_comment_in_parentheses_of_cast_breaks(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// `a =⏎// comment⏎b?.c` is `a = // comment⏎b?.c` in TypeScript: typescript-estree has a `ChainExpression` around
|
||||
/// `b?.c`, which Prettier attaches no comment to. The comment leads what is in it, and the right side has none.
|
||||
fn comments_lead_what_is_in_chain_expression<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
e.is_chain_root() && !f.context().has_tree_of_babel() && !f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
fn leading_comments_of_right_side<'a>(right: Expr<'a>, f: &Formatter<'a>) -> LeadingComments {
|
||||
if f.is_quiet() || comments_lead_what_is_in_chain_expression(right, f) {
|
||||
return LeadingComments::None;
|
||||
/// Whether a leading comment of `right`, the right side of an assignment, ends its line, or is a block comment whose lines
|
||||
/// all start with `*`. The right side starts on a line of its own then.
|
||||
fn has_leading_comment_that_breaks_after_operator<'a>(right: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
let start = right.span().start;
|
||||
if right.tag() == ExprTag::Jsx {
|
||||
return match f.comments().is_suppressed(start) && ignored_jsx_gets_no_parentheses(f) {
|
||||
true => LeadingComments::Break,
|
||||
false => LeadingComments::None,
|
||||
};
|
||||
}
|
||||
if block_comment_in_parentheses_of_cast_breaks(f)
|
||||
&& (f.comments().comments_before_iter(start)).any(|comment| comment.is_indentable_block())
|
||||
{
|
||||
return LeadingComments::Break;
|
||||
}
|
||||
for comment in f.comments().comments_before_iter(start) {
|
||||
if comment.leads_left_edge_of(right.span()) {
|
||||
continue;
|
||||
}
|
||||
if comment.followed_by_newline() || comment.is_indentable_block() {
|
||||
return LeadingComments::Break;
|
||||
}
|
||||
// `/** @type {T} */ a.b()` casts nothing.
|
||||
if f.comments().is_type_cast_comment(comment)
|
||||
&& f.source_text()
|
||||
.next_non_whitespace_byte_is(comment.end(), b'(')
|
||||
{
|
||||
return match right.is_parenthesized() {
|
||||
true => LeadingComments::TypeCast,
|
||||
false => LeadingComments::TypeCastOfLeftEdge,
|
||||
};
|
||||
}
|
||||
}
|
||||
LeadingComments::None
|
||||
has_leading_own_line_comment(right, f)
|
||||
|| (comments_of_outermost_node(right, f).leading.iter())
|
||||
.any(|comment| comment.is_indentable_block())
|
||||
|| (block_comment_in_parentheses_of_cast_breaks(f)
|
||||
&& is_cast_target(right, f)
|
||||
&& (f
|
||||
.comments()
|
||||
.of_node(right.as_chain_element())
|
||||
.leading
|
||||
.iter())
|
||||
.any(|comment| comment.is_indentable_block()))
|
||||
}
|
||||
|
||||
/// Prettier's `shouldBreakAfterOperator`.
|
||||
fn should_break_after_operator<'a>(
|
||||
right: Expr<'a>,
|
||||
is_left_short: bool,
|
||||
starts_with_type_cast: bool,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
// `None`: it is neither a binary nor a logical expression.
|
||||
@@ -830,12 +771,7 @@ fn should_break_after_operator<'a>(
|
||||
matches!(right.kind(), ExprKind::Cond { yes, no, .. } if yes.tag() == ExprTag::Cond || no.tag() == ExprTag::Cond)
|
||||
}
|
||||
// `/** @type {T} */ (a || b) ? c : d`: the test is in parentheses.
|
||||
ExprTag::Cond
|
||||
if starts_with_type_cast
|
||||
&& right.test().is_some_and(|test| is_cast_target(test, f)) =>
|
||||
{
|
||||
false
|
||||
}
|
||||
ExprTag::Cond if right.test().is_some_and(|test| is_cast_target(test, f)) => false,
|
||||
ExprTag::Cond => right
|
||||
.test()
|
||||
.and_then(breaks_as_binary_expression)
|
||||
@@ -847,8 +783,7 @@ fn should_break_after_operator<'a>(
|
||||
_ => {
|
||||
let inner_expression = get_innermost_expression(right, f);
|
||||
(inner_expression.tag() == ExprTag::String && !is_cast_target(inner_expression, f))
|
||||
|| (!starts_with_type_cast
|
||||
&& is_poorly_breakable_member_or_call_chain(inner_expression, f))
|
||||
|| is_poorly_breakable_member_or_call_chain(inner_expression, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,30 +832,18 @@ impl<'a> Format<'a> for AssignmentLike<'a> {
|
||||
// layout, which depends on what is written for the left side.
|
||||
let outer_group = f.reserve_tag();
|
||||
let left_group = f.reserve_tag();
|
||||
// See `comments_stay_around_operator`. Where the left side ends, the right side starts and the
|
||||
// operator ends.
|
||||
let sides = match !f.is_quiet() && comments_stay_around_operator(f) {
|
||||
true => self.sides_with_comments_between(right_expression, f),
|
||||
false => None,
|
||||
};
|
||||
let previous_limit =
|
||||
sides.map(|(_, _, operator_end)| f.comments_mut().limit_comments_up_to(operator_end));
|
||||
let is_left_short = self.write_left(f);
|
||||
if let Some(previous_limit) = previous_limit {
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
}
|
||||
let operator_line_run = sides.map_or(&[][..], |(_, _, operator_end)| {
|
||||
operator_line_run(operator_end, f)
|
||||
});
|
||||
let has_line_comment_on_operator_line = !operator_line_run.is_empty()
|
||||
|| sides.is_some_and(|(left_end, ..)| {
|
||||
f.comments().has_printed_line_comment_after(left_end)
|
||||
});
|
||||
let left = f.elements().get(left_group + 1..).unwrap_or_default();
|
||||
let is_left_text = left.len() <= 12 && left.iter().all(is_text_on_one_line);
|
||||
let left_may_break = !is_left_text
|
||||
&& f.elements_from(left_group + 1)
|
||||
.may_directly_break(f.options().flavor);
|
||||
let comments_behind_operator = self.comments_behind_operator(f);
|
||||
let has_line_comment_on_operator_line = !comments_behind_operator.is_empty()
|
||||
|| (!f.is_quiet()
|
||||
&& comments_stay_around_operator(f)
|
||||
&& f.comments()
|
||||
.has_printed_line_comment_after(self.left_end(f)));
|
||||
let layout = match self.layout(right_expression, is_left_short, left_may_break, f) {
|
||||
AssignmentLikeLayout::Fluid | AssignmentLikeLayout::NeverBreakAfterOperator
|
||||
if has_line_comment_on_operator_line
|
||||
@@ -938,17 +861,10 @@ impl<'a> Format<'a> for AssignmentLike<'a> {
|
||||
}
|
||||
|
||||
self.write_operator(f);
|
||||
if let Some((_, right_start, _)) = sides
|
||||
&& !operator_line_run.is_empty()
|
||||
{
|
||||
write!(f, FormatTrailingComments::Comments(operator_line_run));
|
||||
if operator_line_run
|
||||
.iter()
|
||||
.any(|comment| f.comments().is_suppression_comment(comment))
|
||||
{
|
||||
f.comments_mut().mark_suppressed_after_operator(right_start);
|
||||
}
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments_behind_operator)
|
||||
);
|
||||
// Without a group of its own the line break follows the group around it all, which the line
|
||||
// comment breaks.
|
||||
let follows_line_comment =
|
||||
@@ -975,53 +891,42 @@ fn is_require_call(e: Expr<'_>) -> bool {
|
||||
}
|
||||
|
||||
impl<'a> AssignmentLike<'a> {
|
||||
/// Where the left side ends, where the right side starts and where the operator between them ends,
|
||||
/// if there is a comment between the two sides.
|
||||
///
|
||||
/// `right_expression`: [`AssignmentLike::get_right_expression`].
|
||||
fn sides_with_comments_between(
|
||||
&self,
|
||||
right_expression: Option<Expr<'a>>,
|
||||
f: &Formatter<'a>,
|
||||
) -> Option<(u32, u32, u32)> {
|
||||
let right_start = right_expression.map(|it| it.span().start);
|
||||
let (left_end, right_start, operator) = match *self {
|
||||
Self::VariableDeclarator(declarator) => (
|
||||
declarator
|
||||
.ty()
|
||||
.map_or_else(|| declarator.pat().span().end, |ty| ty.span().end),
|
||||
right_start?,
|
||||
b'=',
|
||||
),
|
||||
Self::AssignmentExpression(assignment) => {
|
||||
(assignment.left()?.span().end, right_start?, b'=')
|
||||
}
|
||||
Self::ObjectProperty(property) => {
|
||||
(property.key()?.span(f.file()).end, right_start?, b':')
|
||||
}
|
||||
Self::BindingProperty(property) => (
|
||||
property.key()?.span(f.file()).end,
|
||||
property.value().span().start,
|
||||
b':',
|
||||
),
|
||||
fn node(&self) -> AstNodes<'a> {
|
||||
match *self {
|
||||
Self::VariableDeclarator(declarator) => AstNodes::VariableDeclarator(declarator),
|
||||
Self::AssignmentExpression(assignment) => assignment.as_ast_nodes(),
|
||||
Self::ObjectProperty(property) => property.as_ast_nodes(),
|
||||
Self::BindingProperty(property) => AstNodes::BindingProperty(property),
|
||||
Self::PropertyDefinition(member) | Self::AccessorProperty(member) => {
|
||||
let key_end = member.key().map(|key| key.span(f.file()).end);
|
||||
(
|
||||
member.ty().map(|ty| ty.span().end).or(key_end)?,
|
||||
right_start?,
|
||||
b'=',
|
||||
)
|
||||
member.as_ast_nodes()
|
||||
}
|
||||
};
|
||||
f.comments()
|
||||
.has_any_comment_in(Span::new(left_end, right_start))
|
||||
.then(|| {
|
||||
(
|
||||
left_end,
|
||||
right_start,
|
||||
f.comments().position_after_character(left_end, operator),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_stay_around_operator`].
|
||||
fn comments_behind_operator(&self, f: &Formatter<'a>) -> CommentList<'a> {
|
||||
match !f.is_quiet() && comments_stay_around_operator(f) {
|
||||
true => f.comments().of_node(self.node()).dangling,
|
||||
false => CommentList::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the left side ends.
|
||||
fn left_end(&self, f: &Formatter<'a>) -> u32 {
|
||||
let key_end = |key: Option<Key<'a>>| key.map_or(0, |key| key.span(f.file()).end);
|
||||
match *self {
|
||||
Self::VariableDeclarator(declarator) => declarator
|
||||
.ty()
|
||||
.map_or_else(|| declarator.pat().span().end, |ty| ty.span().end),
|
||||
Self::AssignmentExpression(assignment) => {
|
||||
assignment.left().map_or(0, |left| left.span().end)
|
||||
}
|
||||
Self::ObjectProperty(property) => key_end(property.key()),
|
||||
Self::BindingProperty(property) => key_end(property.key()),
|
||||
Self::PropertyDefinition(member) | Self::AccessorProperty(member) => member
|
||||
.ty()
|
||||
.map_or_else(|| key_end(member.key()), |ty| ty.span().end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1121,6 +1026,10 @@ fn is_poorly_breakable_member_or_call_chain<'a>(
|
||||
let mut current = expression;
|
||||
|
||||
loop {
|
||||
// For Prettier it is a `ParenthesizedExpression`.
|
||||
if is_cast_target(current, f) {
|
||||
return false;
|
||||
}
|
||||
let next = match current.tag() {
|
||||
ExprTag::NonNull => current.operand(),
|
||||
ExprTag::Call => {
|
||||
@@ -1164,12 +1073,7 @@ fn is_poorly_breakable_member_or_call_chain<'a>(
|
||||
let Some(call) = call_expression.as_call_like() else {
|
||||
continue;
|
||||
};
|
||||
let has_comment_around_argument = !call.args().is_empty()
|
||||
&& f.comments()
|
||||
.has_comment_in_range(call.callee().span().end, call_expression.span().end);
|
||||
if has_comment_around_argument
|
||||
|| is_complex_type_arguments(call_expression, call.type_args(), f)
|
||||
{
|
||||
if is_complex_type_arguments(call_expression, call.type_args(), f) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1212,11 +1116,17 @@ pub(crate) fn is_short_argument<'a>(
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
let threshold = threshold as usize;
|
||||
let has_comments = |e: Expr<'a>| !f.is_quiet() && has_comments(e, f);
|
||||
if has_comments(argument) {
|
||||
return false;
|
||||
}
|
||||
let mut node = argument.as_ast_nodes();
|
||||
while let AstNodes::UnaryExpression(_) = node {
|
||||
match argument.argument() {
|
||||
Some(operand) => (argument, node) = (operand, operand.as_ast_nodes()),
|
||||
None => return false,
|
||||
Some(operand) if !has_comments(operand) => {
|
||||
(argument, node) = (operand, operand.as_ast_nodes());
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
// Prettier asks for the `length` of a string of JavaScript, oxfmt for that of one of Rust.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Calls that are written in a special way because of what they call: `it("..", () => {})`.
|
||||
|
||||
use crate::js::trivia::{comments_stay_between_head_and_body, write_comments_between_blocks};
|
||||
use super::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use super::typecast::comments_of_expression;
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
|
||||
/// Prettier's `isCallExpression`.
|
||||
///
|
||||
@@ -33,98 +35,84 @@ pub(crate) fn strip_chain_element_wrappers(mut e: Expr<'_>) -> Expr<'_> {
|
||||
e
|
||||
}
|
||||
|
||||
/// Of the comments between the callee of `call`, which ends at `callee_end` and is written, and the `<` or the `(`,
|
||||
/// those that trail the callee. `call` has a `(`.
|
||||
pub(crate) fn callee_trailing_comments<'a>(
|
||||
call: Call<'a>,
|
||||
callee_end: u32,
|
||||
/// oxfmt leaves the comments between a callee and the `(`, the `?.` or the `<` after it where they are. Prettier attaches most
|
||||
/// of them to the first argument.
|
||||
///
|
||||
/// ```js
|
||||
/// a // comment a(
|
||||
/// (b); // comment
|
||||
/// b,
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// Not after `new A` without type arguments, not after `a[b]`, not between `a.b` and `?.`, and not after a call that is a link
|
||||
/// of a member chain.
|
||||
///
|
||||
/// `is_in_member_chain`: whether `callee` is written as a part of a member chain.
|
||||
pub(crate) fn comments_stay_behind_callee<'a>(
|
||||
callee: Expr<'a>,
|
||||
is_in_member_chain: bool,
|
||||
f: &Formatter<'a>,
|
||||
) -> &'a [Comment] {
|
||||
if let Some(comments) = comments_trailing_computed_callee(call, f) {
|
||||
return comments;
|
||||
}
|
||||
if comments_stay_between_head_and_body(f) {
|
||||
let opener = match (call.is_optional(), call.type_args().is_empty()) {
|
||||
(true, _) => b'?',
|
||||
(false, false) => b'<',
|
||||
(false, true) => b'(',
|
||||
};
|
||||
return f.comments().comments_before_character(callee_end, opener);
|
||||
}
|
||||
f.comments().comments_trailing_link(callee_end)
|
||||
) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
&& comments_stay_after_callee_for_oxfmt(callee, |_| is_in_member_chain)
|
||||
}
|
||||
|
||||
/// `a["b"]( // comment⏎c)` is `a["b"](c); // comment` for oxfmt: to a callee like `a[b]` the `(` is nothing, and what is
|
||||
/// between it and the first argument is its own or the argument's as between any two nodes.
|
||||
fn comments_trailing_computed_callee<'a>(
|
||||
call: Call<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> Option<&'a [Comment]> {
|
||||
if !f.options().flavor.is_oxfmt() {
|
||||
return None;
|
||||
}
|
||||
let callee = call.callee();
|
||||
let (ExprKind::Index { .. }, AstNodes::CallExpression(parent)) =
|
||||
(callee.kind(), callee.ast_parent())
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let following = (call.type_args().angle_brackets_span())
|
||||
.or_else(|| call.args().first().map(|first| first.span()))?;
|
||||
Some(
|
||||
f.comments()
|
||||
.get_trailing_comments(parent.span(), callee.span(), following.start),
|
||||
)
|
||||
/// See [`comments_stay_behind_callee`]. The attachment of comments asks this too, and finds out from the tree whether a call
|
||||
/// is a link of a member chain.
|
||||
pub(crate) fn comments_stay_after_callee_for_oxfmt<'a>(
|
||||
callee: Expr<'a>,
|
||||
is_link_of_member_chain: impl FnOnce(Expr<'a>) -> bool,
|
||||
) -> bool {
|
||||
matches!(callee.parent(), Node::Expr(parent) if match parent.kind() {
|
||||
ExprKind::Call(call) => call.callee() == callee && match callee.tag() {
|
||||
ExprTag::Index => false,
|
||||
ExprTag::Dot => !call.is_optional(),
|
||||
ExprTag::Call => !is_link_of_member_chain(parent),
|
||||
_ => true,
|
||||
},
|
||||
ExprKind::New(call) => call.callee() == callee && !call.type_args().is_empty(),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes [`callee_trailing_comments`].
|
||||
pub(crate) fn write_callee_trailing_comments<'a>(
|
||||
call: Call<'a>,
|
||||
callee_end: u32,
|
||||
f: &mut Formatter<'a>,
|
||||
) {
|
||||
if let Some(comments) = comments_trailing_computed_callee(call, f) {
|
||||
return FormatTrailingComments::Comments(comments).fmt(f);
|
||||
}
|
||||
let comments = callee_trailing_comments(call, callee_end, f);
|
||||
match comments_stay_between_head_and_body(f) {
|
||||
true => {
|
||||
write_comments_between_blocks(comments, f);
|
||||
/// The callee of a call or a `new`, with its comments.
|
||||
pub(crate) struct FormatCallee<'a> {
|
||||
pub(crate) callee: Expr<'a>,
|
||||
pub(crate) is_in_member_chain: bool,
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatCallee<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let callee = self.callee;
|
||||
if f.is_quiet() || !comments_stay_behind_callee(callee, self.is_in_member_chain, f) {
|
||||
return write!(f, callee);
|
||||
}
|
||||
false => FormatTrailingComments::Comments(comments).fmt(f),
|
||||
write!(f, FormatNodeWithoutTrailingComments(&callee));
|
||||
write_comments_behind_callee(comments_of_expression(callee, f).trailing, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments that trail the type arguments of `call`, which end at `end`. One behind the `(` at
|
||||
/// the end of the line is among them: Prettier's `handleCallExpressionComments` only gives it to
|
||||
/// the first argument if what is before it is the callee.
|
||||
pub(crate) fn type_arguments_trailing_comments<'a>(
|
||||
call: Call<'a>,
|
||||
end: u32,
|
||||
f: &Formatter<'a>,
|
||||
) -> &'a [Comment] {
|
||||
match call.args().first() {
|
||||
Some(first) => {
|
||||
trailing_prefix(f.comments().comments_in(Span::before(end, first.span())), f)
|
||||
/// See [`comments_stay_behind_callee`]. The comments on the line of the callee trail it. The others keep their lines.
|
||||
pub(crate) fn write_comments_behind_callee<'a>(comments: CommentList<'a>, f: &mut Formatter<'a>) {
|
||||
let same_line_count = (comments.iter())
|
||||
.take_while(|comment| !comment.has_newline_before())
|
||||
.count();
|
||||
let (same_line, own_line) = comments.split_at(same_line_count);
|
||||
FormatTrailingComments::Comments(same_line).fmt(f);
|
||||
if let Some(first) = own_line.first() {
|
||||
let previous_end =
|
||||
(same_line.last()).map_or_else(|| first.prev_token_end(), |comment| comment.span().end);
|
||||
match f.source_text().lines_after(previous_end) > 1 {
|
||||
true => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
}
|
||||
None => f.comments().comments_before_character(end, b'('),
|
||||
FormatLeadingComments::Comments(own_line).fmt(f);
|
||||
} else if same_line.last().is_some_and(|comment| comment.is_line()) {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
}
|
||||
|
||||
fn trailing_prefix<'a>(comments: &'a [Comment], f: &Formatter<'a>) -> &'a [Comment] {
|
||||
let source_text = f.source_text();
|
||||
let count = comments
|
||||
.iter()
|
||||
.rposition(|comment| {
|
||||
!comment.preceded_by_newline()
|
||||
&& (comment.followed_by_newline()
|
||||
|| source_text.next_non_whitespace_byte_is(comment.span.end, b'?')
|
||||
|| source_text.next_non_whitespace_byte_is(comment.span.end, b')'))
|
||||
})
|
||||
.map_or(0, |index| index + 1);
|
||||
comments.get(..count).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Prettier's `isTestCall`. `e` is a call.
|
||||
///
|
||||
/// - `it("name", () => {})`, `test.only("name", async function () {}, 1000)`: the callee is one of
|
||||
@@ -156,14 +144,14 @@ fn has_comments_around_arguments<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let mut start = call.callee().span().end;
|
||||
for argument in call.args() {
|
||||
if f.comments()
|
||||
.has_any_comment_in(Span::before(start, argument.span()))
|
||||
.has_comment_in_span(Span::before(start, argument.span()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
start = argument.span().end;
|
||||
}
|
||||
f.comments()
|
||||
.has_any_comment_in(Span::new(start, e.span().end))
|
||||
.has_comment_in_span(Span::new(start, e.span().end))
|
||||
}
|
||||
|
||||
/// oxfmt has a longer list than Prettier: `it.todo("name", () => {})`, `Deno.test("name", () => {})`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! `a ? b : c` and `A extends B ? C : D`. Prettier's `printTernary`.
|
||||
|
||||
use super::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use super::typecast::is_cast_target_that_hides_its_kind;
|
||||
use crate::js::format::format_node;
|
||||
use crate::js::print::binary_like_expression::is_angular_pipe;
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
@@ -47,13 +47,6 @@ impl Operand<'_> {
|
||||
}
|
||||
|
||||
impl<'a> ConditionalLike<'a> {
|
||||
fn span(&self) -> Span {
|
||||
match self {
|
||||
ConditionalLike::ConditionalExpression(e) => e.span(),
|
||||
ConditionalLike::TSConditionalType(ty) => ty.span(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parent(&self) -> AstNodes<'a> {
|
||||
match self {
|
||||
ConditionalLike::ConditionalExpression(e) => e.ast_parent(),
|
||||
@@ -195,40 +188,7 @@ impl ConditionalLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the comments after an operand, up to `operator` or to the end of the line. `gap`: what is
|
||||
/// between the operand and the next one.
|
||||
fn format_operand_trailing_comments<'a>(gap: Span, operator: u8, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return;
|
||||
}
|
||||
let Span { mut start, end } = gap;
|
||||
let comments = f.comments().unprinted_comments();
|
||||
let source_text = f.source_text();
|
||||
let mut index_before_operator = None;
|
||||
let mut count = None;
|
||||
for (index, comment) in comments.iter().enumerate() {
|
||||
if comment.end() > end {
|
||||
count = Some(index_before_operator.unwrap_or(index));
|
||||
break;
|
||||
}
|
||||
if source_text.contains_newline(Span::before(start, comment.span)) {
|
||||
// It is on a new line.
|
||||
count = Some(index);
|
||||
break;
|
||||
} else if comment.is_line() || comment.followed_by_newline() {
|
||||
// Prettier's `handleClosureTypeCastComments`: `a ? /** @type {T} */ ⏎ b : c`
|
||||
count = Some(index + usize::from(!f.comments().looks_like_type_cast_comment(comment)));
|
||||
break;
|
||||
} else if source_text.contains_byte(Span::before(start, comment.span), operator) {
|
||||
index_before_operator = Some(index);
|
||||
}
|
||||
start = comment.span.end;
|
||||
}
|
||||
let count = count.unwrap_or_else(|| index_before_operator.unwrap_or(comments.len()));
|
||||
FormatTrailingComments::Comments(comments.get(..count).unwrap_or_default()).fmt(f);
|
||||
}
|
||||
|
||||
fn is_jsx_conditional_chain(e: Expr<'_>) -> bool {
|
||||
pub(crate) fn is_jsx_conditional_chain(e: Expr<'_>) -> bool {
|
||||
let mut pending = smallvec::SmallVec::<[Expr<'_>; 8]>::new();
|
||||
pending.push(e);
|
||||
while let Some(e) = pending.pop() {
|
||||
@@ -294,49 +254,19 @@ impl<'a> FormatConditionalLike<'a> {
|
||||
}
|
||||
|
||||
fn format_test(&self, f: &mut Formatter<'a>, layout: ConditionalLayout) {
|
||||
let format_inner = format_with(|f| {
|
||||
let gap = match self.conditional {
|
||||
ConditionalLike::ConditionalExpression(conditional) => {
|
||||
let ExprKind::Cond { test, yes, .. } = conditional.kind() else {
|
||||
return;
|
||||
};
|
||||
write!(f, FormatNodeWithoutTrailingComments(&test));
|
||||
test.span().between(yes.span())
|
||||
let format_inner = format_with(|f| match self.conditional {
|
||||
ConditionalLike::ConditionalExpression(conditional) => {
|
||||
write!(f, conditional.test());
|
||||
}
|
||||
ConditionalLike::TSConditionalType(conditional) => {
|
||||
if let TypeKind::Cond { check, extends, .. } = conditional.kind() {
|
||||
write!(f, [check, space(), "extends", space(), extends]);
|
||||
}
|
||||
ConditionalLike::TSConditionalType(conditional) => {
|
||||
let TypeKind::Cond {
|
||||
check,
|
||||
extends,
|
||||
yes,
|
||||
..
|
||||
} = conditional.kind()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
write!(f, [check, space(), "extends", space()]);
|
||||
// A union writes the comments after it in its own group.
|
||||
match extends.kind() {
|
||||
TypeKind::Union(types) if types.len() > 1 => write!(f, extends),
|
||||
_ => write!(f, FormatNodeWithoutTrailingComments(&extends)),
|
||||
}
|
||||
extends.span().between(yes.span())
|
||||
}
|
||||
};
|
||||
format_operand_trailing_comments(gap, b'?', f);
|
||||
}
|
||||
});
|
||||
|
||||
if layout.is_nested_alternate() {
|
||||
// The comments before it are not aligned.
|
||||
let comments = f.comments().comments_before(self.conditional.span().start);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatLeadingComments::Comments(comments),
|
||||
align(2, &format_inner)
|
||||
]
|
||||
);
|
||||
} else {
|
||||
write!(f, format_inner);
|
||||
match layout.is_nested_alternate() {
|
||||
true => write!(f, align(2, &format_inner)),
|
||||
false => write!(f, format_inner),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,35 +275,29 @@ impl<'a> FormatConditionalLike<'a> {
|
||||
return;
|
||||
};
|
||||
let is_space = f.options().indent_style.is_space();
|
||||
write!(f, [soft_line_break_or_space(), "?", space()]);
|
||||
|
||||
let format_consequent_with_trailing_comments = format_with(|f| {
|
||||
write!(f, FormatNodeWithoutTrailingComments(&consequent));
|
||||
format_operand_trailing_comments(consequent.span().between(alternate.span()), b':', f);
|
||||
});
|
||||
let format_consequent = format_with(|f| match is_space {
|
||||
true => write!(f, align(2, &format_consequent_with_trailing_comments)),
|
||||
false => write!(f, indent(&format_consequent_with_trailing_comments)),
|
||||
});
|
||||
let format_branch = |branch: Operand<'a>| {
|
||||
format_with(move |f: &mut Formatter<'a>| match is_space {
|
||||
true => write!(f, align(2, &branch)),
|
||||
false => write!(f, indent(&branch)),
|
||||
})
|
||||
};
|
||||
// `a ? (b ? c : d) : e`
|
||||
let is_nested_consequent = consequent.is_conditional();
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
soft_line_break_or_space(),
|
||||
"?",
|
||||
space(),
|
||||
is_nested_consequent.then_some(if_group_fits_on_line(&"(")),
|
||||
format_consequent,
|
||||
format_branch(consequent),
|
||||
is_nested_consequent.then_some(if_group_fits_on_line(&")")),
|
||||
soft_line_break_or_space(),
|
||||
":",
|
||||
space()
|
||||
space(),
|
||||
format_branch(alternate)
|
||||
]
|
||||
);
|
||||
|
||||
let format_alternate = FormatNodeWithoutTrailingComments(&alternate);
|
||||
match is_space {
|
||||
true => write!(f, align(2, &format_alternate)),
|
||||
false => write!(f, indent(&format_alternate)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,14 +343,14 @@ impl<'a> Format<'a> for FormatConditionalLike<'a> {
|
||||
space(),
|
||||
FormatJsxChainExpression {
|
||||
expression: yes,
|
||||
following: Some(no)
|
||||
is_alternate: false
|
||||
},
|
||||
space(),
|
||||
":",
|
||||
space(),
|
||||
FormatJsxChainExpression {
|
||||
expression: no,
|
||||
following: None
|
||||
is_alternate: true
|
||||
}
|
||||
]
|
||||
);
|
||||
@@ -484,8 +408,8 @@ impl<'a> Format<'a> for FormatConditionalLike<'a> {
|
||||
/// ```
|
||||
struct FormatJsxChainExpression<'a> {
|
||||
expression: Expr<'a>,
|
||||
/// What is after the `:`, if `expression` is what is before it.
|
||||
following: Option<Expr<'a>>,
|
||||
/// It is what is after the `:`.
|
||||
is_alternate: bool,
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatJsxChainExpression<'a> {
|
||||
@@ -495,30 +419,23 @@ impl<'a> Format<'a> for FormatJsxChainExpression<'a> {
|
||||
return;
|
||||
}
|
||||
let expression = self.expression;
|
||||
let is_conditional = matches!(expression.kind(), ExprKind::Cond { .. });
|
||||
let is_conditional = matches!(expression.kind(), ExprKind::Cond { .. })
|
||||
&& !is_cast_target_that_hides_its_kind(expression, f);
|
||||
let no_wrap = match expression.kind() {
|
||||
ExprKind::Ident(name) => name.bytes() == b"undefined",
|
||||
ExprKind::Null => true,
|
||||
_ => is_conditional && self.following.is_none(),
|
||||
_ => is_conditional && self.is_alternate,
|
||||
};
|
||||
|
||||
let format_expression = format_with(|f| match is_conditional {
|
||||
true => FormatConditionalLike {
|
||||
conditional: ConditionalLike::ConditionalExpression(expression),
|
||||
jsx_chain: true,
|
||||
}
|
||||
.fmt(f),
|
||||
false => match self.following {
|
||||
Some(following) => {
|
||||
write!(f, FormatNodeWithoutTrailingComments(&expression));
|
||||
format_operand_trailing_comments(
|
||||
expression.span().between(following.span()),
|
||||
b':',
|
||||
f,
|
||||
);
|
||||
true => format_node(expression.as_ast_nodes(), f, |f| {
|
||||
FormatConditionalLike {
|
||||
conditional: ConditionalLike::ConditionalExpression(expression),
|
||||
jsx_chain: true,
|
||||
}
|
||||
None => expression.fmt(f),
|
||||
},
|
||||
.fmt(f);
|
||||
}),
|
||||
false => expression.fmt(f),
|
||||
});
|
||||
|
||||
if no_wrap {
|
||||
|
||||
@@ -10,118 +10,53 @@
|
||||
|
||||
use super::assignment_like::is_short_argument;
|
||||
use super::conditional::{ConditionalLike, Operand, should_extra_indent};
|
||||
use super::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use super::typecast::{comments_of_expression, is_chain_expression_without_comments};
|
||||
use crate::js::parentheses;
|
||||
use crate::js::print::binary_like_expression::is_angular_pipe;
|
||||
use crate::js::print::statements::parenthesis_before_needs_semicolon;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
/// The comments between the operands, by what they belong to. Prettier's
|
||||
/// `handleConditionalExpressionComments`.
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct OperandComments<'a> {
|
||||
after_test: &'a [Comment],
|
||||
before_consequent: &'a [Comment],
|
||||
after_consequent: &'a [Comment],
|
||||
/// On lines of their own before the `:`.
|
||||
before_colon: &'a [Comment],
|
||||
before_alternate: &'a [Comment],
|
||||
}
|
||||
|
||||
impl<'a> OperandComments<'a> {
|
||||
fn new(
|
||||
test: Operand<'a>,
|
||||
consequent: Operand<'a>,
|
||||
alternate: Operand<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> Self {
|
||||
if f.is_quiet() {
|
||||
return Self::default();
|
||||
}
|
||||
let split = |before: Operand<'a>, after: Operand<'a>, operator: u8| {
|
||||
let start = before.span().end;
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_in(before.span().between(after.span()));
|
||||
comments
|
||||
.split_at_checked(trailing_count(comments, start, operator, f.source_text()))
|
||||
.unwrap_or((comments, &[]))
|
||||
impl<'a> Operand<'a> {
|
||||
/// Whether one of the comments attached to it is a block comment that spans several lines.
|
||||
fn has_multiline_block_comment(self, f: &Formatter<'a>) -> bool {
|
||||
let comments = match self {
|
||||
Operand::Expr(e) if is_chain_expression_without_comments(e, f) => return false,
|
||||
Operand::Expr(e) => comments_of_expression(e, f),
|
||||
Operand::Type(ty) => f.comments().of_node(ty.as_ast_nodes()),
|
||||
};
|
||||
let (after_test, before_consequent) = split(test, consequent, b'?');
|
||||
let (after_consequent, rest) = split(consequent, alternate, b':');
|
||||
// Anything but a block comment on one line. Not in a conditional type, and not before a
|
||||
// `ChainExpression`, which TypeScript files have: Prettier compares the alternate with the
|
||||
// node that follows the comment, and that is the member access or the call in it.
|
||||
let count = match alternate {
|
||||
Operand::Expr(e) if f.file().is_javascript() || !e.is_chain_root() => rest
|
||||
.iter()
|
||||
.take_while(|comment| comment.is_line() || comment.is_multiline_block())
|
||||
.count(),
|
||||
_ => 0,
|
||||
};
|
||||
let (before_colon, before_alternate) = rest.split_at_checked(count).unwrap_or((rest, &[]));
|
||||
Self {
|
||||
after_test,
|
||||
before_consequent,
|
||||
after_consequent,
|
||||
before_colon,
|
||||
before_alternate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier's `hasMultilineBlockComments`: of the comments of the operands.
|
||||
fn has_multiline_block(&self) -> bool {
|
||||
[
|
||||
self.after_test,
|
||||
self.before_consequent,
|
||||
self.after_consequent,
|
||||
self.before_alternate,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|comment| comment.is_multiline_block())
|
||||
(comments.leading.iter())
|
||||
.chain(comments.trailing)
|
||||
.chain(comments.dangling)
|
||||
.any(|comment| comment.is_multiline_block())
|
||||
}
|
||||
}
|
||||
|
||||
/// How many of `comments`, which are between an operand that ends at `start` and the next, trail the
|
||||
/// former: those on its line, if they are before `operator` or the last of them ends the line.
|
||||
fn trailing_count(
|
||||
comments: &[Comment],
|
||||
mut start: u32,
|
||||
operator: u8,
|
||||
source_text: SourceText<'_>,
|
||||
) -> usize {
|
||||
let mut first_after_operator = None;
|
||||
for (index, comment) in comments.iter().enumerate() {
|
||||
if source_text.contains_newline(Span::before(start, comment.span)) {
|
||||
return index;
|
||||
}
|
||||
if comment.is_line() || comment.followed_by_newline() {
|
||||
return index + 1;
|
||||
}
|
||||
if first_after_operator.is_none()
|
||||
&& source_text.contains_byte(Span::before(start, comment.span), operator)
|
||||
{
|
||||
first_after_operator = Some(index);
|
||||
}
|
||||
start = comment.span.end;
|
||||
/// Prettier's `hasMultilineBlockComments`.
|
||||
fn has_multiline_block_comments<'a>(conditional: ConditionalLike<'a>, f: &Formatter<'a>) -> bool {
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
first_after_operator.unwrap_or(comments.len())
|
||||
}
|
||||
|
||||
/// An operand and the comments after it.
|
||||
struct FormatOperand<'a>(Operand<'a>, &'a [Comment]);
|
||||
|
||||
impl<'a> Format<'a> for FormatOperand<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
FormatNodeWithoutTrailingComments(&self.0),
|
||||
FormatTrailingComments::Comments(self.1)
|
||||
]
|
||||
);
|
||||
match conditional {
|
||||
ConditionalLike::ConditionalExpression(e) => match e.kind() {
|
||||
ExprKind::Cond { test, yes, no } => [test, yes, no]
|
||||
.map(Operand::Expr)
|
||||
.into_iter()
|
||||
.any(|operand| operand.has_multiline_block_comment(f)),
|
||||
_ => false,
|
||||
},
|
||||
ConditionalLike::TSConditionalType(ty) => match ty.kind() {
|
||||
TypeKind::Cond {
|
||||
check,
|
||||
extends,
|
||||
yes,
|
||||
no,
|
||||
} => [check, extends, yes, no]
|
||||
.map(Operand::Type)
|
||||
.into_iter()
|
||||
.any(|operand| operand.has_multiline_block_comment(f)),
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,14 +90,12 @@ impl<'a, T: Format<'a>> Format<'a> for WrapInParens<'_, T> {
|
||||
/// Whether `conditional`, an expression, breaks whatever its width: there is a conditional after
|
||||
/// its `?` or its `:`, or an operand has a block comment of several lines. Prettier's `shouldBreak`.
|
||||
pub(crate) fn should_break<'a>(conditional: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let ExprKind::Cond { test, yes, no } = conditional.kind() else {
|
||||
let ExprKind::Cond { yes, no, .. } = conditional.kind() else {
|
||||
return false;
|
||||
};
|
||||
let (test, consequent, alternate) =
|
||||
(Operand::Expr(test), Operand::Expr(yes), Operand::Expr(no));
|
||||
consequent.is_conditional()
|
||||
|| alternate.is_conditional()
|
||||
|| OperandComments::new(test, consequent, alternate, f).has_multiline_block()
|
||||
Operand::Expr(yes).is_conditional()
|
||||
|| Operand::Expr(no).is_conditional()
|
||||
|| has_multiline_block_comments(ConditionalLike::ConditionalExpression(conditional), f)
|
||||
}
|
||||
|
||||
pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Formatter<'a>) {
|
||||
@@ -249,9 +182,9 @@ pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Format
|
||||
};
|
||||
|
||||
// A chain breaks as a whole.
|
||||
let comments = OperandComments::new(test, consequent, alternate, f);
|
||||
let should_break =
|
||||
is_consequent_ternary || is_alternate_ternary || comments.has_multiline_block();
|
||||
let should_break = is_consequent_ternary
|
||||
|| is_alternate_ternary
|
||||
|| has_multiline_block_comments(conditional, f);
|
||||
|
||||
// So that a short consequent is not pushed to the next line:
|
||||
//
|
||||
@@ -265,9 +198,7 @@ pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Format
|
||||
matches!(consequent.kind(), ExprKind::Null)
|
||||
}
|
||||
(Operand::Expr(test), Operand::Expr(consequent)) => {
|
||||
comments.before_consequent.is_empty()
|
||||
&& comments.after_consequent.is_empty()
|
||||
&& is_short_argument(consequent, f.options().line_width.value() / 4, f)
|
||||
is_short_argument(consequent, f.options().line_width.value() / 4, f)
|
||||
&& is_simple_expression_by_node_count(test, 3, f)
|
||||
}
|
||||
_ => false,
|
||||
@@ -279,6 +210,18 @@ pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Format
|
||||
&& matches!(test, Operand::Expr(test) if is_simple_expression_by_node_count(test, 1, f)))
|
||||
|| try_to_parenthesize_alternate;
|
||||
|
||||
// Prettier's `alternateComments`: they are on lines of their own before the `:`. Prettier also writes the dangling
|
||||
// comments of the test here, a second time: `f(/* comment */) ? a : b`.
|
||||
let comments_before_colon = match (f.is_quiet(), conditional) {
|
||||
(true, _) => CommentList::empty(),
|
||||
(false, ConditionalLike::ConditionalExpression(e)) => {
|
||||
f.comments().of_node(e.as_ast_nodes()).dangling
|
||||
}
|
||||
(false, ConditionalLike::TSConditionalType(ty)) => {
|
||||
f.comments().of_node(ty.as_ast_nodes()).dangling
|
||||
}
|
||||
};
|
||||
|
||||
let test_id = f.group_id("test");
|
||||
let test_and_consequent_id = f.group_id("test-and-consequent");
|
||||
|
||||
@@ -286,7 +229,6 @@ pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Format
|
||||
let format_test = format_with(|f| match conditional {
|
||||
ConditionalLike::ConditionalExpression(e) => {
|
||||
let is_conditional = test.is_conditional();
|
||||
let test = FormatOperand(test, comments.after_test);
|
||||
if parenthesis_before_needs_semicolon(e, f) {
|
||||
write!(f, if_group_breaks(&";"));
|
||||
}
|
||||
@@ -303,10 +245,9 @@ pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Format
|
||||
return;
|
||||
};
|
||||
write!(f, [check, space(), "extends", space()]);
|
||||
let format_extends = FormatOperand(test, comments.after_test);
|
||||
match extends.kind() {
|
||||
TypeKind::Cond { .. } | TypeKind::Mapped(_) => write!(f, format_extends),
|
||||
_ => write!(f, group(&WrapInParens(&format_extends))),
|
||||
TypeKind::Cond { .. } | TypeKind::Mapped(_) => write!(f, extends),
|
||||
_ => write!(f, group(&WrapInParens(&extends))),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -327,13 +268,7 @@ pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Format
|
||||
} else {
|
||||
soft_line_break_or_space()
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
indent(&format_args!(
|
||||
line,
|
||||
FormatOperand(consequent, comments.after_consequent)
|
||||
))
|
||||
);
|
||||
write!(f, indent(&format_args!(line, consequent)));
|
||||
});
|
||||
|
||||
let format_test_and_consequent = format_with(|f| {
|
||||
@@ -362,7 +297,6 @@ pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Format
|
||||
});
|
||||
|
||||
let format_alternate = format_with(|f| {
|
||||
let alternate = FormatNodeWithoutTrailingComments(&alternate);
|
||||
if !try_to_parenthesize_alternate {
|
||||
return write!(f, alternate);
|
||||
}
|
||||
@@ -391,9 +325,9 @@ pub(crate) fn write_ternary<'a>(conditional: ConditionalLike<'a>, f: &mut Format
|
||||
let format_parts = format_with(|f| {
|
||||
write!(f, format_test_and_consequent);
|
||||
|
||||
if !comments.before_colon.is_empty() {
|
||||
if !comments_before_colon.is_empty() {
|
||||
let comments = FormatDanglingComments::Comments {
|
||||
comments: comments.before_colon,
|
||||
comments: comments_before_colon,
|
||||
indent: DanglingIndentMode::None,
|
||||
};
|
||||
write!(
|
||||
|
||||
@@ -99,8 +99,4 @@ impl<'a> ExpressionLeftSide<'a> {
|
||||
.filter(|it| !it.is_assignment_target)
|
||||
.map(|it| it.expr)
|
||||
}
|
||||
|
||||
pub(crate) fn span(self) -> Span {
|
||||
self.expr.span()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::suppressed::FormatSuppressedNode;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Writes a node and leaves the comments after it to the caller.
|
||||
/// Writes a node without the trailing comments it owns. The caller writes them.
|
||||
pub(crate) struct FormatNodeWithoutTrailingComments<'b, T>(pub(crate) &'b T);
|
||||
|
||||
impl<'a, T: Format<'a> + Spanned> Format<'a> for FormatNodeWithoutTrailingComments<'_, T> {
|
||||
@@ -9,15 +8,8 @@ impl<'a, T: Format<'a> + Spanned> Format<'a> for FormatNodeWithoutTrailingCommen
|
||||
if f.is_quiet() {
|
||||
return self.0.fmt(f);
|
||||
}
|
||||
let span = self.0.span();
|
||||
if f.comments().has_trailing_suppression_comment(span.end) {
|
||||
format_leading_comments(span).fmt(f);
|
||||
FormatSuppressedNode(span).fmt(f);
|
||||
return;
|
||||
}
|
||||
// The comments after the node are hidden while it is written.
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(span.end);
|
||||
f.context_mut().set_skip_trailing_comments(true);
|
||||
self.0.fmt(f);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
f.context_mut().set_skip_trailing_comments(false);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::js::comments::inner_of_link;
|
||||
use crate::js::format::{
|
||||
FormatNonNullMarks, identifier, no_comment_trails_what_is_before_another,
|
||||
write_trailing_comments_of,
|
||||
};
|
||||
use crate::js::print::call_like_expression::FormatTypeArguments;
|
||||
use crate::js::comments::OwnedComments;
|
||||
use crate::js::format::FormatNonNullMarks;
|
||||
use crate::js::print::call_like_expression::arguments::FormatArguments;
|
||||
use crate::js::print::member_expression::write_lookup_without_comments;
|
||||
use crate::js::utils::call_expression::write_callee_trailing_comments;
|
||||
use crate::js::utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::print::member_expression::{
|
||||
comments_in_member_access_stay, write_lookup, write_lookup_after_its_comments,
|
||||
};
|
||||
use crate::js::print::type_parameters::type_arguments;
|
||||
use crate::js::utils::call_expression::{
|
||||
FormatCallee, comments_stay_behind_callee, write_comments_behind_callee,
|
||||
};
|
||||
use crate::js::utils::typecast::{comments_of_outermost_node, has_comments};
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
@@ -53,45 +54,26 @@ impl<'a> ChainMember<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where what the link follows ends. `None` for what the chain starts with.
|
||||
pub(super) fn inner_end(&self) -> Option<u32> {
|
||||
/// The comments attached to the node that Prettier's `printedNodes` has for the link.
|
||||
pub(super) fn comments(&self, f: &Formatter<'a>) -> OwnedComments<'a> {
|
||||
match *self {
|
||||
Self::Node(_) => None,
|
||||
_ => inner_of_link(self.expr()).map(|inner| inner.span().end),
|
||||
Self::Node(node) => comments_of_outermost_node(node, f),
|
||||
_ => f.comments().of_node(self.expr().as_chain_element()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prettier attaches every comment between the links of a chain to one of them, which it leads or trails: see
|
||||
/// `attach_in_link`. oxfmt goes by what is around a comment when it comes to it.
|
||||
pub(super) fn comments_are_attached_to_links(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// The call that `callee` is the callee of.
|
||||
pub(super) fn call_of_callee(callee: Expr<'_>) -> Option<Call<'_>> {
|
||||
match callee.parent() {
|
||||
Node::Expr(parent) => match parent.kind() {
|
||||
ExprKind::Call(call) if call.callee() == callee => Some(call),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `a.b⏎// comment⏎.c()`: for oxfmt the comment stays on its line, before `.c`.
|
||||
fn comment_that_starts_line_leads_next_link(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt() && no_comment_trails_what_is_before_another(f)
|
||||
}
|
||||
|
||||
/// The comments behind a link of the chain.
|
||||
fn write_trailing_comments_of_member<'a>(member: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
match call_of_callee(member) {
|
||||
Some(call) => {
|
||||
write_callee_trailing_comments(call, member.span().end, f);
|
||||
/// The comments that are printed before the link. For `[b]` oxfmt has them between a line break and the `[`:
|
||||
/// [`FormatComputedMemberExpressionWithoutObject`] prints them.
|
||||
pub(super) fn leading_comments(&self, f: &Formatter<'a>) -> CommentList<'a> {
|
||||
match *self {
|
||||
Self::StaticMember(member) if comments_in_member_access_stay(f) => member
|
||||
.member_name_start()
|
||||
.map_or(CommentList::empty(), |start| {
|
||||
f.comments().of_name(start).leading
|
||||
}),
|
||||
Self::ComputedMember(_) if comments_in_member_access_stay(f) => CommentList::empty(),
|
||||
_ => self.comments(f).leading,
|
||||
}
|
||||
None if comment_that_starts_line_leads_next_link(f) => {}
|
||||
None => write_trailing_comments_of(member.as_chain_element(), f),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,97 +89,90 @@ fn write_call_without_callee<'a>(expression: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
f,
|
||||
[
|
||||
call.is_optional().then_some("?."),
|
||||
FormatTypeArguments(expression, call),
|
||||
type_arguments(call.type_args(), Node::Expr(expression)),
|
||||
FormatArguments::of_call(expression, call)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// `(a?.b()) /* comment */?.c()`: oxfmt drops the parentheses and prints the comment before the `?.c`, not behind `a?.b()`.
|
||||
/// `link` is the `a?.b()`.
|
||||
fn comments_behind_inner_chain_lead_next_link<'a>(link: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
&& link.is_chain_root()
|
||||
&& matches!(link.parent(), Node::Expr(parent) if parent.tag() == ExprTag::Dot)
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for ChainMember<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if f.is_quiet() {
|
||||
return self.write_without_comments(f);
|
||||
return self.write_link(f);
|
||||
}
|
||||
match comments_are_attached_to_links(f) {
|
||||
true => self.write_with_attached_comments(f),
|
||||
false => self.write_with_comments_around(f),
|
||||
match *self {
|
||||
// The comments of the last call are printed around the whole chain.
|
||||
Self::CallExpression {
|
||||
position: CallExpressionPosition::End,
|
||||
..
|
||||
} => self.write_link(f),
|
||||
Self::Node(callee) => write!(
|
||||
f,
|
||||
FormatCallee {
|
||||
callee,
|
||||
is_in_member_chain: true
|
||||
}
|
||||
),
|
||||
// A `prettier-ignore` comment has no effect on a link.
|
||||
_ => {
|
||||
if let Some(object) = (self.expr().object())
|
||||
.filter(|object| comments_behind_inner_chain_lead_next_link(*object, f))
|
||||
{
|
||||
let comments = f.comments().of_node(object.as_chain_element()).trailing;
|
||||
FormatLeadingComments::Comments(comments).fmt(f);
|
||||
}
|
||||
FormatLeadingComments::Comments(self.leading_comments(f)).fmt(f);
|
||||
self.write_link(f);
|
||||
let trailing_comments = self.comments(f).trailing;
|
||||
if comments_stay_behind_callee(self.expr(), true, f) {
|
||||
write_comments_behind_callee(trailing_comments, f);
|
||||
} else if !comments_behind_inner_chain_lead_next_link(self.expr(), f) {
|
||||
FormatTrailingComments::Comments(trailing_comments).fmt(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ChainMember<'a> {
|
||||
/// What the link adds to what it follows.
|
||||
fn write_without_comments(&self, f: &mut Formatter<'a>) {
|
||||
/// What the link adds to its object or its callee.
|
||||
fn write_link(&self, f: &mut Formatter<'a>) {
|
||||
match *self {
|
||||
Self::StaticMember(member) if f.is_quiet() => write_lookup_without_comments(member, f),
|
||||
Self::StaticMember(member) => {
|
||||
let ExprKind::Dot { name, .. } = member.kind() else {
|
||||
return;
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
member.is_optional().then_some("?"),
|
||||
".",
|
||||
identifier(name, member.as_chain_element())
|
||||
]
|
||||
);
|
||||
}
|
||||
Self::StaticMember(member) => match comments_in_member_access_stay(f) {
|
||||
true => write_lookup_after_its_comments(member, f),
|
||||
false => write_lookup(member, f),
|
||||
},
|
||||
Self::TSNonNullExpression(e) => write!(f, FormatNonNullMarks(e)),
|
||||
Self::CallExpression { expression, .. } => write_call_without_callee(expression, f),
|
||||
Self::ComputedMember(member) => {
|
||||
write!(f, FormatComputedMemberExpressionWithoutObject(member));
|
||||
let boundary = comments_in_member_access_stay(f).then_some(line_suffix_boundary());
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
boundary,
|
||||
FormatComputedMemberExpressionWithoutObject(member)
|
||||
]
|
||||
);
|
||||
}
|
||||
Self::Node(node) => write!(f, FormatNodeWithoutTrailingComments(&node)),
|
||||
Self::Node(node) => write!(f, node),
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_are_attached_to_links`].
|
||||
fn write_with_attached_comments(&self, f: &mut Formatter<'a>) {
|
||||
// The comments around the last call are around the chain.
|
||||
if let Self::CallExpression {
|
||||
position: CallExpressionPosition::End,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
return self.write_without_comments(f);
|
||||
}
|
||||
if let Some(inner_end) = self.inner_end() {
|
||||
FormatLeadingComments::Comments(f.comments().comments_leading_link(inner_end)).fmt(f);
|
||||
}
|
||||
self.write_without_comments(f);
|
||||
let end = self.expr().span().end;
|
||||
FormatTrailingComments::Comments(f.comments().comments_trailing_link(end)).fmt(f);
|
||||
}
|
||||
|
||||
fn write_with_comments_around(&self, f: &mut Formatter<'a>) {
|
||||
match *self {
|
||||
Self::StaticMember(member) => {
|
||||
// Those after the `.` are before it too.
|
||||
let name_start = member
|
||||
.member_name_start()
|
||||
.unwrap_or_else(|| member.span().end);
|
||||
let comments = f.comments().comments_before(name_start);
|
||||
FormatLeadingComments::Comments(comments).fmt(f);
|
||||
}
|
||||
Self::CallExpression {
|
||||
position: CallExpressionPosition::End,
|
||||
..
|
||||
} => return self.write_without_comments(f),
|
||||
Self::Node(node) if call_of_callee(node).is_none() => return write!(f, node),
|
||||
_ => {}
|
||||
}
|
||||
self.write_without_comments(f);
|
||||
write_trailing_comments_of_member(self.expr(), f);
|
||||
}
|
||||
}
|
||||
|
||||
/// oxfmt writes `a[0 /* comment */]` like `a[b /* comment */]`, with a way to break after the `[`.
|
||||
fn breaks_around_a_number_with_comments<'a>(member: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
f.options().flavor.is_oxfmt() && f.comments().has_comment_before(member.span().end)
|
||||
fn breaks_around_a_number_with_comments<'a>(index: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
f.options().flavor.is_oxfmt() && !f.is_quiet() && has_comments(index, f)
|
||||
}
|
||||
|
||||
/// The `[b]` or `?.[b]` of `a[b]`.
|
||||
/// The `[b]` or `?.[b]` of `a[b]`. Prettier's `printMemberLookup`.
|
||||
pub(crate) struct FormatComputedMemberExpressionWithoutObject<'a>(pub(crate) Expr<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatComputedMemberExpressionWithoutObject<'a> {
|
||||
@@ -206,20 +181,18 @@ impl<'a> Format<'a> for FormatComputedMemberExpressionWithoutObject<'a> {
|
||||
let ExprKind::Index { index, .. } = member.kind() else {
|
||||
return;
|
||||
};
|
||||
// A comment on its own line before the `[` leads what is in the brackets, unless that is a
|
||||
// name. For oxfmt it stays where it is.
|
||||
if !f.is_quiet()
|
||||
&& (matches!(index.kind(), ExprKind::Ident(_)) || f.options().flavor.is_oxfmt())
|
||||
{
|
||||
let comments = f
|
||||
.comments()
|
||||
.comments_before_character(member.span().start, b'[');
|
||||
write!(f, FormatLeadingComments::Comments(comments));
|
||||
if !f.is_quiet() && comments_in_member_access_stay(f) {
|
||||
let comments = f.comments().of_node(member.as_chain_element()).dangling;
|
||||
if !comments.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
[soft_line_break(), FormatLeadingComments::Comments(comments)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let optional = member.is_optional().then_some("?.");
|
||||
if matches!(index.kind(), ExprKind::Number(_))
|
||||
&& !breaks_around_a_number_with_comments(member, f)
|
||||
&& !breaks_around_a_number_with_comments(index, f)
|
||||
{
|
||||
write!(f, [optional, "[", index, "]"]);
|
||||
} else {
|
||||
|
||||
@@ -23,12 +23,13 @@ impl<'a> Format<'a> for FormatMemberChainGroup<'a, '_> {
|
||||
while let Some(member) = members.next() {
|
||||
write!(f, member);
|
||||
// Prettier writes a line break after a call that an empty line follows, which makes an
|
||||
// empty line at the end of a group. It is there in the middle of a group as well.
|
||||
// empty line at the end of a group. It is there in the middle of a group as well. oxfmt writes none.
|
||||
if let ChainMember::CallExpression {
|
||||
expression,
|
||||
position: CallExpressionPosition::Middle,
|
||||
} = *member
|
||||
&& members.peek().is_some()
|
||||
&& !f.options().flavor.is_oxfmt()
|
||||
&& should_insert_empty_line_after(expression, f)
|
||||
{
|
||||
write!(f, hard_line_break());
|
||||
|
||||
@@ -7,12 +7,10 @@ pub(crate) mod chain_member;
|
||||
mod groups;
|
||||
pub(crate) mod simple_argument;
|
||||
|
||||
use self::chain_member::{
|
||||
CallExpressionPosition, ChainMember, call_of_callee, comments_are_attached_to_links,
|
||||
};
|
||||
use self::chain_member::{CallExpressionPosition, ChainMember};
|
||||
use self::groups::{FormatMemberChainGroup, should_insert_empty_line_after};
|
||||
use self::simple_argument::SimpleArgument;
|
||||
use super::call_expression::{callee_trailing_comments, is_call_expression, is_member_expression};
|
||||
use super::call_expression::{is_call_expression, is_member_expression};
|
||||
use super::is_long_curried_call;
|
||||
use super::typecast::is_cast_target;
|
||||
use crate::js::parentheses::expression::expression_needs_parentheses;
|
||||
@@ -34,7 +32,11 @@ struct MemberChain<'a, 'b> {
|
||||
pub(crate) fn write_member_chain<'a>(call_expression: Expr<'a>, f: &mut Formatter<'a>) {
|
||||
let mut members = Members::new();
|
||||
let has_inner_call = push_chain_members(call_expression, &mut members, f);
|
||||
if !is_one_group_after_the_head(has_inner_call, f) {
|
||||
if !is_one_group_after_the_head(has_inner_call, f)
|
||||
|| (goes_by_position_of_comments(f)
|
||||
&& (members.iter())
|
||||
.any(|member| needs_empty_line_before_for_oxfmt(std::slice::from_ref(member), f)))
|
||||
{
|
||||
members.reverse();
|
||||
return MemberChain::new(call_expression, &members, f).fmt(f);
|
||||
}
|
||||
@@ -116,9 +118,10 @@ impl<'a, 'b> MemberChain<'a, 'b> {
|
||||
let Some(first_member) = self.group(1).first() else {
|
||||
return false;
|
||||
};
|
||||
if !f.is_quiet()
|
||||
&& (has_leading_comment(first_member, f) || has_trailing_comment(first_member, f))
|
||||
{
|
||||
if match goes_by_position_of_comments(f) {
|
||||
true => !f.is_quiet() && has_comment_in_member_for_oxfmt(first_member, f),
|
||||
false => has_leading_comment(first_member, f) || has_trailing_comment(first_member, f),
|
||||
} {
|
||||
return false;
|
||||
}
|
||||
let has_computed_property = first_member.is_computed_expression();
|
||||
@@ -166,6 +169,13 @@ impl<'a, 'b> MemberChain<'a, 'b> {
|
||||
/// call, which takes breaking the chain. One after the head, if that does not end with a call,
|
||||
/// is only kept if the chain breaks.
|
||||
fn find_empty_lines(&self, f: &Formatter<'a>) -> (SmallVec<[bool; 8]>, bool) {
|
||||
if goes_by_position_of_comments(f) {
|
||||
let needs_empty_line: SmallVec<[bool; 8]> = (self.groups().skip(1))
|
||||
.map(|group| needs_empty_line_before_for_oxfmt(group, f))
|
||||
.collect();
|
||||
let breaks_chain = needs_empty_line.contains(&true);
|
||||
return (needs_empty_line, breaks_chain);
|
||||
}
|
||||
let mut has_empty_line_after_call = false;
|
||||
let mut previous = self.head().last();
|
||||
let needs_empty_line = (self.groups().skip(1).enumerate())
|
||||
@@ -243,19 +253,18 @@ impl<'a, 'b> MemberChain<'a, 'b> {
|
||||
|
||||
/// Prettier's `nodeHasComment`.
|
||||
fn has_comment(&self, f: &Formatter<'a>) -> bool {
|
||||
!f.is_quiet()
|
||||
&& self.members.iter().any(|member| {
|
||||
has_leading_comment(member, f)
|
||||
|| (!matches!(
|
||||
member,
|
||||
ChainMember::CallExpression {
|
||||
position: CallExpressionPosition::End,
|
||||
..
|
||||
}
|
||||
) && !(matches!(member, ChainMember::ComputedMember(_))
|
||||
&& comment_behind_brackets_breaks_no_chain(f))
|
||||
&& has_trailing_comment(member, f))
|
||||
})
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
if goes_by_position_of_comments(f) {
|
||||
return (self.members.iter()).any(|member| has_comment_in_member_for_oxfmt(member, f));
|
||||
}
|
||||
let Some((_, all_but_last)) = self.members.split_last() else {
|
||||
return false;
|
||||
};
|
||||
(all_but_last.iter().skip(1)).any(|member| has_leading_comment(member, f))
|
||||
|| (all_but_last.iter()).any(|member| has_trailing_comment(member, f))
|
||||
|| (self.group(2).first()).is_some_and(|member| has_leading_comment(member, f))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,81 +340,101 @@ impl<'a> Format<'a> for MemberChain<'a, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a comment leads `member`. For oxfmt: it is on a line of its own before the `.`, or before the `[` of
|
||||
/// `[name]`.
|
||||
fn has_leading_comment<'a>(member: &ChainMember<'a>, f: &Formatter<'a>) -> bool {
|
||||
if comments_are_attached_to_links(f) {
|
||||
return member
|
||||
.inner_end()
|
||||
.is_some_and(|end| f.comments().has_comment_leading_link(end));
|
||||
}
|
||||
let (object, character) = match member.expr().kind() {
|
||||
ExprKind::Dot { obj, .. } if matches!(member, ChainMember::StaticMember(_)) => (obj, b'.'),
|
||||
ExprKind::Index { obj, index, .. }
|
||||
if matches!(member, ChainMember::ComputedMember(_))
|
||||
&& matches!(index.kind(), ExprKind::Ident(_)) =>
|
||||
{
|
||||
(obj, b'[')
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
f.comments()
|
||||
.comments_before_character(object.span().end, character)
|
||||
.iter()
|
||||
.any(|comment| comment.preceded_by_newline())
|
||||
!f.is_quiet() && !member.comments(f).leading.is_empty()
|
||||
}
|
||||
|
||||
/// `a["b"] // comment⏎(c)` is `a["b"](c); // comment` for oxfmt.
|
||||
fn comment_behind_brackets_breaks_no_chain(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Whether a comment trails `member`, which is not the last of a chain.
|
||||
/// `a! /* comment */ !`: the comment dangles from `a!!`. For Prettier it trails the `a!` in it, which is a link too.
|
||||
fn has_trailing_comment<'a>(member: &ChainMember<'a>, f: &Formatter<'a>) -> bool {
|
||||
let expression = member.expr();
|
||||
let end = expression.span().end;
|
||||
if comments_are_attached_to_links(f) {
|
||||
// It is attached to what is in a `ChainExpression`, not to that.
|
||||
let is_chain_expression = matches!(member, ChainMember::Node(node) if node.is_chain_root())
|
||||
&& !f.context().has_tree_of_babel();
|
||||
return !is_chain_expression && f.comments().has_comment_trailing_link(end);
|
||||
}
|
||||
// See `comments_trailing_computed_callee`: those behind the `(` are among them, and separate nothing.
|
||||
if expression.tag() == ExprTag::Index {
|
||||
return (f.comments().comments_after(end).first()).is_some_and(|comment| {
|
||||
(f.source_text())
|
||||
.all_bytes(Span::before(end, comment.span), |b| b.is_ascii_whitespace())
|
||||
});
|
||||
}
|
||||
match call_of_callee(expression) {
|
||||
// All before the `(` are among them. A block comment on its line separates nothing.
|
||||
Some(call) => callee_trailing_comments(call, end, f)
|
||||
.iter()
|
||||
.any(|comment| comment.preceded_by_newline() || comment.followed_by_newline()),
|
||||
// A comment on its own line leads the next member, or what is in its brackets.
|
||||
None => f
|
||||
.comments()
|
||||
.comments_after(end)
|
||||
.first()
|
||||
.is_some_and(|comment| {
|
||||
!comment.preceded_by_newline()
|
||||
&& f.source_text()
|
||||
.all_bytes(Span::before(end, comment.span), |b| {
|
||||
b.is_ascii_whitespace() || b == b')'
|
||||
})
|
||||
}),
|
||||
if f.is_quiet() {
|
||||
return false;
|
||||
}
|
||||
let comments = member.comments(f);
|
||||
!comments.trailing.is_empty()
|
||||
|| (matches!(member, ChainMember::TSNonNullExpression(_)) && !comments.dangling.is_empty())
|
||||
}
|
||||
|
||||
/// `a.b⏎// comment⏎.c()`: for oxfmt a group of the chain ends before the comment, which so stays on its line.
|
||||
fn is_before_comment_that_starts_line<'a>(expression: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let end = expression.span().end;
|
||||
/// oxfmt does not go by the owners of the comments to lay out a chain. It goes by where they are.
|
||||
fn goes_by_position_of_comments(f: &Formatter<'_>) -> bool {
|
||||
f.options().flavor.is_oxfmt()
|
||||
&& (f.comments().comments_after(end).first()).is_some_and(|comment| {
|
||||
comment.preceded_by_newline()
|
||||
&& (f.source_text())
|
||||
.all_bytes(Span::before(end, comment.span), |b| b.is_ascii_whitespace())
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether nothing but blanks and comments are between `expression` and `comment`, which comes after it.
|
||||
fn is_directly_behind(comment: Comment<'_>, expression: Expr<'_>) -> bool {
|
||||
comment.prev_token_end() == expression.span().end
|
||||
}
|
||||
|
||||
/// oxfmt's `has_comment_in_member`: `member` is `.b` and has a comment between its object and its name, or a comment behind
|
||||
/// it that ends the line.
|
||||
fn has_comment_in_member_for_oxfmt<'a>(member: &ChainMember<'a>, f: &Formatter<'a>) -> bool {
|
||||
let ChainMember::StaticMember(expression) = *member else {
|
||||
return false;
|
||||
};
|
||||
let (Some(object), Some(name_start)) = (expression.object(), expression.member_name_start())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
(f.comments()).has_comment_in_span(Span::new(object.span().end, name_start))
|
||||
|| (member.comments(f).trailing.iter())
|
||||
.take_while(|comment| {
|
||||
!comment.has_newline_before() && is_directly_behind(*comment, expression)
|
||||
})
|
||||
.any(|comment| comment.is_line() || comment.has_newline_after())
|
||||
}
|
||||
|
||||
/// oxfmt's `needs_empty_line_before`: `group` starts with `.b`, and there is an empty line between the object and the `.`, or
|
||||
/// the first comment between the object and the name.
|
||||
fn needs_empty_line_before_for_oxfmt<'a>(group: &[ChainMember<'a>], f: &Formatter<'a>) -> bool {
|
||||
let Some(ChainMember::StaticMember(expression)) = group.first() else {
|
||||
return false;
|
||||
};
|
||||
let (Some(object), Some(name_start)) = (expression.object(), expression.member_name_start())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
// Not after a call that the chain starts with: `fn()\n\n.bar()` is `fn().bar()`.
|
||||
if matches!(object.kind(), ExprKind::Call(call)
|
||||
if !matches!(call.callee().tag(), ExprTag::Dot | ExprTag::Index | ExprTag::Call))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let between = Span::new(object.span().end, name_start);
|
||||
let end = match f.is_quiet() {
|
||||
true => None,
|
||||
false => f.comments().comments_in_span(between).first(),
|
||||
}
|
||||
.map_or(name_start, |comment| comment.span().start);
|
||||
let mut start = between.start;
|
||||
while let Some(line_break) =
|
||||
bun_core::strings::index_of_any(f.source_text().text_for(&Span::new(start, end)), b"\r\n")
|
||||
{
|
||||
start += line_break as u32;
|
||||
if f.source_text().lines_after(start) > 1 {
|
||||
return true;
|
||||
}
|
||||
start += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether a group of the chain ends behind `member`, which `next` follows, so that a comment stays behind what it is written
|
||||
/// behind. For oxfmt that is any comment directly behind `member`, also one that starts a line.
|
||||
fn is_followed_by_comment<'a>(
|
||||
member: &ChainMember<'a>,
|
||||
next: Option<&ChainMember<'a>>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
if !goes_by_position_of_comments(f) {
|
||||
return has_trailing_comment(member, f);
|
||||
}
|
||||
let first_before_next = next.and_then(|next| match next {
|
||||
ChainMember::StaticMember(_) => next.leading_comments(f).first(),
|
||||
ChainMember::ComputedMember(_) => next.comments(f).dangling.first(),
|
||||
_ => None,
|
||||
});
|
||||
(member.comments(f).trailing.first().into_iter())
|
||||
.chain(first_before_next)
|
||||
.any(|comment| is_directly_behind(comment, member.expr()))
|
||||
}
|
||||
|
||||
fn is_computed_array_member_access(member: &ChainMember<'_>) -> bool {
|
||||
@@ -481,10 +510,7 @@ fn push_ends_of_remaining_groups<'a>(
|
||||
}
|
||||
|
||||
// So that the comment stays behind what it is written behind.
|
||||
if !f.is_quiet()
|
||||
&& (has_trailing_comment(member, f)
|
||||
|| is_before_comment_that_starts_line(member.expr(), f))
|
||||
{
|
||||
if !f.is_quiet() && is_followed_by_comment(member, members.get(index + 1), f) {
|
||||
group_ends.push(index as u32 + 1);
|
||||
group_start = index + 1;
|
||||
has_seen_call_expression = false;
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::tailwindcss::{
|
||||
context_of_string, has_white_space, sorted_string_literal, sorted_template_text,
|
||||
};
|
||||
use crate::ir::width::string_width;
|
||||
use crate::js::format::format_node;
|
||||
use crate::js::format::format_name;
|
||||
use crate::prelude::*;
|
||||
use crate::write;
|
||||
use bun_lint::linter::goes_to_flow;
|
||||
@@ -42,8 +42,18 @@ fn write_key<'a>(key: Key<'a>, parent: impl Fn() -> AstNodes<'a>, f: &mut Format
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
let printed = printed_key(key, span, parent(), f);
|
||||
format_node(span, parent, f, |f| match &printed {
|
||||
let mut printed = printed_key(key, span, parent(), f);
|
||||
// Prettier's `printPropertyKey` prints a key like any other node unless it adds or removes the quotes, so a
|
||||
// `prettier-ignore` comment applies to `0XAB` and `'a-b'`, and not to the `'a'` that is printed as `a`.
|
||||
let source = f.source_text().text_for(&span);
|
||||
let has_quotes = |text: &[u8]| matches!(text.first(), Some(b'"' | b'\''));
|
||||
if !f.is_quiet()
|
||||
&& has_quotes(source) == has_quotes(&printed)
|
||||
&& f.comments().of_name(span.start).is_ignored
|
||||
{
|
||||
printed = Cow::Borrowed(source);
|
||||
}
|
||||
format_name(span, f, |f| match &printed {
|
||||
Cow::Borrowed(_) if is_name => write!(f, source_text(span)),
|
||||
_ => write!(f, text(&printed)),
|
||||
});
|
||||
@@ -145,7 +155,7 @@ fn printed_key<'a>(
|
||||
let printed = format_trimmed_number(source);
|
||||
// Prettier's `isKeySafeToQuote`: not in TypeScript, where it changes the type, and only
|
||||
// if the number is written the way it is converted to a string.
|
||||
let is_safe_to_quote = f.context().has_tree_of_babel()
|
||||
let is_safe_to_quote = (f.context().has_tree_of_babel() || f.file().is_flow())
|
||||
&& is_simple_number(&printed)
|
||||
&& name.bytes() == &*printed;
|
||||
match is_safe_to_quote && should_quote_keys(parent, f) {
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
use super::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments;
|
||||
use crate::js::trivia::{comments_stay_between_head_and_body, write_head_body_separator};
|
||||
use crate::js::trivia::comments_stay_between_head_and_body;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
|
||||
/// Prettier's `printClause`: the body of an `if`, an `else`, a loop or a `with`. After a space if
|
||||
/// it is a block, otherwise on the next line if it does not fit.
|
||||
///
|
||||
/// The comments between the head of the statement and the body lead the body.
|
||||
pub(crate) struct FormatStatementBody<'a> {
|
||||
body: Stmt<'a>,
|
||||
force_space: bool,
|
||||
trailing_comments: TrailingComments,
|
||||
}
|
||||
|
||||
/// Which of the trailing comments of a body are written with it. The caller writes the others.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum TrailingComments {
|
||||
All,
|
||||
/// Those on the line on which the body ends.
|
||||
OnSameLine,
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'a> FormatStatementBody<'a> {
|
||||
@@ -17,6 +25,7 @@ impl<'a> FormatStatementBody<'a> {
|
||||
Self {
|
||||
body,
|
||||
force_space: false,
|
||||
trailing_comments: TrailingComments::All,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,44 +34,64 @@ impl<'a> FormatStatementBody<'a> {
|
||||
self.force_space = forced;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_trailing_comments(mut self, trailing_comments: TrailingComments) -> Self {
|
||||
self.trailing_comments = trailing_comments;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a> for FormatStatementBody<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
if !f.is_quiet() {
|
||||
return self.fmt_with_comments(f);
|
||||
}
|
||||
let body = self.body;
|
||||
let content = FormatBodyAndItsComments(body);
|
||||
if comments_stay_between_head_and_body(f)
|
||||
&& matches!(body.kind(), StmtKind::Empty | StmtKind::Block(_))
|
||||
{
|
||||
if !matches!(body.kind(), StmtKind::Empty)
|
||||
|| f.comments().has_comment_before(body.span().start)
|
||||
{
|
||||
write_head_body_separator(body.span().start, f);
|
||||
match body.kind() {
|
||||
StmtKind::Empty => write!(f, body),
|
||||
StmtKind::Block(_) => write!(f, [space(), body]),
|
||||
_ if self.force_space => write!(f, [space(), body]),
|
||||
_ => write!(f, soft_line_indent_or_space(&body)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FormatStatementBody<'a> {
|
||||
/// Not inlined: `if (a) if (b) ..` is nested, and what this needs of the stack would be taken at each level.
|
||||
#[inline(never)]
|
||||
fn fmt_with_comments(&self, f: &mut Formatter<'a>) {
|
||||
let body = self.body;
|
||||
let content = format_with(|f| {
|
||||
if f.is_quiet() || self.trailing_comments == TrailingComments::All {
|
||||
return write!(f, body);
|
||||
}
|
||||
write!(f, FormatNodeWithoutTrailingComments(&body));
|
||||
if self.trailing_comments == TrailingComments::OnSameLine {
|
||||
let (same_line, _) =
|
||||
split_at_first_own_line(f.comments().of_node(body.as_ast_nodes()).trailing);
|
||||
write!(f, FormatTrailingComments::Comments(same_line));
|
||||
}
|
||||
});
|
||||
let first_leading_comment = match f.is_quiet() {
|
||||
true => None,
|
||||
false => f.comments().of_node(body.as_ast_nodes()).leading.first(),
|
||||
};
|
||||
let is_empty = matches!(body.kind(), StmtKind::Empty);
|
||||
let is_block = matches!(body.kind(), StmtKind::Block(_));
|
||||
if first_leading_comment.is_some()
|
||||
&& (is_empty || is_block)
|
||||
&& comments_stay_between_head_and_body(f)
|
||||
{
|
||||
write_space_before_body(body.as_ast_nodes(), f);
|
||||
return write!(f, content);
|
||||
}
|
||||
if matches!(body.kind(), StmtKind::Empty) {
|
||||
// Not `is_quiet`: to the statement that this is the body of, a `;` at its end and the
|
||||
// comments before that are not part of it.
|
||||
let has_comments = f.comments().has_comment_before(body.span().start);
|
||||
return write!(
|
||||
f,
|
||||
[
|
||||
maybe_space(has_comments),
|
||||
format_leading_comments(body.span()),
|
||||
content
|
||||
]
|
||||
);
|
||||
if is_empty {
|
||||
return write!(f, [maybe_space(first_leading_comment.is_some()), content]);
|
||||
}
|
||||
|
||||
let first_comment = match f.is_quiet() {
|
||||
true => None,
|
||||
false => f.comments().comments_before(body.span().start).first(),
|
||||
};
|
||||
let is_block = matches!(body.kind(), StmtKind::Block(_));
|
||||
// Prettier's `shouldPrintLeadingHardline`
|
||||
if first_comment
|
||||
.is_some_and(|comment| comment.is_multiline_block() || comment.preceded_by_newline())
|
||||
if first_leading_comment
|
||||
.is_some_and(|comment| comment.is_multiline_block() || comment.has_newline_before())
|
||||
{
|
||||
match is_block {
|
||||
true => write!(f, [hard_line_break(), content]),
|
||||
@@ -79,117 +108,122 @@ impl<'a> Format<'a> for FormatStatementBody<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `if (a) b() // comment⏎;[c].d()`: the `;` ends `b()`, so the comment is in the statement. oxfmt writes it behind the
|
||||
/// `;`, and what it is in breaks. Not after `return` and `throw`, which write such a comment themselves.
|
||||
/// `if (a) b() // comment⏎;[c].d()`: the `;` ends `b()`. oxfmt writes the comment behind the `;`, and the statement that
|
||||
/// `b()` is the body of breaks. Not after `return` and `throw`.
|
||||
fn has_line_comment_before_semicolon<'a>(body: Stmt<'a>, f: &Formatter<'a>) -> bool {
|
||||
if !f.options().flavor.is_oxfmt()
|
||||
|| f.is_quiet()
|
||||
if f.is_quiet()
|
||||
|| !f.options().flavor.is_oxfmt()
|
||||
|| matches!(body.kind(), StmtKind::Return(_) | StmtKind::Throw(_))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let span = body.span();
|
||||
let mut end = span.end.saturating_sub(1);
|
||||
if f.source_text().text_for(&Span::new(end, span.end)) != b";" {
|
||||
return false;
|
||||
}
|
||||
for comment in f.comments().comments_in(span).iter().rev() {
|
||||
let between = f
|
||||
.source_text()
|
||||
.text_for(&Span::new(comment.end().min(end), end));
|
||||
if !between.trim_ascii().is_empty() {
|
||||
return false;
|
||||
}
|
||||
if comment.is_line() && !comment.preceded_by_newline() {
|
||||
return true;
|
||||
}
|
||||
end = comment.start();
|
||||
}
|
||||
false
|
||||
f.file().without_semicolon(span).end < span.end
|
||||
&& (f.comments().comments_directly_before(span.end - 1).iter())
|
||||
.any(|comment| comment.is_line() && !comment.has_newline_before())
|
||||
}
|
||||
|
||||
struct FormatBodyAndItsComments<'a>(Stmt<'a>);
|
||||
|
||||
impl<'a> Format<'a> for FormatBodyAndItsComments<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
let body = self.0;
|
||||
if f.is_quiet() {
|
||||
return write!(f, body);
|
||||
}
|
||||
let alternate = match body.ast_parent() {
|
||||
AstNodes::IfStatement(parent) if parent.consequent() == Some(body) => {
|
||||
parent.alternate()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let Some(alternate) = alternate else {
|
||||
return write!(f, body);
|
||||
};
|
||||
|
||||
// if (condition)
|
||||
// statement; // comment1
|
||||
// // comment2
|
||||
// else {}
|
||||
//
|
||||
// `comment1` trails the statement, `comment2` is left for the `else`.
|
||||
if matches!(body.kind(), StmtKind::Block(_)) {
|
||||
return write!(f, FormatNodeWithoutTrailingComments(&body));
|
||||
}
|
||||
let end = f.comments().without_semicolon(body.span()).end;
|
||||
// `statement; // prettier-ignore` has to see its comment.
|
||||
// The `;` can be behind the comment.
|
||||
let limit = match [body.span().end, end]
|
||||
.into_iter()
|
||||
.find(|&at| f.comments().has_trailing_suppression_comment(at))
|
||||
{
|
||||
Some(at) => f
|
||||
.comments()
|
||||
.end_of_line_comments_after(at)
|
||||
.last()
|
||||
.map_or(end, |last| last.span.end),
|
||||
None => end,
|
||||
};
|
||||
let previous_limit = f.comments_mut().limit_comments_up_to(limit);
|
||||
write!(f, body);
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
// They are written before the `else`, outside of the group that this is in.
|
||||
if comments_stay_between_head_and_body(f) {
|
||||
return;
|
||||
}
|
||||
let comments = comments_before_else(body, alternate, f);
|
||||
let count = comments
|
||||
.iter()
|
||||
.take_while(|comment| {
|
||||
!comment.is_multiline_block()
|
||||
&& !f
|
||||
.source_text()
|
||||
.contains_newline(Span::before(end, comment.span))
|
||||
})
|
||||
.count();
|
||||
write!(
|
||||
f,
|
||||
FormatTrailingComments::Comments(comments.get(..count).unwrap_or_default())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The comments that are not printed yet between `consequent` and the `else` after it.
|
||||
pub(crate) fn comments_before_else<'a>(
|
||||
consequent: Stmt<'a>,
|
||||
alternate: Stmt<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> &'a [Comment] {
|
||||
let comments = f.comments().comments_before(alternate.span().start);
|
||||
let mut position = consequent.span().end;
|
||||
let count = comments
|
||||
/// Splits `comments`, which follow something, into those on its line and the rest.
|
||||
fn split_at_first_own_line(comments: CommentList<'_>) -> (CommentList<'_>, CommentList<'_>) {
|
||||
let same_line_count = comments
|
||||
.iter()
|
||||
.take_while(|comment| {
|
||||
let gap = f
|
||||
.source_text()
|
||||
.text_for(&Span::before(position, comment.span));
|
||||
position = comment.span.end;
|
||||
!bun_core::strings::contains(gap, b"else")
|
||||
})
|
||||
.take_while(|comment| !comment.has_newline_before())
|
||||
.count();
|
||||
comments.get(..count).unwrap_or_default()
|
||||
comments.split_at(same_line_count)
|
||||
}
|
||||
|
||||
/// An empty line if there is one before `comment`, which starts its line. Otherwise a line break.
|
||||
fn write_line_break_before(comment: Comment<'_>, f: &mut Formatter<'_>) {
|
||||
let start = comment.span().start as usize;
|
||||
match crate::text::is_previous_line_empty(f.source_text().as_bytes(), start) {
|
||||
true => write!(f, empty_line()),
|
||||
false => write!(f, hard_line_break()),
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_stay_between_head_and_body`]: what separates a head from `body`. A line break if the first leading
|
||||
/// comment of the body starts its line, otherwise a space.
|
||||
///
|
||||
/// ```js
|
||||
/// while (a)
|
||||
/// // comment
|
||||
/// {}
|
||||
/// ```
|
||||
pub(crate) fn write_space_before_body<'a>(body: AstNodes<'a>, f: &mut Formatter<'a>) {
|
||||
let starts_on_new_line = !f.is_quiet()
|
||||
&& comments_stay_between_head_and_body(f)
|
||||
&& (f.comments().of_node(body).leading.first())
|
||||
.is_some_and(|comment| comment.has_newline_before());
|
||||
match starts_on_new_line {
|
||||
true => write!(f, hard_line_break()),
|
||||
false => write!(f, space()),
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_stay_between_head_and_body`]: writes the comments between the end of a block or a statement and the
|
||||
/// keyword that continues it (`else`, `catch`, `finally`, `while`). The ones on the line of the block trail it, the others
|
||||
/// keep their lines. Returns whether the caller still has to separate the keyword from what is before it.
|
||||
///
|
||||
/// `trailing`: the trailing comments of the block. `others`: the comments after them, which something else owns.
|
||||
///
|
||||
/// ```js
|
||||
/// if (a) {
|
||||
/// } // comment
|
||||
/// // comment
|
||||
/// else {
|
||||
/// }
|
||||
/// ```
|
||||
pub(crate) fn write_comments_before_keyword<'a>(
|
||||
trailing: CommentList<'a>,
|
||||
others: CommentList<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
let needs_separator = write_run_of_comments_before_keyword(trailing, f);
|
||||
match others.is_empty() {
|
||||
true => needs_separator,
|
||||
false => write_run_of_comments_before_keyword(others, f),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_run_of_comments_before_keyword<'a>(
|
||||
comments: CommentList<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
let (same_line, own_line) = split_at_first_own_line(comments);
|
||||
write!(f, FormatTrailingComments::Comments(same_line));
|
||||
if let Some(first) = own_line.first() {
|
||||
write_line_break_before(first, f);
|
||||
write!(f, FormatLeadingComments::Comments(own_line));
|
||||
false
|
||||
} else if same_line.last().is_some_and(|comment| comment.is_line()) {
|
||||
write!(f, hard_line_break());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`comments_stay_between_head_and_body`]: writes `comments`, which are before a `;` or a `:` that the caller writes
|
||||
/// next. The ones on the line of what is before them trail it, the others keep their lines. Returns whether a line break
|
||||
/// has to follow the token, because a line comment is waiting for the end of the line.
|
||||
///
|
||||
/// ```js
|
||||
/// case a
|
||||
/// // comment
|
||||
/// : {
|
||||
/// }
|
||||
/// ```
|
||||
pub(crate) fn write_comments_before_token<'a>(
|
||||
comments: CommentList<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
) -> bool {
|
||||
let (same_line, own_line) = split_at_first_own_line(comments);
|
||||
write!(f, FormatTrailingComments::Comments(same_line));
|
||||
for comment in own_line {
|
||||
if f.comments_mut().mark_printed(comment) {
|
||||
write_line_break_before(comment, f);
|
||||
write!(f, [comment, comment.is_line().then_some(hard_line_break())]);
|
||||
}
|
||||
}
|
||||
own_line.is_empty() && same_line.last().is_some_and(|comment| comment.is_line())
|
||||
}
|
||||
@@ -16,7 +16,7 @@ fn text_over_several_lines_breaks_groups(f: &Formatter<'_>) -> bool {
|
||||
impl<'a> Format<'a> for FormatSuppressedNode {
|
||||
fn fmt(&self, f: &mut Formatter<'a>) {
|
||||
write_source(self.0, !text_over_several_lines_breaks_groups(f), f);
|
||||
f.comments_mut().skip_comments_before(self.0.end);
|
||||
f.comments_mut().mark_printed_in(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! and not what is in it: it asks [`is_cast_target`].
|
||||
|
||||
use super::suppressed::FormatSuppressedNode;
|
||||
use crate::js::format::write_trailing_comments_of;
|
||||
use crate::js::comments::OwnedComments;
|
||||
use crate::js::print::jsx::ignored_jsx_gets_no_parentheses;
|
||||
use crate::prelude::*;
|
||||
use crate::{format_args, write};
|
||||
use smallvec::SmallVec;
|
||||
@@ -25,20 +26,80 @@ pub(crate) fn is_cast_target_that_hides_its_kind<'a>(e: Expr<'a>, f: &Formatter<
|
||||
}
|
||||
|
||||
/// The parentheses around `e` that stay, from the outside in.
|
||||
fn cast_parentheses<'a>(e: Expr<'a>, f: &Formatter<'a>) -> SmallVec<[Span; 2]> {
|
||||
pub(crate) fn cast_parentheses<'a>(e: Expr<'a>, f: &Formatter<'a>) -> SmallVec<[Span; 2]> {
|
||||
e.parens()
|
||||
.rev()
|
||||
.filter(|it| f.comments().is_cast_parenthesis(it.start))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// If `e` is in parentheses that stay, writes them with the comments around them, and `e` in them
|
||||
/// with `write_target`. Returns whether it has.
|
||||
/// The comments that are written before and after `e`. If `e` is in type cast parentheses, they are those of the outermost
|
||||
/// `ParenthesizedExpression`. If `e` is an optional chain, they are those of the member access or the call, also where Prettier
|
||||
/// has a `ChainExpression` around that. To ask what Prettier's `hasComment(node)` asks, use [`comments_of_outermost_node`].
|
||||
pub(crate) fn comments_of_expression<'a>(e: Expr<'a>, f: &Formatter<'a>) -> OwnedComments<'a> {
|
||||
if f.comments().has_type_cast_comments()
|
||||
&& e.is_parenthesized()
|
||||
&& let Some(outermost) = cast_parentheses(e, f).first()
|
||||
{
|
||||
return f.comments().of_cast_parens(outermost.start);
|
||||
}
|
||||
f.comments().of_node(e.as_chain_element())
|
||||
}
|
||||
|
||||
/// typescript-estree wraps an optional chain in a `ChainExpression`. Prettier attaches comments to the member access or the
|
||||
/// call in it, so `hasComment(node)` is false for the node in the place of `e`. For oxfmt the comments count as those of `e`.
|
||||
pub(crate) fn is_chain_expression_without_comments<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
e.is_chain_root()
|
||||
&& !f.context().has_tree_of_babel()
|
||||
&& !f.options().flavor.is_oxfmt()
|
||||
&& !is_cast_target(e, f)
|
||||
}
|
||||
|
||||
/// Prettier's `hasComment(node)`.
|
||||
///
|
||||
/// `node`: `e`, or the `ChainExpression` around it.
|
||||
/// This and the functions below can be asked about the expression that is being written. Its leading and trailing comments
|
||||
/// are outside of it, so [`Formatter::is_quiet`] says nothing about them.
|
||||
pub(crate) fn has_comments<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
!comments_of_outermost_node(e, f).is_empty()
|
||||
}
|
||||
|
||||
/// `node.comments` of the node that Prettier has in the place of `e`. The same as [`comments_of_expression`], except for an
|
||||
/// optional chain with typescript-estree: the node is a `ChainExpression` then, which has no comments.
|
||||
pub(crate) fn comments_of_outermost_node<'a>(e: Expr<'a>, f: &Formatter<'a>) -> OwnedComments<'a> {
|
||||
match is_chain_expression_without_comments(e, f) {
|
||||
true => OwnedComments::NONE,
|
||||
false => comments_of_expression(e, f),
|
||||
}
|
||||
}
|
||||
|
||||
/// `!a(/* comment */)`: the comment is in `a()`. For Prettier `a()` has a comment. oxfmt only looks at the comments before and
|
||||
/// after it.
|
||||
fn comments_inside_count(f: &Formatter<'_>) -> bool {
|
||||
!f.options().flavor.is_oxfmt()
|
||||
}
|
||||
|
||||
/// Prettier's `hasComment(node)`, where oxfmt asks for less: see [`comments_inside_count`].
|
||||
pub(crate) fn has_comments_that_count<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
let comments = comments_of_outermost_node(e, f);
|
||||
!comments.leading.is_empty()
|
||||
|| !comments.trailing.is_empty()
|
||||
|| (!comments.dangling.is_empty() && comments_inside_count(f))
|
||||
}
|
||||
|
||||
/// Prettier's `hasLeadingOwnLineComment`: whether a leading comment of `e` is the last thing on its line. A JSX element
|
||||
/// writes parentheses around its comments itself, unless it is ignored.
|
||||
pub(crate) fn has_leading_own_line_comment<'a>(e: Expr<'a>, f: &Formatter<'a>) -> bool {
|
||||
if e.tag() == ExprTag::Jsx && !is_cast_target(e, f) {
|
||||
return ignored_jsx_gets_no_parentheses(f) && f.comments().is_ignored(e.as_ast_nodes());
|
||||
}
|
||||
let comments = comments_of_outermost_node(e, f).leading;
|
||||
comments.iter().any(|comment| comment.has_newline_after())
|
||||
}
|
||||
|
||||
/// If `e` is in parentheses that stay, writes them with the comments they own, and `e` in them with `write_target`.
|
||||
/// Returns whether it has.
|
||||
pub(crate) fn write_type_casts<'a>(
|
||||
e: Expr<'a>,
|
||||
node: AstNodes<'a>,
|
||||
f: &mut Formatter<'a>,
|
||||
write_target: &dyn Fn(&mut Formatter<'a>),
|
||||
) -> bool {
|
||||
@@ -46,28 +107,19 @@ pub(crate) fn write_type_casts<'a>(
|
||||
return false;
|
||||
}
|
||||
let parentheses = cast_parentheses(e, f);
|
||||
let Some(&outermost) = parentheses.first() else {
|
||||
if parentheses.is_empty() {
|
||||
return false;
|
||||
};
|
||||
let is_suppressed = f.comments().is_suppressed(outermost.start)
|
||||
|| f.comments().has_trailing_suppression_comment(outermost.end);
|
||||
write!(f, format_leading_comments(outermost));
|
||||
match is_suppressed {
|
||||
true => write!(f, FormatSuppressedNode(outermost)),
|
||||
false => write!(
|
||||
f,
|
||||
FormatParenthesizedExpression {
|
||||
target: e,
|
||||
parentheses: &parentheses,
|
||||
write_target,
|
||||
}
|
||||
),
|
||||
}
|
||||
write_trailing_comments_of(node, f);
|
||||
FormatParenthesizedExpression {
|
||||
target: e,
|
||||
parentheses: &parentheses,
|
||||
write_target,
|
||||
}
|
||||
.fmt(f);
|
||||
true
|
||||
}
|
||||
|
||||
/// Prettier's `ParenthesizedExpression`s around `target`, without the comments around the outermost.
|
||||
/// Prettier's `ParenthesizedExpression`s around `target`, from the outside in, each with the comments it owns.
|
||||
struct FormatParenthesizedExpression<'a, 'b> {
|
||||
target: Expr<'a>,
|
||||
parentheses: &'b [Span],
|
||||
@@ -83,31 +135,30 @@ impl<'a> Format<'a> for FormatParenthesizedExpression<'a, '_> {
|
||||
if !f.context_mut().has_stack_left() {
|
||||
return;
|
||||
}
|
||||
// The comments in the parentheses are written in them, also where those after the
|
||||
// expression are left to somebody else.
|
||||
let previous_limit = f.comments_mut().show_comments_up_to(span.end);
|
||||
let target = self.target.span();
|
||||
let skip_trailing = f.context_mut().take_skip_trailing_comments();
|
||||
let owned = f.comments().of_cast_parens(span.start);
|
||||
FormatLeadingComments::Comments(owned.leading).fmt(f);
|
||||
if owned.is_ignored {
|
||||
FormatSuppressedNode(span).fmt(f);
|
||||
} else {
|
||||
self.write_parentheses(inner, f);
|
||||
}
|
||||
if !skip_trailing {
|
||||
FormatTrailingComments::Comments(owned.trailing).fmt(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FormatParenthesizedExpression<'a, '_> {
|
||||
fn write_parentheses(&self, inner: &[Span], f: &mut Formatter<'a>) {
|
||||
let comments = f.comments().of_node(self.target.as_chain_element());
|
||||
let hugs = inner.is_empty()
|
||||
&& matches!(self.target.kind(), ExprKind::Object(_) | ExprKind::Array(_))
|
||||
&& !f
|
||||
.comments()
|
||||
.has_comment_in_span(Span::before(span.start, target))
|
||||
&& !f
|
||||
.comments()
|
||||
.has_comment_in_span(Span::after(target, span.end));
|
||||
let content = format_with(|f| {
|
||||
if let Some(&next) = inner.first() {
|
||||
write!(f, format_leading_comments(next));
|
||||
}
|
||||
let expression = FormatParenthesizedExpression {
|
||||
parentheses: inner,
|
||||
..*self
|
||||
};
|
||||
write!(f, expression);
|
||||
// Nothing follows the expression in here, so whatever is left trails it.
|
||||
let comments = f.comments().comments_before(span.end);
|
||||
write!(f, FormatTrailingComments::Comments(comments));
|
||||
});
|
||||
&& comments.is_empty();
|
||||
let content = FormatParenthesizedExpression {
|
||||
parentheses: inner,
|
||||
..*self
|
||||
};
|
||||
match hugs {
|
||||
true => write!(f, ["(", content, ")"]),
|
||||
false => write!(
|
||||
@@ -115,6 +166,5 @@ impl<'a> Format<'a> for FormatParenthesizedExpression<'a, '_> {
|
||||
group(&format_args!("(", soft_block_indent(&content), ")"))
|
||||
),
|
||||
}
|
||||
f.comments_mut().restore_view_limit(previous_limit);
|
||||
}
|
||||
}
|
||||
@@ -32,83 +32,29 @@ pub(crate) fn without_lone_operator(mut ty: TypeNode<'_>) -> TypeNode<'_> {
|
||||
ty
|
||||
}
|
||||
|
||||
/// Those at the start of `comments` for which Prettier's `isEndOfLineComment` holds and
|
||||
/// `isOwnLineComment` does not: there is code before them on their line, and none after them.
|
||||
pub(crate) fn end_of_line_comments(comments: &[Comment]) -> &[Comment] {
|
||||
let mut count = 0;
|
||||
for (index, comment) in comments.iter().enumerate() {
|
||||
if comment.preceded_by_newline() {
|
||||
break;
|
||||
}
|
||||
if comment.followed_by_newline() {
|
||||
count = index + 1;
|
||||
}
|
||||
}
|
||||
&comments[..count]
|
||||
/// Prettier's `hasLeadingOwnLineComment`: a leading comment of `node` is the last thing on its line.
|
||||
pub(crate) fn has_leading_own_line_comment<'a>(node: AstNodes<'a>, f: &Formatter<'a>) -> bool {
|
||||
!f.is_quiet()
|
||||
&& (f.comments().of_node(node).leading.iter()).any(|comment| comment.has_newline_after())
|
||||
}
|
||||
|
||||
/// The comments before the union `ty`: those that lead the union, and those that lead its first
|
||||
/// type.
|
||||
/// Prettier's `shouldHugUnionType`: `types` are one object type or type reference, and otherwise only `null` and `void`.
|
||||
///
|
||||
/// Prettier's `shouldAttachToUnionTypeFirstElement`: a block comment on one line, with nothing but
|
||||
/// blanks and other comments between it and the union.
|
||||
pub(crate) fn union_leading_comments<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
f: &Formatter<'a>,
|
||||
) -> (&'a [Comment], &'a [Comment]) {
|
||||
if f.is_quiet() {
|
||||
return (&[], &[]);
|
||||
}
|
||||
let comments = f.comments().comments_before(ty.span().start);
|
||||
let mut end = ty.span().start;
|
||||
let mut at = comments.len();
|
||||
for comment in comments.iter().rev() {
|
||||
let is_adjacent = comment.is_block()
|
||||
&& !comment.is_multiline_block()
|
||||
&& !f.comments().is_suppression_comment(comment)
|
||||
&& f.source_text()
|
||||
.all_bytes(Span::after(comment.span, end), |b| {
|
||||
matches!(b, b' ' | b'\t')
|
||||
});
|
||||
if !is_adjacent {
|
||||
break;
|
||||
}
|
||||
end = comment.span.start;
|
||||
at -= 1;
|
||||
}
|
||||
comments.split_at(at)
|
||||
}
|
||||
|
||||
/// Prettier's `shouldHugUnionType` for the union `ty`: it is one object type or name, and otherwise
|
||||
/// only `null` and `void`.
|
||||
pub(crate) fn should_hug_type<'a>(
|
||||
ty: TypeNode<'a>,
|
||||
types: List<'a, TypeNode<'a>>,
|
||||
f: &Formatter<'a>,
|
||||
) -> bool {
|
||||
/// ```ts
|
||||
/// let a: {
|
||||
/// b: string;
|
||||
/// } | null;
|
||||
/// ```
|
||||
pub(crate) fn should_hug_type<'a>(types: List<'a, TypeNode<'a>>, f: &Formatter<'a>) -> bool {
|
||||
let is_object_like =
|
||||
|t: &TypeNode<'a>| matches!(t.kind(), TypeKind::Object(_) | TypeKind::Ref { .. });
|
||||
let Some(object_type) = types.iter().find(is_object_like) else {
|
||||
return false;
|
||||
};
|
||||
if !types.iter().all(|t| {
|
||||
types.iter().all(|t| {
|
||||
t == object_type || matches!(t.kind(), TypeKind::Keyword(Keyword::Void | Keyword::Null))
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
// Not if one of the types has a comment.
|
||||
if f.is_quiet() {
|
||||
return true;
|
||||
}
|
||||
if !union_leading_comments(ty, f).1.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let mut start = ty.span().start;
|
||||
for t in types {
|
||||
if f.comments().has_comment_in_range(start, t.span().start) {
|
||||
return false;
|
||||
}
|
||||
start = t.span().end;
|
||||
}
|
||||
true
|
||||
}) && (f.is_quiet()
|
||||
|| !types
|
||||
.iter()
|
||||
.any(|t| f.comments().has_comments(t.as_ast_nodes())))
|
||||
}
|
||||
@@ -99,6 +99,18 @@ pub struct FormatOptions {
|
||||
pub tailwind: Option<std::sync::Arc<crate::tailwind::Tailwind>>,
|
||||
/// Format TOML even in Prettier's flavor. Only the caller that detects file types reads this.
|
||||
pub toml: bool,
|
||||
/// For the tests: what to output instead of the formatted text.
|
||||
pub debug_comments: DebugComments,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum DebugComments {
|
||||
#[default]
|
||||
Off,
|
||||
/// Every comment and the node that owns it.
|
||||
Owners,
|
||||
/// The comments that no printer has printed, which is a bug.
|
||||
Unprinted,
|
||||
}
|
||||
|
||||
/// Formats JavaScript or TypeScript, which this crate cannot parse by itself. It is given the name of a file that
|
||||
|
||||
+8
-30
@@ -10,10 +10,8 @@ use crate::ir::prelude::{Format, Formatter, hard_line_break};
|
||||
use crate::js::ast_nodes::{
|
||||
AstNodes, ExpressionStatement, Program, node_as_ast_nodes, type_parameters_of,
|
||||
};
|
||||
use crate::js::comments::{self, Comment, Comments};
|
||||
use crate::js::print::program::ends_before_semicolon;
|
||||
use crate::js::source_text::SourceText;
|
||||
use crate::options::{Flavor, LineEnding};
|
||||
use crate::options::LineEnding;
|
||||
use crate::text::BOM;
|
||||
use crate::{FormatError, FormatOptions, Scratch};
|
||||
use bun_lint::ast::{File, FnBody, FnKind, Node, Stmt, StmtKind};
|
||||
@@ -59,7 +57,7 @@ pub fn format_with_cursor<'a>(
|
||||
return Err(FormatError::SyntaxError);
|
||||
}
|
||||
|
||||
let range = calculate_range(file, options.flavor, start as u32, end as u32)
|
||||
let range = calculate_range(file, start as u32, end as u32)
|
||||
.unwrap_or_else(|| Span::empty(first as u32));
|
||||
let (start, end) = (range.start as usize, range.end as usize);
|
||||
let (Some(before), Some(slice), Some(after)) =
|
||||
@@ -211,16 +209,6 @@ pub(crate) fn write_with_line_ending(mut text: &[u8], line_ending: &[u8], out: &
|
||||
out.extend_from_slice(text);
|
||||
}
|
||||
|
||||
fn comments_of<'a>(file: &'a File<'a>, flavor: Flavor) -> &'a [Comment] {
|
||||
let comments = file.extension(|| {
|
||||
let mut comments = Vec::new();
|
||||
let parsed_by = comments::ParsedBy::WhatItsNameSays;
|
||||
comments::collect(file, flavor, parsed_by, &mut comments);
|
||||
comments
|
||||
});
|
||||
comments.map_or(&[][..], |comments: &Vec<Comment>| comments)
|
||||
}
|
||||
|
||||
/// Writes the document of `file` so that every line is indented by `alignment` columns more.
|
||||
/// Prettier's `addAlignmentToDoc`.
|
||||
fn write_aligned<'a>(file: &'a File<'a>, alignment: usize, f: &mut Formatter<'a>) {
|
||||
@@ -296,7 +284,7 @@ fn full_span(node: AstNodes<'_>) -> Span {
|
||||
}
|
||||
|
||||
/// Prettier's `locEnd`: a statement ends before its `;`.
|
||||
fn loc_end<'a>(node: AstNodes<'a>, comments: &Comments<'a>) -> u32 {
|
||||
fn loc_end<'a>(node: AstNodes<'a>) -> u32 {
|
||||
use AstNodes as N;
|
||||
let span = node.span();
|
||||
let ends_before_semicolon = match node {
|
||||
@@ -323,7 +311,7 @@ fn loc_end<'a>(node: AstNodes<'a>, comments: &Comments<'a>) -> u32 {
|
||||
_ => false,
|
||||
};
|
||||
match ends_before_semicolon {
|
||||
true => comments.without_semicolon(span).end,
|
||||
true => node.raw().file().without_semicolon(span).end,
|
||||
false => span.end,
|
||||
}
|
||||
}
|
||||
@@ -459,7 +447,6 @@ fn find_sibling_ancestors<'a>(
|
||||
file: AstNodes<'a>,
|
||||
start_path: &[AstNodes<'a>],
|
||||
end_path: &[AstNodes<'a>],
|
||||
comments: &Comments<'a>,
|
||||
) -> Option<(AstNodes<'a>, AstNodes<'a>)> {
|
||||
let (&(mut start_node), start_ancestors) = start_path.split_first()?;
|
||||
let (&(mut end_node), end_ancestors) = end_path.split_first()?;
|
||||
@@ -487,11 +474,8 @@ fn find_sibling_ancestors<'a>(
|
||||
{
|
||||
end_node = ancestor;
|
||||
}
|
||||
let end = loc_end(end_node, comments);
|
||||
for &ancestor in start_ancestors
|
||||
.iter()
|
||||
.take_while(|&&it| loc_end(it, comments) <= end)
|
||||
{
|
||||
let end = loc_end(end_node);
|
||||
for &ancestor in start_ancestors.iter().take_while(|&&it| loc_end(it) <= end) {
|
||||
start_node = ancestor;
|
||||
if start_node == end_node {
|
||||
break;
|
||||
@@ -501,12 +485,7 @@ fn find_sibling_ancestors<'a>(
|
||||
}
|
||||
|
||||
/// Prettier's `calculateRange`: what to format so that everything from `start` to `end` is.
|
||||
fn calculate_range<'a>(
|
||||
file: &'a File<'a>,
|
||||
flavor: Flavor,
|
||||
mut start: u32,
|
||||
mut end: u32,
|
||||
) -> Option<Span> {
|
||||
fn calculate_range<'a>(file: &'a File<'a>, mut start: u32, mut end: u32) -> Option<Span> {
|
||||
let text = file.text();
|
||||
// The range is narrowed so that it starts and ends with something.
|
||||
let selected = text.get(start as usize..end as usize)?;
|
||||
@@ -522,9 +501,8 @@ fn calculate_range<'a>(
|
||||
true => start_path.clone(),
|
||||
false => find_node_at_offset(file, end, Edge::End)?,
|
||||
};
|
||||
let comments = Comments::new(SourceText::new(text), comments_of(file, flavor));
|
||||
let root = AstNodes::Program(Program(file));
|
||||
let (start_node, end_node) = find_sibling_ancestors(root, &start_path, &end_path, &comments)?;
|
||||
let (start_node, end_node) = find_sibling_ancestors(root, &start_path, &end_path)?;
|
||||
Some(Span::new(
|
||||
loc_start(start_node).min(loc_start(end_node)),
|
||||
start_node.span().end.max(end_node.span().end),
|
||||
|
||||
+29
-28
@@ -74,9 +74,9 @@ fn starts_like_a_tag<'a>(mut e: Expr<'a>, brace: Brace, f: &Formatter<'a>) -> bo
|
||||
/// waits for the end of a line: `a[0\n/* b */\n]` is `a[0]\n/* b */`. Any other stays before what follows it, unless that goes,
|
||||
/// like parentheses and the `;` of a statement, or is written before it: `a as B[/* c */]` is `a as B[] /* c */`. So it can if
|
||||
/// nothing follows it that does not close something.
|
||||
fn can_end_with_comment(text: &[u8], comments: &[Comment]) -> bool {
|
||||
fn can_end_with_comment(text: &[u8], comments: CommentList<'_>) -> bool {
|
||||
// They are in the order in which they are printed.
|
||||
let Some(end) = comments.iter().map(|it| it.span.end).max() else {
|
||||
let Some(end) = comments.iter().map(|it| it.span().end).max() else {
|
||||
return false;
|
||||
};
|
||||
let mut behind = text.get(end as usize..).unwrap_or_default();
|
||||
@@ -89,7 +89,7 @@ fn can_end_with_comment(text: &[u8], comments: &[Comment]) -> bool {
|
||||
}
|
||||
behind.is_empty()
|
||||
|| (comments.iter())
|
||||
.any(|it| it.is_line() || it.preceded_by_newline() || it.followed_by_newline())
|
||||
.any(|it| it.is_line() || it.has_newline_before() || it.has_newline_after())
|
||||
}
|
||||
|
||||
/// Whether `e` can end with `as B` once it is printed: the parentheses go in `() => (a as B)`.
|
||||
@@ -153,7 +153,7 @@ fn write_js_expression(
|
||||
let Some(e) = js::expression_of(file) else {
|
||||
return false;
|
||||
};
|
||||
let comments = f.comments().unprinted_comments();
|
||||
let comments = f.comments().all();
|
||||
// As it is written, there is none: `{#each a[0 // b\n]}`. Nor is there an `as` that Svelte takes for that of the
|
||||
// block, as there is in `{#each () => (a as B)}` without the parentheses.
|
||||
if js.is_without_comment_behind
|
||||
@@ -163,27 +163,25 @@ fn write_js_expression(
|
||||
}
|
||||
// One on a line of its own is printed at the end of a line too, and so is all that follows one that is.
|
||||
ends_with_line_comment = (comments.iter())
|
||||
.filter(|it| it.span.start >= e.span().end)
|
||||
.any(|it| it.is_line() || it.preceded_by_newline());
|
||||
.filter(|it| it.span().start >= e.span().end)
|
||||
.any(|it| it.is_line() || it.has_newline_before());
|
||||
let is_in_parentheses =
|
||||
(js.follows).is_some_and(|brace| starts_like_a_tag(e, brace, f));
|
||||
let content = format_with(|f| {
|
||||
if is_in_parentheses {
|
||||
write!(f, ["(", e, ")"]);
|
||||
let rest = f.comments().unprinted_comments();
|
||||
return write!(f, FormatTrailingComments::Comments(rest));
|
||||
return write!(f, ["(", e, ")"]);
|
||||
}
|
||||
match js.is_without_parentheses {
|
||||
// `removeParentheses` takes a block comment at the start for one that never ends, and leaves the
|
||||
// parentheses, which Svelte refuses around the two functions of a `bind:`.
|
||||
true => {
|
||||
write!(f, FormatLeadingComments::Node(e.span()));
|
||||
let node = e.as_chain_element();
|
||||
write!(f, leading_comments(node));
|
||||
write_expression(e, ExprOptions::None, f);
|
||||
write!(f, trailing_comments(node));
|
||||
}
|
||||
false => write!(f, e),
|
||||
}
|
||||
let rest = f.comments().unprinted_comments();
|
||||
write!(f, FormatTrailingComments::Comments(rest));
|
||||
});
|
||||
// Without its line breaks, the plugin has what follows a `//` before it: `{#if a && b} // c`.
|
||||
match js.is_on_one_line && !comments.iter().any(|it| it.is_line()) {
|
||||
@@ -219,19 +217,19 @@ fn write_js_function(
|
||||
};
|
||||
// Such a comment is printed behind the name, where Svelte wants the `(`: `{#snippet a /* b */(c)}`.
|
||||
let first = function.params_with_this().next();
|
||||
let is_behind_name = |it: &Comment| {
|
||||
let is_behind_name = |it: Comment<'_>| {
|
||||
function
|
||||
.open_paren()
|
||||
.is_some_and(|open| it.span.start < open)
|
||||
.is_some_and(|open| it.span().start < open)
|
||||
|| (!it.is_line()
|
||||
// One on a line of its own is printed before the parameter.
|
||||
&& !it.preceded_by_newline()
|
||||
&& (it.followed_by_newline() || {
|
||||
let behind = file.text().get(it.span.end as usize..);
|
||||
&& !it.has_newline_before()
|
||||
&& (it.has_newline_after() || {
|
||||
let behind = file.text().get(it.span().end as usize..);
|
||||
strings::trim_js_whitespace_start(behind.unwrap_or_default())
|
||||
.starts_with(b"//")
|
||||
})
|
||||
&& first.is_some_and(|first| it.span.start < first.span().start))
|
||||
&& first.is_some_and(|first| it.span().start < first.span().start))
|
||||
};
|
||||
// Where the only parameter is hugged no line ends before the `)`, and a comment that waits for the end of one is
|
||||
// printed behind that, where Svelte wants the `}`: `{#snippet a({ b } = {}) // c\n}`. Between the brackets of the
|
||||
@@ -249,16 +247,16 @@ fn write_js_function(
|
||||
spans
|
||||
})
|
||||
};
|
||||
let is_behind_hugged = |it: &Comment| {
|
||||
let is_in = |part: Span| part.start <= it.span.start && it.span.end <= part.end;
|
||||
(it.is_line() || it.preceded_by_newline())
|
||||
let is_behind_hugged = |it: Comment<'_>| {
|
||||
let is_in = |part: Span| part.start <= it.span().start && it.span().end <= part.end;
|
||||
(it.is_line() || it.has_newline_before())
|
||||
&& hugged.is_some_and(|hugged| {
|
||||
!is_in(hugged.pat().span())
|
||||
&& !hugged.default().is_some_and(|it| is_in(it.span()))
|
||||
&& !{
|
||||
let types = types_with_braces();
|
||||
let behind =
|
||||
types.partition_point(|part| part.start <= it.span.start);
|
||||
types.partition_point(|part| part.start <= it.span().start);
|
||||
behind.checked_sub(1).is_some_and(|at| is_in(types[at]))
|
||||
}
|
||||
})
|
||||
@@ -276,16 +274,19 @@ fn write_js_function(
|
||||
starts
|
||||
})
|
||||
};
|
||||
let is_before_parenthesis = |it: &Comment| {
|
||||
let behind = file.text().get(it.span.end as usize..).unwrap_or_default();
|
||||
let is_before_parenthesis = |it: Comment<'_>| {
|
||||
let behind = file
|
||||
.text()
|
||||
.get(it.span().end as usize..)
|
||||
.unwrap_or_default();
|
||||
let code = strings::trim_js_whitespace_start(behind);
|
||||
let at = it.span.end + (behind.len() - code.len()) as u32;
|
||||
let at = it.span().end + (behind.len() - code.len()) as u32;
|
||||
it.is_line()
|
||||
&& !it.preceded_by_newline()
|
||||
&& !it.has_newline_before()
|
||||
&& code.starts_with(b"(")
|
||||
&& starts().binary_search(&at).is_ok()
|
||||
};
|
||||
let comments = f.comments().unprinted_comments();
|
||||
let comments = f.comments().all();
|
||||
if (comments.iter())
|
||||
.any(|it| is_behind_name(it) || is_behind_hugged(it) || is_before_parenthesis(it))
|
||||
{
|
||||
@@ -318,7 +319,7 @@ fn write_js_statement(
|
||||
return false;
|
||||
};
|
||||
// As it is written, there is none: `{const a = (b /* c */)}`.
|
||||
let comments = f.comments().unprinted_comments();
|
||||
let comments = f.comments().all();
|
||||
if js.is_without_comment_behind && can_end_with_comment(&js.text, comments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -162,7 +162,8 @@ pub fn format(
|
||||
parse: parsers.program,
|
||||
unformatted: &unformatted,
|
||||
};
|
||||
let mut context = JsFormatContext::without_file(text, host.clone(), &[]);
|
||||
let mut context =
|
||||
JsFormatContext::without_file(host.clone(), &crate::js::comments::NO_COMMENTS);
|
||||
context.parse_javascript = parse_javascript;
|
||||
let root = crate::ir::run::write_with(context, text, &mut scratch.document, |f| {
|
||||
let mut writer = Writer::new(f, Some(0), true);
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
use super::Difference;
|
||||
use bun_core::strings;
|
||||
use bun_lint::ast::File;
|
||||
use bun_lint::tokens::{Token, skip_trivia};
|
||||
use bun_lint::tokens::Token;
|
||||
use bun_sema::hir::Comment;
|
||||
|
||||
/// The text of a program, and where its comments are.
|
||||
type Comments<'a> = (&'a [u8], &'a [(u32, u32)]);
|
||||
type Comments<'a> = (&'a [u8], &'a [Comment]);
|
||||
|
||||
#[optimize(size)]
|
||||
fn text_of<'a>(text: &'a [u8], comment: (u32, u32)) -> &'a [u8] {
|
||||
text.get(comment.0 as usize..comment.1 as usize)
|
||||
fn text_of<'a>(text: &'a [u8], comment: Comment) -> &'a [u8] {
|
||||
text.get(comment.pos as usize..comment.end as usize)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ fn compare_in_any_order(before: Comments<'_>, after: Comments<'_>) -> Result<(),
|
||||
let sorted = |(text, comments): Comments<'_>| {
|
||||
let mut all: Vec<(Vec<u8>, u32)> = comments
|
||||
.iter()
|
||||
.map(|&it| (bstr::join(b"\n", lines(text_of(text, it))), it.0))
|
||||
.map(|&it| (bstr::join(b"\n", lines(text_of(text, it))), it.pos))
|
||||
.collect();
|
||||
crate::sort::sort(&mut all[..]);
|
||||
all
|
||||
@@ -137,9 +138,9 @@ fn says_something(inner: &[u8]) -> bool {
|
||||
|
||||
/// The name or the keyword that follows the comment, if it is one that follows.
|
||||
#[optimize(size)]
|
||||
fn word_after(text: &[u8], comment: (u32, u32)) -> &[u8] {
|
||||
fn word_after(text: &[u8], comment: Comment) -> &[u8] {
|
||||
let rest = text
|
||||
.get(skip_trivia(text, comment.1) as usize..)
|
||||
.get(comment.next_token_start as usize..)
|
||||
.unwrap_or_default();
|
||||
let len = rest
|
||||
.iter()
|
||||
@@ -152,7 +153,7 @@ fn word_after(text: &[u8], comment: (u32, u32)) -> &[u8] {
|
||||
/// each is before the same word.
|
||||
#[optimize(size)]
|
||||
fn compare_without_jsdoc(before: Comments<'_>, after: Comments<'_>) -> Result<(), Difference> {
|
||||
type Lists = (Vec<(u32, u32)>, Vec<(u32, u32)>);
|
||||
type Lists = (Vec<Comment>, Vec<Comment>);
|
||||
// Those that are not JSDoc comments, and those that are.
|
||||
let split = |(text, comments): Comments<'_>| -> Lists {
|
||||
comments
|
||||
@@ -177,8 +178,8 @@ fn compare_without_jsdoc(before: Comments<'_>, after: Comments<'_>) -> Result<()
|
||||
if word != word2 && !word.is_empty() && !word2.is_empty() {
|
||||
return Err(Difference::new(
|
||||
"what follows a JSDoc comment",
|
||||
(before.0, p.0),
|
||||
(after.0, q.0),
|
||||
(before.0, p.pos),
|
||||
(after.0, q.pos),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ lists! {
|
||||
import_attributes: (u32, ExprId),
|
||||
deferred_import_calls: (ExprId, u32),
|
||||
import_call_type_args: (ExprId, IdList<TypeNodeId>),
|
||||
comments: (u32, u32),
|
||||
comments: Comment,
|
||||
diagnostics: Diagnostic,
|
||||
specifier_uses: SpecifierUse,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Comments, with the tokens and the nodes around them as the parser recorded them.
|
||||
|
||||
use super::*;
|
||||
use bun_sema::hir::NodeData;
|
||||
|
||||
/// A `//` or `/* */` comment.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Comment<'a> {
|
||||
file: &'a File<'a>,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum NodeOrName<'a> {
|
||||
Node(Node<'a>),
|
||||
/// An identifier or a literal key, given by its start offset.
|
||||
Name(u32),
|
||||
}
|
||||
|
||||
fn node_from_data<'a>(file: &'a File<'a>, data: NodeData) -> Option<Node<'a>> {
|
||||
Some(match data {
|
||||
NodeData::Expr(id) => Expr::new(file, id).into(),
|
||||
NodeData::Stmt(id) => Stmt::new(file, id).into(),
|
||||
NodeData::Type(id) => TypeNode::new(file, id).into(),
|
||||
NodeData::Pat(id) => Pat::new(file, id).into(),
|
||||
NodeData::PatProp(id) => PatProp::new(file, id).into(),
|
||||
NodeData::PatElem(id) => PatElem::new(file, id).into(),
|
||||
NodeData::Param(id) => Param::new(file, id).into(),
|
||||
NodeData::TypeParam(id) => TypeParam::new(file, id).into(),
|
||||
NodeData::Member(id) => Member::new(file, id).into(),
|
||||
NodeData::Prop(id) => Prop::new(file, id).into(),
|
||||
NodeData::VarDecl(id) => VarDecl::new(file, id).into(),
|
||||
NodeData::Case(id) => Case::new(file, id).into(),
|
||||
NodeData::EnumMember(id) => EnumMember::new(file, id).into(),
|
||||
NodeData::ImportSpec(id) => ImportSpec::new(file, id).into(),
|
||||
NodeData::ExportSpec(id) => ExportSpec::new(file, id).into(),
|
||||
NodeData::TupleElem(id) => TupleElem::new(file, id).into(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
impl<'a> Comment<'a> {
|
||||
#[inline]
|
||||
pub fn file(self) -> &'a File<'a> {
|
||||
self.file
|
||||
}
|
||||
|
||||
/// Its index among the comments of the file.
|
||||
#[inline]
|
||||
pub fn id(self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn raw(self) -> hir::Comment {
|
||||
const MISSING: hir::Comment = hir::Comment {
|
||||
pos: 0,
|
||||
end: 0,
|
||||
prev_token_end: 0,
|
||||
next_token_start: 0,
|
||||
is_block: false,
|
||||
is_multiline: false,
|
||||
has_newline_before: false,
|
||||
has_newline_after: false,
|
||||
enclosing_node: hir::NodeOrName::None,
|
||||
preceding_node: hir::NodeOrName::None,
|
||||
following_node: hir::NodeOrName::None,
|
||||
};
|
||||
(self.file.hir.comments.get(self.id as usize)).map_or(MISSING, |comment| *comment)
|
||||
}
|
||||
|
||||
pub fn span(self) -> Span {
|
||||
let raw = self.raw();
|
||||
Span::new(raw.pos, raw.end)
|
||||
}
|
||||
|
||||
/// The source text, with the `//` or the `/*` and `*/`.
|
||||
pub fn text(self) -> &'a [u8] {
|
||||
self.file.slice(self.span())
|
||||
}
|
||||
|
||||
/// The text between the `/*` and the `*/`, or after the `//`, the `<!--` or the `-->`.
|
||||
pub fn content(self) -> &'a [u8] {
|
||||
match self.text() {
|
||||
[b'/', b'*', content @ .., b'*', b'/']
|
||||
| [b'/', b'/', content @ ..]
|
||||
| [b'<', b'!', b'-', b'-', content @ ..]
|
||||
| [b'-', b'-', b'>', content @ ..] => content,
|
||||
text => text,
|
||||
}
|
||||
}
|
||||
|
||||
/// `// comment`
|
||||
pub fn is_line(self) -> bool {
|
||||
!self.raw().is_block
|
||||
}
|
||||
|
||||
/// `/* comment */`
|
||||
pub fn is_block(self) -> bool {
|
||||
self.raw().is_block
|
||||
}
|
||||
|
||||
/// A block comment that spans more than one line.
|
||||
pub fn is_multiline_block(self) -> bool {
|
||||
self.raw().is_multiline
|
||||
}
|
||||
|
||||
/// Whether there is a line break between the comment and the token or comment before it.
|
||||
pub fn has_newline_before(self) -> bool {
|
||||
self.raw().has_newline_before
|
||||
}
|
||||
|
||||
/// Whether there is a line break between the comment and the token or comment after it.
|
||||
pub fn has_newline_after(self) -> bool {
|
||||
self.raw().has_newline_after
|
||||
}
|
||||
|
||||
/// End of the previous token, or 0 if there is none.
|
||||
pub fn prev_token_end(self) -> u32 {
|
||||
self.raw().prev_token_end
|
||||
}
|
||||
|
||||
/// Start of the next token, or the length of the text if there is none.
|
||||
pub fn next_token_start(self) -> u32 {
|
||||
self.raw().next_token_start
|
||||
}
|
||||
|
||||
fn node_or_name(self, raw: hir::NodeOrName) -> Option<NodeOrName<'a>> {
|
||||
match raw {
|
||||
hir::NodeOrName::None => None,
|
||||
hir::NodeOrName::Node(data) => node_from_data(self.file, data).map(NodeOrName::Node),
|
||||
hir::NodeOrName::Name(start) => Some(NodeOrName::Name(start)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The smallest node that contains the comment. `None` at the top level of the file.
|
||||
pub fn enclosing_node(self) -> Option<Node<'a>> {
|
||||
match self.raw().enclosing_node {
|
||||
hir::NodeOrName::Node(data) => node_from_data(self.file, data),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The closest child of the enclosing node that ends before the comment.
|
||||
pub fn preceding_node(self) -> Option<NodeOrName<'a>> {
|
||||
self.node_or_name(self.raw().preceding_node)
|
||||
}
|
||||
|
||||
/// The closest child of the enclosing node that starts after the comment.
|
||||
pub fn following_node(self) -> Option<NodeOrName<'a>> {
|
||||
self.node_or_name(self.raw().following_node)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> File<'a> {
|
||||
/// `span` without the `;` at its end, and without the whitespace and comments before that `;`. `span` is the span of a
|
||||
/// statement or a class member.
|
||||
pub fn without_semicolon(&self, span: Span) -> Span {
|
||||
if span.is_empty() || self.text().get(span.end as usize - 1) != Some(&b';') {
|
||||
return span;
|
||||
}
|
||||
let semicolon = span.end - 1;
|
||||
let detached = self.hir.detached_semicolons;
|
||||
match detached.binary_search_by_key(&semicolon, |entry| entry.0) {
|
||||
Ok(index) => Span::new(span.start, detached[index].1.max(span.start)),
|
||||
Err(_) => Span::new(span.start, semicolon),
|
||||
}
|
||||
}
|
||||
|
||||
/// The comment with that id.
|
||||
#[inline]
|
||||
pub fn comment(&'a self, id: u32) -> Comment<'a> {
|
||||
Comment { file: self, id }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn comment_count(&self) -> usize {
|
||||
self.hir.comments.len()
|
||||
}
|
||||
|
||||
/// All comments in source order. The nodes around them are only known if the file was parsed with `attach_comments`.
|
||||
pub fn comments_by_id(&'a self) -> impl ExactSizeIterator<Item = Comment<'a>> {
|
||||
(0..self.hir.comments.len() as u32).map(move |id| Comment { file: self, id })
|
||||
}
|
||||
}
|
||||
@@ -928,6 +928,7 @@ impl<'a> Class<'a> {
|
||||
pub fn body_span(self) -> Span {
|
||||
// The last thing before the `{`.
|
||||
let head_end = (self.implements().last().map(|ty| ty.span().end))
|
||||
.or_else(|| self.flow_mixins().last().map(|ty| ty.span().end))
|
||||
.or_else(|| self.extends_args().angle_brackets_span().map(|it| it.end))
|
||||
.or_else(|| self.extends().map(|e| e.outer_span().end))
|
||||
.or_else(|| self.type_params().angle_brackets_span().map(|it| it.end))
|
||||
|
||||
@@ -972,8 +972,28 @@ impl<'a> Expr<'a> {
|
||||
/// `AssignmentPattern` or `RestElement`. Parentheses make what is in them an ordinary
|
||||
/// expression.
|
||||
pub fn is_assignment_target(self) -> bool {
|
||||
let mut container = None;
|
||||
let answer = self.is_assignment_target_uncached(&mut container);
|
||||
// The answer is the same for the array or the object that it is in, and so for everything else in that. With it
|
||||
// remembered, what is nested does not go up to the root each time.
|
||||
if let Some(container) = container {
|
||||
let last = &self.file.lazy.last_assignment_target;
|
||||
last.set(Some((container, answer)));
|
||||
}
|
||||
answer
|
||||
}
|
||||
|
||||
/// `container`: the array or the object that it is in.
|
||||
#[inline(always)]
|
||||
fn is_assignment_target_uncached(self, container: &mut Option<hir::ExprId>) -> bool {
|
||||
let mut at = self;
|
||||
loop {
|
||||
if container.is_some()
|
||||
&& let Some((last, answer)) = self.file.lazy.last_assignment_target.get()
|
||||
&& last == at.id
|
||||
{
|
||||
return answer;
|
||||
}
|
||||
if at.is_parenthesized() {
|
||||
return false;
|
||||
}
|
||||
@@ -999,6 +1019,7 @@ impl<'a> Expr<'a> {
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
container.get_or_insert(at.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-2
@@ -27,6 +27,7 @@
|
||||
//! - The default of the shorthand `{ a = 1 }` is an [`ExprKind::Assign`] that is the value of the
|
||||
//! property. A [`TupleElem`] is there for every element of a tuple type, also for a plain `T`.
|
||||
|
||||
pub mod comments;
|
||||
mod decl;
|
||||
mod entities;
|
||||
mod expr;
|
||||
@@ -123,10 +124,11 @@ slices! {
|
||||
modifiers_of_params: hir::Span<hir::ModifierId>,
|
||||
modifiers_of_props: (hir::PropId, hir::Span<hir::ModifierId>),
|
||||
with_bodies: (u32, u32),
|
||||
detached_semicolons: (u32, u32),
|
||||
import_attributes: (u32, hir::ExprId),
|
||||
deferred_import_calls: (hir::ExprId, u32),
|
||||
jsdoc_comments: (u32, u32),
|
||||
comments: (u32, u32),
|
||||
comments: hir::Comment,
|
||||
keyword_identifier_positions: u32,
|
||||
mentioned: u64,
|
||||
diagnostics: hir::Diagnostic,
|
||||
@@ -185,8 +187,12 @@ pub(crate) struct Lazy {
|
||||
unicode_escape_range: UnicodeEscapeRange,
|
||||
/// A bit for each expression: it is in parentheses.
|
||||
parenthesized: OnceCell<Box<[u64]>>,
|
||||
/// [`Node::index`]
|
||||
pub(crate) kind_index_offsets: OnceCell<[u32; node::KINDS + 1]>,
|
||||
/// [`Stmt::directive`]
|
||||
pub(crate) directives: Cell<Option<(u32, u32)>>,
|
||||
/// The array or the object of the last expression that [`Expr::is_assignment_target`] was asked about, and the answer.
|
||||
pub(crate) last_assignment_target: Cell<Option<(hir::ExprId, bool)>>,
|
||||
/// For each `a, b` that is the start of a long `a, b, c, ..`: all of that.
|
||||
pub(crate) sequence_roots: OnceCell<rustc_hash::FxHashMap<hir::ExprId, hir::ExprId>>,
|
||||
/// Where the `a.b` of each `<a.b>` and `</a.b>` starts and ends, in order.
|
||||
@@ -476,7 +482,7 @@ impl<'a> File<'a> {
|
||||
///
|
||||
/// Unlike [`skip_trivia_back`](crate::tokens::skip_trivia_back) it is never fooled by what a
|
||||
/// comment or a string contains. Where there is only whitespace in between, it looks at nothing
|
||||
/// else. Where there may be a comment, it asks the tokens of the file.
|
||||
/// else. Where there is a comment, the comment knows. Otherwise it asks the tokens of the file.
|
||||
pub fn end_of_token_before(&'a self, at: u32) -> u32 {
|
||||
let text = self.hir.text;
|
||||
let mut end = (at as usize).min(text.len());
|
||||
@@ -499,6 +505,15 @@ impl<'a> File<'a> {
|
||||
Some(_) => return end as u32,
|
||||
}
|
||||
}
|
||||
let comments = self.hir.comments;
|
||||
let count_before = comments.partition_point(|comment| comment.pos < at);
|
||||
if let Some(last) = count_before
|
||||
.checked_sub(1)
|
||||
.and_then(|index| comments.get(index))
|
||||
&& last.next_token_start == at
|
||||
{
|
||||
return last.prev_token_end;
|
||||
}
|
||||
self.tokens_before(Span::empty(at))
|
||||
.next()
|
||||
.map_or(0, crate::tokens::Token::end)
|
||||
@@ -709,6 +724,10 @@ pub trait Handle<'a>: Copy {
|
||||
#[doc(hidden)]
|
||||
fn from_raw(file: &'a File<'a>, id: u32) -> Self;
|
||||
|
||||
/// How many nodes of this kind `file` has.
|
||||
#[doc(hidden)]
|
||||
fn count_in(file: &File<'a>) -> u32;
|
||||
|
||||
/// It is synthesized from a JSDoc comment: lists leave it out.
|
||||
#[doc(hidden)]
|
||||
#[inline]
|
||||
@@ -793,6 +812,10 @@ macro_rules! handle {
|
||||
$name::new(file, ::bun_sema::hir::$id(id))
|
||||
}
|
||||
#[inline]
|
||||
fn count_in(file: &$crate::ast::File<'a>) -> u32 {
|
||||
file.hir.$field.len() as u32
|
||||
}
|
||||
#[inline]
|
||||
fn is_synthetic(self) -> bool {
|
||||
self.file.has_synthetic_nodes() && self.starts_in_jsdoc()
|
||||
}
|
||||
|
||||
@@ -96,6 +96,40 @@ macro_rules! conversions {
|
||||
#[repr(u8)]
|
||||
enum Tag { File, $($variant,)* }
|
||||
|
||||
/// How many kinds of nodes there are.
|
||||
pub(crate) const KINDS: usize = [Tag::File, $(Tag::$variant),*].len();
|
||||
|
||||
impl<'a> File<'a> {
|
||||
/// The [`Node::index`] of the first node of each kind, and after them [`File::node_count`].
|
||||
fn kind_index_offsets(&self) -> &[u32; KINDS + 1] {
|
||||
self.lazy.kind_index_offsets.get_or_init(|| {
|
||||
let mut offsets = [0; KINDS + 1];
|
||||
let mut next = 1;
|
||||
$(
|
||||
offsets[Tag::$variant as usize] = next;
|
||||
next += <$handle as super::Handle>::count_in(self);
|
||||
)*
|
||||
offsets[KINDS] = next;
|
||||
offsets
|
||||
})
|
||||
}
|
||||
|
||||
/// How many nodes there are, of all kinds.
|
||||
pub fn node_count(&self) -> u32 {
|
||||
self.kind_index_offsets()[KINDS]
|
||||
}
|
||||
}
|
||||
|
||||
impl Node<'_> {
|
||||
/// A number below [`File::node_count`] that no other node of the file has. For who keeps something for some of
|
||||
/// the nodes, the way the file keeps which expressions are in parentheses.
|
||||
#[inline]
|
||||
pub fn index(self) -> u32 {
|
||||
let Packed { tag, id } = self.packed();
|
||||
self.file().kind_index_offsets()[tag as usize] + id
|
||||
}
|
||||
}
|
||||
|
||||
impl Packed {
|
||||
#[inline]
|
||||
pub(crate) fn unpack<'a>(self, file: &'a File<'a>) -> Node<'a> {
|
||||
|
||||
@@ -520,6 +520,14 @@ impl<'a> Stmt<'a> {
|
||||
ImportAttributes::within(self.file, self.span())
|
||||
}
|
||||
|
||||
/// The offset of the `*` of an `ExportStar`.
|
||||
pub fn export_star_offset(self) -> Option<u32> {
|
||||
match self.try_raw()?.kind {
|
||||
hir::StmtKind::ExportStar { star_pos, .. } => Some(star_pos),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `N` of the `ExportAsNamespace` `export as namespace N`, where it is written.
|
||||
pub fn namespace_export_name(self) -> Option<Ident<'a>> {
|
||||
let hir::StmtKind::ExportAsNamespace(name) = self.try_raw()?.kind else {
|
||||
|
||||
@@ -518,6 +518,11 @@ impl<'a> ImportAttributes<'a> {
|
||||
Some(Span::new(argument.map_or(ty.pos, |it| it.end), ty.end))
|
||||
}
|
||||
|
||||
/// The `{ type: "json" }`.
|
||||
pub fn object(self) -> Expr<'a> {
|
||||
self.object
|
||||
}
|
||||
|
||||
/// `with`, or the deprecated `assert`.
|
||||
pub fn keyword_span(self) -> Span {
|
||||
let rest = self
|
||||
|
||||
@@ -351,6 +351,7 @@ fn with_tree<'h, R>(
|
||||
how.atoms.of_this_thread(),
|
||||
options.experimental_decorators,
|
||||
options.every_file_is_a_module,
|
||||
true,
|
||||
// The names are the file's own.
|
||||
|summary, atoms| then(summary, atoms, &language),
|
||||
)
|
||||
@@ -922,11 +923,20 @@ fn print<'a>(
|
||||
&& out != file.text()
|
||||
{
|
||||
let is_same = with_tree(how, &out, |after, atoms, _| {
|
||||
let after = match &after {
|
||||
Summary::InPlace(hir) => Program::new(&**hir, &out, atoms),
|
||||
Summary::InArena(hir) => Program::new(&**hir, &out, atoms),
|
||||
// The parser goes on after an error, and the tree can be the same: `a\n=> b`.
|
||||
let (has_parse_errors, after) = match &after {
|
||||
Summary::InPlace(hir) => (
|
||||
hir.has_errors || hir.has_parse_diagnostics,
|
||||
Program::new(&**hir, &out, atoms),
|
||||
),
|
||||
Summary::InArena(hir) => (
|
||||
hir.has_errors || hir.has_parse_diagnostics,
|
||||
Program::new(&**hir, &out, atoms),
|
||||
),
|
||||
};
|
||||
bun_format::verify::compare(file, program, &after, options, &mut scratch.verify).is_ok()
|
||||
!has_parse_errors
|
||||
&& bun_format::verify::compare(file, program, &after, options, &mut scratch.verify)
|
||||
.is_ok()
|
||||
});
|
||||
if !is_same {
|
||||
return Err(Failure::Changed(
|
||||
@@ -999,6 +1009,28 @@ pub fn format_for_tests(
|
||||
})
|
||||
}
|
||||
|
||||
/// A line for each comment of the file at `path`: the node that owns it, or with `only_unprinted` the comments that no
|
||||
/// printer has printed. `None` if the file has a syntax error.
|
||||
pub fn debug_comments(
|
||||
path: &[u8],
|
||||
text: &[u8],
|
||||
only_unprinted: bool,
|
||||
is_oxfmt: bool,
|
||||
) -> Option<Vec<u8>> {
|
||||
let options = FormatOptions {
|
||||
flavor: match is_oxfmt {
|
||||
true => bun_format::options::Flavor::Oxfmt,
|
||||
false => bun_format::options::Flavor::Prettier,
|
||||
},
|
||||
debug_comments: match only_unprinted {
|
||||
true => bun_format::options::DebugComments::Unprinted,
|
||||
false => bun_format::options::DebugComments::Owners,
|
||||
},
|
||||
..FormatOptions::default()
|
||||
};
|
||||
Some(format_for_tests(path, text, &options).ok()?.0)
|
||||
}
|
||||
|
||||
/// What `--debug-check` says about the Svelte component `before` if `after` is what it is formatted to: whether both say the
|
||||
/// same. For the tests of the check, which the formatter gives nothing to complain about.
|
||||
pub fn is_same_svelte_for_tests(before: &[u8], after: &[u8]) -> bool {
|
||||
|
||||
Loaded 100 of 541 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user