@tishlang/tish-format 1.0.12 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,2157 @@
|
|
|
1
|
+
//! Pretty-print Tish AST to source. Style: 2-space indent, braces for blocks, trailing newline.
|
|
2
|
+
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
|
|
5
|
+
use tishlang_ast::{
|
|
6
|
+
ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern,
|
|
7
|
+
ExportDeclaration, Expr, FunParam, ImportSpecifier, JsxAttrValue, JsxChild, JsxProp, Literal,
|
|
8
|
+
LogicalAssignOp, MemberProp, ObjectProp, Program, Span, Statement, TypeAnnotation, TypedParam,
|
|
9
|
+
UnaryOp,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/// A comment recovered from source by [`scan_comments`]. The lexer discards
|
|
13
|
+
/// comments, so the parsed AST has none; the formatter re-inserts these by source
|
|
14
|
+
/// position so they survive a format pass.
|
|
15
|
+
#[derive(Clone)]
|
|
16
|
+
struct CommentTok {
|
|
17
|
+
/// 1-based (line, col) of the opening `/`, matching `ast::Span`.
|
|
18
|
+
start: (usize, usize),
|
|
19
|
+
/// Verbatim comment text, including the `//` or `/* */` delimiters.
|
|
20
|
+
text: String,
|
|
21
|
+
/// True when only whitespace precedes the comment on its line (a leading,
|
|
22
|
+
/// own-line comment); false for a trailing `code // note` comment.
|
|
23
|
+
own_line: bool,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Maps a `{`'s 1-based (line, col) to its matching `}`'s (line, col). Used to
|
|
27
|
+
/// bound a block's dangling-comment flush to the real closing brace, since the
|
|
28
|
+
/// parser's `Block` `span.end` overshoots to the next token.
|
|
29
|
+
type BraceMap = HashMap<(usize, usize), (usize, usize)>;
|
|
30
|
+
|
|
31
|
+
/// Format Tish source. On parse error, returns the parser message.
|
|
32
|
+
pub fn format_source(source: &str) -> Result<String, String> {
|
|
33
|
+
let program = tishlang_parser::parse(source)?;
|
|
34
|
+
let (comments, braces, bracket_spans) = scan_comments(source);
|
|
35
|
+
Ok(format_with_comments(
|
|
36
|
+
&program,
|
|
37
|
+
comments,
|
|
38
|
+
blank_line_map(source),
|
|
39
|
+
braces,
|
|
40
|
+
bracket_spans,
|
|
41
|
+
source,
|
|
42
|
+
))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Format an already-parsed program. Comments and blank lines are unavailable
|
|
46
|
+
/// here (the AST carries neither), so the output is comment-free and dense and
|
|
47
|
+
/// `// tish-fmt-ignore` has no effect; use [`format_source`] when the original
|
|
48
|
+
/// text is available.
|
|
49
|
+
pub fn format_program(program: &Program) -> String {
|
|
50
|
+
format_with_comments(
|
|
51
|
+
program,
|
|
52
|
+
Vec::new(),
|
|
53
|
+
Vec::new(),
|
|
54
|
+
BraceMap::new(),
|
|
55
|
+
Vec::new(),
|
|
56
|
+
"",
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn format_with_comments(
|
|
61
|
+
program: &Program,
|
|
62
|
+
comments: Vec<CommentTok>,
|
|
63
|
+
blank_lines: Vec<bool>,
|
|
64
|
+
braces: BraceMap,
|
|
65
|
+
bracket_spans: Vec<(usize, usize)>,
|
|
66
|
+
source: &str,
|
|
67
|
+
) -> String {
|
|
68
|
+
let mut p = Printer::new(comments, blank_lines, braces, bracket_spans, source);
|
|
69
|
+
p.print_seq(&program.statements, 0);
|
|
70
|
+
// Comments after the last statement (trailing file comments).
|
|
71
|
+
p.emit_leading_comments((usize::MAX, usize::MAX), 0);
|
|
72
|
+
// Exactly one trailing newline.
|
|
73
|
+
while p.buf.ends_with('\n') {
|
|
74
|
+
p.buf.pop();
|
|
75
|
+
}
|
|
76
|
+
p.buf.push('\n');
|
|
77
|
+
p.buf
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// `out[line]` is true when 1-based source `line` is blank (empty or whitespace
|
|
81
|
+
/// only). Index 0 is unused so callers can index by 1-based line number.
|
|
82
|
+
fn blank_line_map(source: &str) -> Vec<bool> {
|
|
83
|
+
let mut v = vec![false];
|
|
84
|
+
v.extend(source.lines().map(|l| l.trim().is_empty()));
|
|
85
|
+
v
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
struct Printer {
|
|
89
|
+
buf: String,
|
|
90
|
+
/// Comments in source order; `ci` is the next one not yet emitted.
|
|
91
|
+
comments: Vec<CommentTok>,
|
|
92
|
+
ci: usize,
|
|
93
|
+
/// 1-indexed: `blank_lines[n]` is true when source line `n` is blank. Empty
|
|
94
|
+
/// when no source is available (`format_program`).
|
|
95
|
+
blank_lines: Vec<bool>,
|
|
96
|
+
/// `{`→`}` position map for bounding block dangling-comment flushes.
|
|
97
|
+
braces: BraceMap,
|
|
98
|
+
/// Nonzero once anything has been emitted in the current sequence; used only
|
|
99
|
+
/// to suppress a leading blank line at the very start of a file or block.
|
|
100
|
+
emitted: usize,
|
|
101
|
+
/// Current structural indentation level for expression layout — set to the
|
|
102
|
+
/// enclosing statement's level and bumped as broken containers nest. Continuation
|
|
103
|
+
/// lines of a broken object/array/argument-list indent to `depth + 1`.
|
|
104
|
+
depth: usize,
|
|
105
|
+
/// When set, containers render on one line regardless of width. Used to measure
|
|
106
|
+
/// a container's flat width before deciding whether to break it.
|
|
107
|
+
force_flat: bool,
|
|
108
|
+
/// Source as chars, plus 1-based line → starting char-index, for slicing the
|
|
109
|
+
/// original text of a `// tish-fmt-ignore`-d statement verbatim. Empty when no
|
|
110
|
+
/// source is available (`format_program`).
|
|
111
|
+
src: Vec<char>,
|
|
112
|
+
line_start: Vec<usize>,
|
|
113
|
+
/// Set by [`Printer::emit_leading_comments`] when the comment directly above the
|
|
114
|
+
/// next statement is `// tish-fmt-ignore`; consumed by [`Printer::print_seq`].
|
|
115
|
+
ignore_next: bool,
|
|
116
|
+
/// `(open_line, close_line)` for every bracket pair, used to bound an ignored
|
|
117
|
+
/// statement's verbatim slice to its own full extent.
|
|
118
|
+
bracket_spans: Vec<(usize, usize)>,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Target line width: objects/arrays/argument lists that fit within this stay on
|
|
122
|
+
/// one line; longer ones break one item per line.
|
|
123
|
+
const WIDTH: usize = 100;
|
|
124
|
+
|
|
125
|
+
/// The escape-hatch marker (à la Prettier's `// prettier-ignore`): a comment with
|
|
126
|
+
/// exactly this content leaves the next statement's original source untouched.
|
|
127
|
+
const IGNORE_MARKER: &str = "tish-fmt-ignore";
|
|
128
|
+
|
|
129
|
+
impl Printer {
|
|
130
|
+
fn new(
|
|
131
|
+
comments: Vec<CommentTok>,
|
|
132
|
+
blank_lines: Vec<bool>,
|
|
133
|
+
braces: BraceMap,
|
|
134
|
+
bracket_spans: Vec<(usize, usize)>,
|
|
135
|
+
source: &str,
|
|
136
|
+
) -> Self {
|
|
137
|
+
let src: Vec<char> = source.chars().collect();
|
|
138
|
+
// line_start[L] = char index where 1-based line L begins (index 0 unused).
|
|
139
|
+
let mut line_start = vec![0usize, 0usize];
|
|
140
|
+
for (i, &c) in src.iter().enumerate() {
|
|
141
|
+
if c == '\n' {
|
|
142
|
+
line_start.push(i + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
Self {
|
|
146
|
+
buf: String::with_capacity(4096),
|
|
147
|
+
comments,
|
|
148
|
+
ci: 0,
|
|
149
|
+
blank_lines,
|
|
150
|
+
braces,
|
|
151
|
+
emitted: 0,
|
|
152
|
+
depth: 0,
|
|
153
|
+
force_flat: false,
|
|
154
|
+
src,
|
|
155
|
+
line_start,
|
|
156
|
+
ignore_next: false,
|
|
157
|
+
bracket_spans,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Char index of a 1-based (line, col) position, clamped to the source length.
|
|
162
|
+
fn char_pos(&self, (line, col): (usize, usize)) -> usize {
|
|
163
|
+
if line >= self.line_start.len() {
|
|
164
|
+
return self.src.len();
|
|
165
|
+
}
|
|
166
|
+
(self.line_start[line] + col - 1).min(self.src.len())
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// The original source spanning `[from, to)`, with trailing whitespace trimmed.
|
|
170
|
+
fn verbatim(&self, from: (usize, usize), to: (usize, usize)) -> String {
|
|
171
|
+
let a = self.char_pos(from);
|
|
172
|
+
let b = self.char_pos(to).max(a);
|
|
173
|
+
let s: String = self.src[a..b].iter().collect();
|
|
174
|
+
s.trim_end().to_string()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn indent(&mut self, level: usize) {
|
|
178
|
+
for _ in 0..level {
|
|
179
|
+
self.buf.push_str(" ");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Current column (chars since the last newline) — the start position for the
|
|
184
|
+
/// next thing to be printed.
|
|
185
|
+
fn col(&self) -> usize {
|
|
186
|
+
let line_start = self.buf.rfind('\n').map(|i| i + 1).unwrap_or(0);
|
|
187
|
+
self.buf[line_start..].chars().count()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Render a container flat into `buf`; if it fits in the remaining width keep it,
|
|
191
|
+
/// otherwise roll back and render the broken form. While measuring (and inside an
|
|
192
|
+
/// already-flat context) nested containers stay inline too, so the measured width
|
|
193
|
+
/// is the true single-line length. This is the layout decision in miniature — the
|
|
194
|
+
/// `fits` check of a Wadler/Prettier-style pretty-printer, done by rendering.
|
|
195
|
+
fn fit(&mut self, inline: impl Fn(&mut Self), broken: impl Fn(&mut Self)) {
|
|
196
|
+
if self.force_flat {
|
|
197
|
+
inline(self);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let mark = self.buf.len();
|
|
201
|
+
let col = self.col();
|
|
202
|
+
self.force_flat = true;
|
|
203
|
+
inline(self);
|
|
204
|
+
self.force_flat = false;
|
|
205
|
+
if col + (self.buf.len() - mark) <= WIDTH {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
self.buf.truncate(mark);
|
|
209
|
+
broken(self);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- Width-aware containers: inline when they fit, else one item per line. ----
|
|
213
|
+
|
|
214
|
+
fn emit_object(&mut self, props: &[ObjectProp]) {
|
|
215
|
+
if props.is_empty() {
|
|
216
|
+
self.buf.push_str("{}");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
self.fit(|s| s.object_inline(props), |s| s.object_broken(props));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn object_inline(&mut self, props: &[ObjectProp]) {
|
|
223
|
+
self.buf.push_str("{ ");
|
|
224
|
+
for (i, pr) in props.iter().enumerate() {
|
|
225
|
+
if i > 0 {
|
|
226
|
+
self.buf.push_str(", ");
|
|
227
|
+
}
|
|
228
|
+
self.object_prop(pr);
|
|
229
|
+
}
|
|
230
|
+
self.buf.push_str(" }");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn object_broken(&mut self, props: &[ObjectProp]) {
|
|
234
|
+
self.buf.push_str("{\n");
|
|
235
|
+
self.depth += 1;
|
|
236
|
+
for (i, pr) in props.iter().enumerate() {
|
|
237
|
+
if i > 0 {
|
|
238
|
+
self.buf.push_str(",\n");
|
|
239
|
+
}
|
|
240
|
+
self.indent(self.depth);
|
|
241
|
+
self.object_prop(pr);
|
|
242
|
+
}
|
|
243
|
+
self.depth -= 1;
|
|
244
|
+
self.buf.push('\n');
|
|
245
|
+
self.indent(self.depth);
|
|
246
|
+
self.buf.push('}');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn object_prop(&mut self, pr: &ObjectProp) {
|
|
250
|
+
match pr {
|
|
251
|
+
ObjectProp::KeyValue(k, v) => {
|
|
252
|
+
self.buf.push_str(k.as_ref());
|
|
253
|
+
self.buf.push_str(": ");
|
|
254
|
+
self.expr(v);
|
|
255
|
+
}
|
|
256
|
+
ObjectProp::Spread(ex) => {
|
|
257
|
+
self.buf.push_str("...");
|
|
258
|
+
self.expr(ex);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn emit_array(&mut self, elems: &[ArrayElement]) {
|
|
264
|
+
if elems.is_empty() {
|
|
265
|
+
self.buf.push_str("[]");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
self.fit(|s| s.array_inline(elems), |s| s.array_broken(elems));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fn array_inline(&mut self, elems: &[ArrayElement]) {
|
|
272
|
+
self.buf.push('[');
|
|
273
|
+
for (i, el) in elems.iter().enumerate() {
|
|
274
|
+
if i > 0 {
|
|
275
|
+
self.buf.push_str(", ");
|
|
276
|
+
}
|
|
277
|
+
self.array_elem(el);
|
|
278
|
+
}
|
|
279
|
+
self.buf.push(']');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn array_broken(&mut self, elems: &[ArrayElement]) {
|
|
283
|
+
self.buf.push_str("[\n");
|
|
284
|
+
self.depth += 1;
|
|
285
|
+
for (i, el) in elems.iter().enumerate() {
|
|
286
|
+
if i > 0 {
|
|
287
|
+
self.buf.push_str(",\n");
|
|
288
|
+
}
|
|
289
|
+
self.indent(self.depth);
|
|
290
|
+
self.array_elem(el);
|
|
291
|
+
}
|
|
292
|
+
self.depth -= 1;
|
|
293
|
+
self.buf.push('\n');
|
|
294
|
+
self.indent(self.depth);
|
|
295
|
+
self.buf.push(']');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn array_elem(&mut self, el: &ArrayElement) {
|
|
299
|
+
match el {
|
|
300
|
+
ArrayElement::Expr(ex) => self.expr(ex),
|
|
301
|
+
ArrayElement::Spread(ex) => {
|
|
302
|
+
self.buf.push_str("...");
|
|
303
|
+
self.expr(ex);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Argument list `(...)`. A sole/last object|array|arrow argument "hugs" the
|
|
309
|
+
/// parens — it breaks itself rather than forcing the whole list to break, e.g.
|
|
310
|
+
/// `f(layout, [\n …\n])`. Otherwise the list breaks as a unit when too wide.
|
|
311
|
+
fn emit_args(&mut self, args: &[CallArg]) {
|
|
312
|
+
if args.is_empty() {
|
|
313
|
+
self.buf.push_str("()");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if !self.force_flat && self.last_arg_huggable(args) {
|
|
317
|
+
self.buf.push('(');
|
|
318
|
+
for (i, a) in args.iter().enumerate() {
|
|
319
|
+
if i > 0 {
|
|
320
|
+
self.buf.push_str(", ");
|
|
321
|
+
}
|
|
322
|
+
self.call_arg(a);
|
|
323
|
+
}
|
|
324
|
+
self.buf.push(')');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
self.fit(|s| s.args_inline(args), |s| s.args_broken(args));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fn last_arg_huggable(&self, args: &[CallArg]) -> bool {
|
|
331
|
+
let huggable = |a: &CallArg| {
|
|
332
|
+
matches!(
|
|
333
|
+
a,
|
|
334
|
+
CallArg::Expr(Expr::Object { .. })
|
|
335
|
+
| CallArg::Expr(Expr::Array { .. })
|
|
336
|
+
| CallArg::Expr(Expr::ArrowFunction { .. })
|
|
337
|
+
)
|
|
338
|
+
};
|
|
339
|
+
let collection =
|
|
340
|
+
|a: &CallArg| matches!(a, CallArg::Expr(Expr::Object { .. } | Expr::Array { .. }));
|
|
341
|
+
match args.split_last() {
|
|
342
|
+
Some((last, rest)) => huggable(last) && !rest.iter().any(collection),
|
|
343
|
+
None => false,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fn args_inline(&mut self, args: &[CallArg]) {
|
|
348
|
+
self.buf.push('(');
|
|
349
|
+
for (i, a) in args.iter().enumerate() {
|
|
350
|
+
if i > 0 {
|
|
351
|
+
self.buf.push_str(", ");
|
|
352
|
+
}
|
|
353
|
+
self.call_arg(a);
|
|
354
|
+
}
|
|
355
|
+
self.buf.push(')');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn args_broken(&mut self, args: &[CallArg]) {
|
|
359
|
+
self.buf.push_str("(\n");
|
|
360
|
+
self.depth += 1;
|
|
361
|
+
for (i, a) in args.iter().enumerate() {
|
|
362
|
+
if i > 0 {
|
|
363
|
+
self.buf.push_str(",\n");
|
|
364
|
+
}
|
|
365
|
+
self.indent(self.depth);
|
|
366
|
+
self.call_arg(a);
|
|
367
|
+
}
|
|
368
|
+
self.depth -= 1;
|
|
369
|
+
self.buf.push('\n');
|
|
370
|
+
self.indent(self.depth);
|
|
371
|
+
self.buf.push(')');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fn call_arg(&mut self, a: &CallArg) {
|
|
375
|
+
match a {
|
|
376
|
+
CallArg::Expr(ex) => self.expr(ex),
|
|
377
|
+
CallArg::Spread(ex) => {
|
|
378
|
+
self.buf.push_str("...");
|
|
379
|
+
self.expr(ex);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn is_blank(&self, line: usize) -> bool {
|
|
385
|
+
self.blank_lines.get(line).copied().unwrap_or(false)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Preserve a single blank line before the item starting at `next_line` when
|
|
389
|
+
/// the source line directly above it was blank. Statement `span.end` is
|
|
390
|
+
/// unreliable (it points at the next token), so spacing keys off the reliable
|
|
391
|
+
/// start line and the source blank-line map rather than on span ranges.
|
|
392
|
+
fn vspace(&mut self, next_line: usize) {
|
|
393
|
+
if self.emitted != 0
|
|
394
|
+
&& self.is_blank(next_line.saturating_sub(1))
|
|
395
|
+
&& !self.buf.ends_with("\n\n")
|
|
396
|
+
{
|
|
397
|
+
self.buf.push('\n');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// Emit every pending comment positioned before `before`, each on its own line
|
|
402
|
+
/// at `level` indentation, preserving source blank-line separation. Used at
|
|
403
|
+
/// statement-sequence boundaries and before a block's closing brace.
|
|
404
|
+
fn emit_leading_comments(&mut self, before: (usize, usize), level: usize) {
|
|
405
|
+
while self.ci < self.comments.len() && self.comments[self.ci].start < before {
|
|
406
|
+
let c = self.comments[self.ci].clone();
|
|
407
|
+
self.ci += 1;
|
|
408
|
+
self.vspace(c.start.0);
|
|
409
|
+
self.indent(level);
|
|
410
|
+
self.buf.push_str(&c.text);
|
|
411
|
+
self.buf.push('\n');
|
|
412
|
+
self.emitted = c.start.0;
|
|
413
|
+
// A marker anywhere in the statement's leading comment group ignores it.
|
|
414
|
+
if is_ignore_marker(&c.text) {
|
|
415
|
+
self.ignore_next = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/// Emit trailing (same-line) comments sitting on source line `line`, inline
|
|
421
|
+
/// after the just-printed statement text.
|
|
422
|
+
fn emit_trailing_comments(&mut self, line: usize) {
|
|
423
|
+
while self.ci < self.comments.len() {
|
|
424
|
+
let c = &self.comments[self.ci];
|
|
425
|
+
if c.own_line || c.start.0 != line {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
let text = c.text.clone();
|
|
429
|
+
self.ci += 1;
|
|
430
|
+
self.buf.push(' ');
|
|
431
|
+
self.buf.push_str(&text);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// Print a run of statements (top level, block body, or switch case body),
|
|
436
|
+
/// interleaving recovered comments and preserving blank-line grouping. Trailing
|
|
437
|
+
/// comments attach by the statement's reliable start line (single-line case);
|
|
438
|
+
/// a trailing comment on a multi-line statement's last line simply migrates to
|
|
439
|
+
/// a leading comment of the next item — preserved, never dropped.
|
|
440
|
+
fn print_seq(&mut self, stmts: &[Statement], level: usize) {
|
|
441
|
+
for (i, s) in stmts.iter().enumerate() {
|
|
442
|
+
let sp = s.span();
|
|
443
|
+
self.ignore_next = false;
|
|
444
|
+
self.emit_leading_comments(sp.start, level);
|
|
445
|
+
self.vspace(sp.start.0);
|
|
446
|
+
if self.ignore_next {
|
|
447
|
+
self.emit_ignored(s, i, stmts, level);
|
|
448
|
+
} else {
|
|
449
|
+
self.stmt(s, level);
|
|
450
|
+
}
|
|
451
|
+
self.emitted = sp.start.0;
|
|
452
|
+
self.emit_trailing_comments(sp.start.0);
|
|
453
|
+
self.buf.push('\n');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/// Last source line covered by the statement at `start` — its start line, grown
|
|
458
|
+
/// over the full lines of any bracket (`{}`/`[]`/`()`) it transitively opens. This
|
|
459
|
+
/// bounds an ignored statement to its own extent regardless of what follows (next
|
|
460
|
+
/// sibling, a `switch` case label, the enclosing `}`), so verbatim never overruns.
|
|
461
|
+
fn ignored_last_line(&self, start: (usize, usize)) -> usize {
|
|
462
|
+
let mut last = start.0;
|
|
463
|
+
loop {
|
|
464
|
+
let mut grew = false;
|
|
465
|
+
for &(open_line, close_line) in &self.bracket_spans {
|
|
466
|
+
if open_line >= start.0 && open_line <= last && close_line > last {
|
|
467
|
+
last = close_line;
|
|
468
|
+
grew = true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if !grew {
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
last
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Emit a `// tish-fmt-ignore`-d statement as its original source, verbatim. The
|
|
479
|
+
/// slice ends at the smallest of: the statement's own bracket extent, the next
|
|
480
|
+
/// sibling, and the next own-line comment — so a same-line trailing comment is
|
|
481
|
+
/// kept but neither the next statement's leading comments nor anything past this
|
|
482
|
+
/// statement leaks in. Captured comments are skipped so they aren't re-emitted.
|
|
483
|
+
fn emit_ignored(&mut self, s: &Statement, i: usize, stmts: &[Statement], level: usize) {
|
|
484
|
+
let start = s.span().start;
|
|
485
|
+
let mut boundary = (self.ignored_last_line(start) + 1, 1);
|
|
486
|
+
if let Some(next) = stmts.get(i + 1) {
|
|
487
|
+
boundary = boundary.min(next.span().start);
|
|
488
|
+
}
|
|
489
|
+
let mut j = self.ci;
|
|
490
|
+
while j < self.comments.len() && self.comments[j].start < boundary {
|
|
491
|
+
let c = &self.comments[j];
|
|
492
|
+
if c.start > start && c.own_line {
|
|
493
|
+
boundary = c.start;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
j += 1;
|
|
497
|
+
}
|
|
498
|
+
self.indent(level);
|
|
499
|
+
let text = self.verbatim(start, boundary);
|
|
500
|
+
self.buf.push_str(&text);
|
|
501
|
+
while self.ci < self.comments.len() && self.comments[self.ci].start < boundary {
|
|
502
|
+
self.ci += 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fn stmt(&mut self, s: &Statement, level: usize) {
|
|
507
|
+
// Expression layout indents relative to this statement's level.
|
|
508
|
+
self.depth = level;
|
|
509
|
+
match s {
|
|
510
|
+
Statement::Block { statements, span } => self.block(statements, *span, level, true),
|
|
511
|
+
// Comma-declarators: render each as its own statement line. The caller
|
|
512
|
+
// (print_seq) emits the trailing newline, so only separate internally.
|
|
513
|
+
Statement::Multi { statements, .. } => {
|
|
514
|
+
for (i, st) in statements.iter().enumerate() {
|
|
515
|
+
if i > 0 {
|
|
516
|
+
self.buf.push('\n');
|
|
517
|
+
}
|
|
518
|
+
self.stmt(st, level);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
Statement::VarDecl {
|
|
522
|
+
name,
|
|
523
|
+
mutable,
|
|
524
|
+
type_ann,
|
|
525
|
+
init,
|
|
526
|
+
..
|
|
527
|
+
} => {
|
|
528
|
+
self.indent(level);
|
|
529
|
+
self.buf.push_str(if *mutable { "let " } else { "const " });
|
|
530
|
+
self.buf.push_str(name);
|
|
531
|
+
if let Some(t) = type_ann {
|
|
532
|
+
self.buf.push_str(": ");
|
|
533
|
+
self.type_ann(t);
|
|
534
|
+
}
|
|
535
|
+
if let Some(e) = init {
|
|
536
|
+
self.buf.push_str(" = ");
|
|
537
|
+
self.expr(e);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
Statement::VarDeclDestructure {
|
|
541
|
+
pattern,
|
|
542
|
+
mutable,
|
|
543
|
+
init,
|
|
544
|
+
..
|
|
545
|
+
} => {
|
|
546
|
+
self.indent(level);
|
|
547
|
+
self.buf.push_str(if *mutable { "let " } else { "const " });
|
|
548
|
+
self.destruct_pat(pattern);
|
|
549
|
+
self.buf.push_str(" = ");
|
|
550
|
+
self.expr(init);
|
|
551
|
+
}
|
|
552
|
+
Statement::ExprStmt { expr, .. } => {
|
|
553
|
+
self.indent(level);
|
|
554
|
+
self.expr(expr);
|
|
555
|
+
}
|
|
556
|
+
Statement::If {
|
|
557
|
+
cond,
|
|
558
|
+
then_branch,
|
|
559
|
+
else_branch,
|
|
560
|
+
..
|
|
561
|
+
} => {
|
|
562
|
+
self.indent(level);
|
|
563
|
+
self.buf.push_str("if (");
|
|
564
|
+
self.expr(cond);
|
|
565
|
+
self.buf.push_str(") ");
|
|
566
|
+
self.stmt_inline_or_block(then_branch, level);
|
|
567
|
+
if let Some(else_b) = else_branch {
|
|
568
|
+
self.buf.push_str(" else ");
|
|
569
|
+
self.stmt_inline_or_block(else_b, level);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
Statement::While { cond, body, .. } => {
|
|
573
|
+
self.indent(level);
|
|
574
|
+
self.buf.push_str("while (");
|
|
575
|
+
self.expr(cond);
|
|
576
|
+
self.buf.push_str(") ");
|
|
577
|
+
self.stmt_inline_or_block(body, level);
|
|
578
|
+
}
|
|
579
|
+
Statement::For {
|
|
580
|
+
init,
|
|
581
|
+
cond,
|
|
582
|
+
update,
|
|
583
|
+
body,
|
|
584
|
+
..
|
|
585
|
+
} => {
|
|
586
|
+
self.indent(level);
|
|
587
|
+
self.buf.push_str("for (");
|
|
588
|
+
if let Some(i) = init {
|
|
589
|
+
self.stmt_for_header(i);
|
|
590
|
+
}
|
|
591
|
+
self.buf.push_str("; ");
|
|
592
|
+
if let Some(c) = cond {
|
|
593
|
+
self.expr(c);
|
|
594
|
+
}
|
|
595
|
+
self.buf.push_str("; ");
|
|
596
|
+
if let Some(u) = update {
|
|
597
|
+
self.expr(u);
|
|
598
|
+
}
|
|
599
|
+
self.buf.push_str(") ");
|
|
600
|
+
self.stmt_inline_or_block(body, level);
|
|
601
|
+
}
|
|
602
|
+
Statement::ForOf {
|
|
603
|
+
name,
|
|
604
|
+
iterable,
|
|
605
|
+
body,
|
|
606
|
+
..
|
|
607
|
+
} => {
|
|
608
|
+
self.indent(level);
|
|
609
|
+
self.buf.push_str("for (let ");
|
|
610
|
+
self.buf.push_str(name);
|
|
611
|
+
self.buf.push_str(" of ");
|
|
612
|
+
self.expr(iterable);
|
|
613
|
+
self.buf.push_str(") ");
|
|
614
|
+
self.stmt_inline_or_block(body, level);
|
|
615
|
+
}
|
|
616
|
+
Statement::Return { value, .. } => {
|
|
617
|
+
self.indent(level);
|
|
618
|
+
self.buf.push_str("return");
|
|
619
|
+
if let Some(v) = value {
|
|
620
|
+
self.buf.push(' ');
|
|
621
|
+
self.expr(v);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
Statement::Break { .. } => {
|
|
625
|
+
self.indent(level);
|
|
626
|
+
self.buf.push_str("break");
|
|
627
|
+
}
|
|
628
|
+
Statement::Continue { .. } => {
|
|
629
|
+
self.indent(level);
|
|
630
|
+
self.buf.push_str("continue");
|
|
631
|
+
}
|
|
632
|
+
Statement::FunDecl {
|
|
633
|
+
async_,
|
|
634
|
+
name,
|
|
635
|
+
params,
|
|
636
|
+
rest_param,
|
|
637
|
+
return_type,
|
|
638
|
+
body,
|
|
639
|
+
..
|
|
640
|
+
} => {
|
|
641
|
+
self.indent(level);
|
|
642
|
+
if *async_ {
|
|
643
|
+
self.buf.push_str("async ");
|
|
644
|
+
}
|
|
645
|
+
self.buf.push_str("fn ");
|
|
646
|
+
self.buf.push_str(name);
|
|
647
|
+
self.buf.push('(');
|
|
648
|
+
self.param_list(params, rest_param);
|
|
649
|
+
self.buf.push(')');
|
|
650
|
+
if let Some(rt) = return_type {
|
|
651
|
+
self.buf.push_str(": ");
|
|
652
|
+
self.type_ann(rt);
|
|
653
|
+
}
|
|
654
|
+
if let Statement::ExprStmt { expr, .. } = body.as_ref() {
|
|
655
|
+
self.buf.push_str(" = ");
|
|
656
|
+
self.expr(expr);
|
|
657
|
+
} else {
|
|
658
|
+
self.buf.push(' ');
|
|
659
|
+
self.stmt_inline_or_block(body, level);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
Statement::Switch {
|
|
663
|
+
expr,
|
|
664
|
+
cases,
|
|
665
|
+
default_body,
|
|
666
|
+
..
|
|
667
|
+
} => {
|
|
668
|
+
self.indent(level);
|
|
669
|
+
self.buf.push_str("switch (");
|
|
670
|
+
self.expr(expr);
|
|
671
|
+
self.buf.push_str(") {\n");
|
|
672
|
+
for (case_e, stmts) in cases {
|
|
673
|
+
self.indent(level + 1);
|
|
674
|
+
match case_e {
|
|
675
|
+
Some(e) => {
|
|
676
|
+
self.buf.push_str("case ");
|
|
677
|
+
self.expr(e);
|
|
678
|
+
self.buf.push_str(":\n");
|
|
679
|
+
}
|
|
680
|
+
None => self.buf.push_str("default:\n"),
|
|
681
|
+
}
|
|
682
|
+
self.emitted = 0;
|
|
683
|
+
self.print_seq(stmts, level + 2);
|
|
684
|
+
}
|
|
685
|
+
if let Some(def) = default_body {
|
|
686
|
+
self.indent(level + 1);
|
|
687
|
+
self.buf.push_str("default:\n");
|
|
688
|
+
self.emitted = 0;
|
|
689
|
+
self.print_seq(def, level + 2);
|
|
690
|
+
}
|
|
691
|
+
self.indent(level);
|
|
692
|
+
self.buf.push('}');
|
|
693
|
+
}
|
|
694
|
+
Statement::DoWhile { body, cond, .. } => {
|
|
695
|
+
self.indent(level);
|
|
696
|
+
self.buf.push_str("do ");
|
|
697
|
+
self.stmt_inline_or_block(body, level);
|
|
698
|
+
self.depth = level; // body recursion moved depth; restore for cond
|
|
699
|
+
self.buf.push_str(" while (");
|
|
700
|
+
self.expr(cond);
|
|
701
|
+
self.buf.push(')');
|
|
702
|
+
}
|
|
703
|
+
Statement::Throw { value, .. } => {
|
|
704
|
+
self.indent(level);
|
|
705
|
+
self.buf.push_str("throw ");
|
|
706
|
+
self.expr(value);
|
|
707
|
+
}
|
|
708
|
+
Statement::Try {
|
|
709
|
+
body,
|
|
710
|
+
catch_param,
|
|
711
|
+
catch_body,
|
|
712
|
+
finally_body,
|
|
713
|
+
..
|
|
714
|
+
} => {
|
|
715
|
+
self.indent(level);
|
|
716
|
+
self.buf.push_str("try ");
|
|
717
|
+
self.stmt_inline_or_block(body, level);
|
|
718
|
+
if let (Some(p), Some(cb)) = (catch_param, catch_body) {
|
|
719
|
+
self.buf.push_str(" catch (");
|
|
720
|
+
self.buf.push_str(p);
|
|
721
|
+
self.buf.push_str(") ");
|
|
722
|
+
self.stmt_inline_or_block(cb, level);
|
|
723
|
+
}
|
|
724
|
+
if let Some(fb) = finally_body {
|
|
725
|
+
self.buf.push_str(" finally ");
|
|
726
|
+
self.stmt_inline_or_block(fb, level);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
Statement::Import {
|
|
730
|
+
specifiers, from, ..
|
|
731
|
+
} => {
|
|
732
|
+
self.indent(level);
|
|
733
|
+
self.buf.push_str("import ");
|
|
734
|
+
self.import_specs(specifiers);
|
|
735
|
+
self.buf.push_str(" from ");
|
|
736
|
+
self.string_lit(from.as_ref());
|
|
737
|
+
}
|
|
738
|
+
Statement::TypeAlias { name, ty, .. } => {
|
|
739
|
+
self.indent(level);
|
|
740
|
+
self.buf.push_str("type ");
|
|
741
|
+
self.buf.push_str(name);
|
|
742
|
+
self.buf.push_str(" = ");
|
|
743
|
+
self.type_ann(ty);
|
|
744
|
+
}
|
|
745
|
+
Statement::DeclareVar {
|
|
746
|
+
name,
|
|
747
|
+
type_ann,
|
|
748
|
+
const_,
|
|
749
|
+
..
|
|
750
|
+
} => {
|
|
751
|
+
self.indent(level);
|
|
752
|
+
self.buf.push_str("declare ");
|
|
753
|
+
self.buf.push_str(if *const_ { "const " } else { "let " });
|
|
754
|
+
self.buf.push_str(name);
|
|
755
|
+
if let Some(t) = type_ann {
|
|
756
|
+
self.buf.push_str(": ");
|
|
757
|
+
self.type_ann(t);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
Statement::DeclareFun {
|
|
761
|
+
async_,
|
|
762
|
+
name,
|
|
763
|
+
params,
|
|
764
|
+
rest_param,
|
|
765
|
+
return_type,
|
|
766
|
+
..
|
|
767
|
+
} => {
|
|
768
|
+
self.indent(level);
|
|
769
|
+
self.buf.push_str("declare ");
|
|
770
|
+
if *async_ {
|
|
771
|
+
self.buf.push_str("async ");
|
|
772
|
+
}
|
|
773
|
+
self.buf.push_str("fn ");
|
|
774
|
+
self.buf.push_str(name);
|
|
775
|
+
self.buf.push('(');
|
|
776
|
+
self.param_list(params, rest_param);
|
|
777
|
+
self.buf.push(')');
|
|
778
|
+
if let Some(rt) = return_type {
|
|
779
|
+
self.buf.push_str(": ");
|
|
780
|
+
self.type_ann(rt);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
Statement::Export { declaration, .. } => {
|
|
784
|
+
self.indent(level);
|
|
785
|
+
self.buf.push_str("export ");
|
|
786
|
+
match declaration.as_ref() {
|
|
787
|
+
ExportDeclaration::Named(inner) => {
|
|
788
|
+
if let Statement::FunDecl {
|
|
789
|
+
async_,
|
|
790
|
+
name,
|
|
791
|
+
params,
|
|
792
|
+
rest_param,
|
|
793
|
+
return_type,
|
|
794
|
+
body,
|
|
795
|
+
..
|
|
796
|
+
} = inner.as_ref()
|
|
797
|
+
{
|
|
798
|
+
if *async_ {
|
|
799
|
+
self.buf.push_str("async ");
|
|
800
|
+
}
|
|
801
|
+
self.buf.push_str("fn ");
|
|
802
|
+
self.buf.push_str(name);
|
|
803
|
+
self.buf.push('(');
|
|
804
|
+
self.param_list(params, rest_param);
|
|
805
|
+
self.buf.push(')');
|
|
806
|
+
if let Some(rt) = return_type {
|
|
807
|
+
self.buf.push_str(": ");
|
|
808
|
+
self.type_ann(rt);
|
|
809
|
+
}
|
|
810
|
+
self.buf.push(' ');
|
|
811
|
+
self.stmt_inline_or_block(body, level);
|
|
812
|
+
} else {
|
|
813
|
+
self.stmt(inner, level);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
ExportDeclaration::Default(e) => {
|
|
817
|
+
self.buf.push_str("default ");
|
|
818
|
+
self.expr(e);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
fn stmt_for_header(&mut self, s: &Statement) {
|
|
826
|
+
match s {
|
|
827
|
+
Statement::VarDecl {
|
|
828
|
+
name,
|
|
829
|
+
mutable,
|
|
830
|
+
type_ann,
|
|
831
|
+
init,
|
|
832
|
+
..
|
|
833
|
+
} => {
|
|
834
|
+
self.buf.push_str(if *mutable { "let " } else { "const " });
|
|
835
|
+
self.buf.push_str(name);
|
|
836
|
+
if let Some(t) = type_ann {
|
|
837
|
+
self.buf.push_str(": ");
|
|
838
|
+
self.type_ann(t);
|
|
839
|
+
}
|
|
840
|
+
if let Some(e) = init {
|
|
841
|
+
self.buf.push_str(" = ");
|
|
842
|
+
self.expr(e);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
Statement::ExprStmt { expr, .. } => self.expr(expr),
|
|
846
|
+
_ => {}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/// Print a `{ … }` block. `lead_indent` controls whether the opening brace is
|
|
851
|
+
/// indented (true for a standalone block statement) or written at the current
|
|
852
|
+
/// position (false when it follows `if (…) `, `fn f() `, `else `, etc.).
|
|
853
|
+
fn block(&mut self, statements: &[Statement], span: Span, level: usize, lead_indent: bool) {
|
|
854
|
+
if lead_indent {
|
|
855
|
+
self.indent(level);
|
|
856
|
+
}
|
|
857
|
+
self.buf.push_str("{\n");
|
|
858
|
+
// Fresh sequence: suppress any blank line immediately after `{`.
|
|
859
|
+
self.emitted = 0;
|
|
860
|
+
self.print_seq(statements, level + 1);
|
|
861
|
+
// Dangling comments between the last statement and `}` (e.g. a note inside an
|
|
862
|
+
// otherwise-empty block). Bound by the real closing brace — `span.end`
|
|
863
|
+
// overshoots to the next token. Brace-less (indent) blocks have no map entry,
|
|
864
|
+
// so `span.start` flushes nothing and the comment migrates to the next sibling.
|
|
865
|
+
let bound = self.braces.get(&span.start).copied().unwrap_or(span.start);
|
|
866
|
+
self.emit_leading_comments(bound, level + 1);
|
|
867
|
+
self.indent(level);
|
|
868
|
+
self.buf.push('}');
|
|
869
|
+
self.emitted = span.start.0;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
fn stmt_inline_or_block(&mut self, s: &Statement, level: usize) {
|
|
873
|
+
if let Statement::Block { statements, span } = s {
|
|
874
|
+
self.block(statements, *span, level, false);
|
|
875
|
+
} else {
|
|
876
|
+
let sp = s.span();
|
|
877
|
+
self.buf.push_str("{\n");
|
|
878
|
+
self.emitted = 0;
|
|
879
|
+
self.emit_leading_comments(sp.start, level + 1);
|
|
880
|
+
self.stmt(s, level + 1);
|
|
881
|
+
self.emitted = sp.start.0;
|
|
882
|
+
self.emit_trailing_comments(sp.start.0);
|
|
883
|
+
self.buf.push('\n');
|
|
884
|
+
self.indent(level);
|
|
885
|
+
self.buf.push('}');
|
|
886
|
+
self.emitted = sp.start.0;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
fn import_specs(&mut self, specs: &[ImportSpecifier]) {
|
|
891
|
+
if specs.len() == 1 {
|
|
892
|
+
match &specs[0] {
|
|
893
|
+
ImportSpecifier::Default { name, .. } => self.buf.push_str(name.as_ref()),
|
|
894
|
+
ImportSpecifier::Namespace { name, .. } => {
|
|
895
|
+
self.buf.push_str("* as ");
|
|
896
|
+
self.buf.push_str(name.as_ref());
|
|
897
|
+
}
|
|
898
|
+
ImportSpecifier::Named { name, alias, .. } => {
|
|
899
|
+
self.buf.push_str("{ ");
|
|
900
|
+
self.import_named(name.as_ref(), alias.as_deref());
|
|
901
|
+
self.buf.push_str(" }");
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
// A long named-import list wraps one name per line.
|
|
907
|
+
self.fit(
|
|
908
|
+
|s| s.import_list_inline(specs),
|
|
909
|
+
|s| s.import_list_broken(specs),
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
fn import_named(&mut self, name: &str, alias: Option<&str>) {
|
|
914
|
+
self.buf.push_str(name);
|
|
915
|
+
if let Some(a) = alias {
|
|
916
|
+
self.buf.push_str(" as ");
|
|
917
|
+
self.buf.push_str(a);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
fn import_list_inline(&mut self, specs: &[ImportSpecifier]) {
|
|
922
|
+
self.buf.push_str("{ ");
|
|
923
|
+
for (i, sp) in specs.iter().enumerate() {
|
|
924
|
+
if i > 0 {
|
|
925
|
+
self.buf.push_str(", ");
|
|
926
|
+
}
|
|
927
|
+
if let ImportSpecifier::Named { name, alias, .. } = sp {
|
|
928
|
+
self.import_named(name.as_ref(), alias.as_deref());
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
self.buf.push_str(" }");
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
fn import_list_broken(&mut self, specs: &[ImportSpecifier]) {
|
|
935
|
+
self.buf.push_str("{\n");
|
|
936
|
+
self.depth += 1;
|
|
937
|
+
for (i, sp) in specs.iter().enumerate() {
|
|
938
|
+
if i > 0 {
|
|
939
|
+
self.buf.push_str(",\n");
|
|
940
|
+
}
|
|
941
|
+
self.indent(self.depth);
|
|
942
|
+
if let ImportSpecifier::Named { name, alias, .. } = sp {
|
|
943
|
+
self.import_named(name.as_ref(), alias.as_deref());
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
self.depth -= 1;
|
|
947
|
+
self.buf.push('\n');
|
|
948
|
+
self.indent(self.depth);
|
|
949
|
+
self.buf.push('}');
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
fn param_list(&mut self, params: &[FunParam], rest: &Option<TypedParam>) {
|
|
953
|
+
for (i, p) in params.iter().enumerate() {
|
|
954
|
+
if i > 0 {
|
|
955
|
+
self.buf.push_str(", ");
|
|
956
|
+
}
|
|
957
|
+
match p {
|
|
958
|
+
FunParam::Simple(tp) => {
|
|
959
|
+
self.buf.push_str(tp.name.as_ref());
|
|
960
|
+
if let Some(t) = &tp.type_ann {
|
|
961
|
+
self.buf.push_str(": ");
|
|
962
|
+
self.type_ann(t);
|
|
963
|
+
}
|
|
964
|
+
if let Some(e) = &tp.default {
|
|
965
|
+
self.buf.push_str(" = ");
|
|
966
|
+
self.expr(e);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
FunParam::Destructure {
|
|
970
|
+
pattern,
|
|
971
|
+
type_ann,
|
|
972
|
+
default,
|
|
973
|
+
} => {
|
|
974
|
+
self.destruct_pat(pattern);
|
|
975
|
+
if let Some(t) = type_ann {
|
|
976
|
+
self.buf.push_str(": ");
|
|
977
|
+
self.type_ann(t);
|
|
978
|
+
}
|
|
979
|
+
if let Some(e) = default {
|
|
980
|
+
self.buf.push_str(" = ");
|
|
981
|
+
self.expr(e);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if let Some(r) = rest {
|
|
987
|
+
if !params.is_empty() {
|
|
988
|
+
self.buf.push_str(", ");
|
|
989
|
+
}
|
|
990
|
+
self.buf.push_str("...");
|
|
991
|
+
self.buf.push_str(r.name.as_ref());
|
|
992
|
+
if let Some(t) = &r.type_ann {
|
|
993
|
+
self.buf.push_str(": ");
|
|
994
|
+
self.type_ann(t);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
fn destruct_pat(&mut self, p: &DestructPattern) {
|
|
1000
|
+
match p {
|
|
1001
|
+
DestructPattern::Array(elems) => {
|
|
1002
|
+
self.buf.push('[');
|
|
1003
|
+
for (i, e) in elems.iter().enumerate() {
|
|
1004
|
+
if i > 0 {
|
|
1005
|
+
self.buf.push_str(", ");
|
|
1006
|
+
}
|
|
1007
|
+
match e {
|
|
1008
|
+
Some(DestructElement::Ident(n, _)) => self.buf.push_str(n.as_ref()),
|
|
1009
|
+
Some(DestructElement::Pattern(inner)) => self.destruct_pat(inner),
|
|
1010
|
+
Some(DestructElement::Rest(n, _)) => {
|
|
1011
|
+
self.buf.push_str("...");
|
|
1012
|
+
self.buf.push_str(n.as_ref());
|
|
1013
|
+
}
|
|
1014
|
+
None => {}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
self.buf.push(']');
|
|
1018
|
+
}
|
|
1019
|
+
DestructPattern::Object(props) => {
|
|
1020
|
+
self.buf.push_str("{ ");
|
|
1021
|
+
for (i, pr) in props.iter().enumerate() {
|
|
1022
|
+
if i > 0 {
|
|
1023
|
+
self.buf.push_str(", ");
|
|
1024
|
+
}
|
|
1025
|
+
self.buf.push_str(pr.key.as_ref());
|
|
1026
|
+
match &pr.value {
|
|
1027
|
+
DestructElement::Ident(n, _) if n.as_ref() != pr.key.as_ref() => {
|
|
1028
|
+
self.buf.push_str(": ");
|
|
1029
|
+
self.buf.push_str(n.as_ref());
|
|
1030
|
+
}
|
|
1031
|
+
DestructElement::Ident(_, _) => {}
|
|
1032
|
+
DestructElement::Pattern(inner) => {
|
|
1033
|
+
self.buf.push_str(": ");
|
|
1034
|
+
self.destruct_pat(inner);
|
|
1035
|
+
}
|
|
1036
|
+
DestructElement::Rest(n, _) => {
|
|
1037
|
+
self.buf.push_str(": ...");
|
|
1038
|
+
self.buf.push_str(n.as_ref());
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
self.buf.push_str(" }");
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
fn type_ann(&mut self, t: &TypeAnnotation) {
|
|
1048
|
+
match t {
|
|
1049
|
+
TypeAnnotation::Simple(s) => self.buf.push_str(s.as_ref()),
|
|
1050
|
+
TypeAnnotation::Array(inner) => {
|
|
1051
|
+
self.type_ann(inner);
|
|
1052
|
+
self.buf.push_str("[]");
|
|
1053
|
+
}
|
|
1054
|
+
TypeAnnotation::Object(props) => {
|
|
1055
|
+
self.buf.push_str("{ ");
|
|
1056
|
+
for (i, (k, v)) in props.iter().enumerate() {
|
|
1057
|
+
if i > 0 {
|
|
1058
|
+
self.buf.push_str(", ");
|
|
1059
|
+
}
|
|
1060
|
+
self.buf.push_str(k.as_ref());
|
|
1061
|
+
self.buf.push_str(": ");
|
|
1062
|
+
self.type_ann(v);
|
|
1063
|
+
}
|
|
1064
|
+
self.buf.push_str(" }");
|
|
1065
|
+
}
|
|
1066
|
+
TypeAnnotation::Function { params, returns } => {
|
|
1067
|
+
self.buf.push('(');
|
|
1068
|
+
for (i, p) in params.iter().enumerate() {
|
|
1069
|
+
if i > 0 {
|
|
1070
|
+
self.buf.push_str(", ");
|
|
1071
|
+
}
|
|
1072
|
+
self.type_ann(p);
|
|
1073
|
+
}
|
|
1074
|
+
self.buf.push_str(") => ");
|
|
1075
|
+
self.type_ann(returns);
|
|
1076
|
+
}
|
|
1077
|
+
TypeAnnotation::Union(u) => {
|
|
1078
|
+
for (i, x) in u.iter().enumerate() {
|
|
1079
|
+
if i > 0 {
|
|
1080
|
+
self.buf.push_str(" | ");
|
|
1081
|
+
}
|
|
1082
|
+
self.type_ann(x);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
TypeAnnotation::Tuple(elems) => {
|
|
1086
|
+
self.buf.push('[');
|
|
1087
|
+
for (i, x) in elems.iter().enumerate() {
|
|
1088
|
+
if i > 0 {
|
|
1089
|
+
self.buf.push_str(", ");
|
|
1090
|
+
}
|
|
1091
|
+
self.type_ann(x);
|
|
1092
|
+
}
|
|
1093
|
+
self.buf.push(']');
|
|
1094
|
+
}
|
|
1095
|
+
TypeAnnotation::Literal(lit) => match lit {
|
|
1096
|
+
tishlang_ast::TypeLiteral::Str(s) => {
|
|
1097
|
+
self.buf.push('"');
|
|
1098
|
+
self.buf.push_str(s.as_ref());
|
|
1099
|
+
self.buf.push('"');
|
|
1100
|
+
}
|
|
1101
|
+
tishlang_ast::TypeLiteral::Num(n) => self.buf.push_str(&n.to_string()),
|
|
1102
|
+
tishlang_ast::TypeLiteral::Bool(b) => self.buf.push_str(&b.to_string()),
|
|
1103
|
+
},
|
|
1104
|
+
TypeAnnotation::Intersection(parts) => {
|
|
1105
|
+
for (i, x) in parts.iter().enumerate() {
|
|
1106
|
+
if i > 0 {
|
|
1107
|
+
self.buf.push_str(" & ");
|
|
1108
|
+
}
|
|
1109
|
+
self.type_ann(x);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/// Print `e` as a sub-expression, wrapping it in parentheses when its operator
|
|
1116
|
+
/// binds looser than `min_prec` (so the printed form re-parses to the same AST).
|
|
1117
|
+
fn child(&mut self, e: &Expr, min_prec: u8) {
|
|
1118
|
+
if expr_prec(e) < min_prec {
|
|
1119
|
+
self.buf.push('(');
|
|
1120
|
+
self.expr(e);
|
|
1121
|
+
self.buf.push(')');
|
|
1122
|
+
} else {
|
|
1123
|
+
self.expr(e);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
fn expr(&mut self, e: &Expr) {
|
|
1128
|
+
match e {
|
|
1129
|
+
Expr::Literal { value, .. } => match value {
|
|
1130
|
+
Literal::Number(n) => {
|
|
1131
|
+
if n.fract() == 0.0 && n.abs() < 1e15 {
|
|
1132
|
+
self.buf.push_str(&format!("{}", *n as i64));
|
|
1133
|
+
} else {
|
|
1134
|
+
self.buf.push_str(&format!("{}", n));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
Literal::String(s) => self.string_lit(s.as_ref()),
|
|
1138
|
+
Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
|
|
1139
|
+
Literal::Null => self.buf.push_str("null"),
|
|
1140
|
+
},
|
|
1141
|
+
Expr::Ident { name, .. } => self.buf.push_str(name.as_ref()),
|
|
1142
|
+
Expr::Binary {
|
|
1143
|
+
left, op, right, ..
|
|
1144
|
+
} => {
|
|
1145
|
+
// Parenthesize operands by precedence/associativity so the printed
|
|
1146
|
+
// grouping re-parses to the same tree (the AST has no paren nodes).
|
|
1147
|
+
let p = binop_prec(*op);
|
|
1148
|
+
let right_assoc = matches!(op, BinOp::Pow);
|
|
1149
|
+
let (lmin, rmin) = if right_assoc { (p + 1, p) } else { (p, p + 1) };
|
|
1150
|
+
self.child(left, lmin);
|
|
1151
|
+
self.buf.push(' ');
|
|
1152
|
+
self.buf.push_str(binop(*op));
|
|
1153
|
+
self.buf.push(' ');
|
|
1154
|
+
self.child(right, rmin);
|
|
1155
|
+
}
|
|
1156
|
+
Expr::Unary { op, operand, .. } => {
|
|
1157
|
+
match op {
|
|
1158
|
+
UnaryOp::Not => self.buf.push('!'),
|
|
1159
|
+
UnaryOp::Neg => self.buf.push('-'),
|
|
1160
|
+
UnaryOp::Pos => self.buf.push('+'),
|
|
1161
|
+
UnaryOp::BitNot => self.buf.push('~'),
|
|
1162
|
+
UnaryOp::Void => self.buf.push_str("void "),
|
|
1163
|
+
}
|
|
1164
|
+
self.child(operand, PREC_POSTFIX);
|
|
1165
|
+
}
|
|
1166
|
+
Expr::Call { callee, args, .. } => {
|
|
1167
|
+
self.child(callee, PREC_POSTFIX);
|
|
1168
|
+
self.emit_args(args);
|
|
1169
|
+
}
|
|
1170
|
+
Expr::New { callee, args, .. } => {
|
|
1171
|
+
self.buf.push_str("new ");
|
|
1172
|
+
self.child(callee, PREC_POSTFIX);
|
|
1173
|
+
if !args.is_empty() {
|
|
1174
|
+
self.emit_args(args);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
Expr::Member {
|
|
1178
|
+
object,
|
|
1179
|
+
prop,
|
|
1180
|
+
optional,
|
|
1181
|
+
..
|
|
1182
|
+
} => {
|
|
1183
|
+
self.child(object, PREC_POSTFIX);
|
|
1184
|
+
if *optional {
|
|
1185
|
+
self.buf.push_str("?.");
|
|
1186
|
+
} else {
|
|
1187
|
+
self.buf.push('.');
|
|
1188
|
+
}
|
|
1189
|
+
match prop {
|
|
1190
|
+
MemberProp::Name { name, .. } => self.buf.push_str(name.as_ref()),
|
|
1191
|
+
MemberProp::Expr(ex) => {
|
|
1192
|
+
self.buf.push('[');
|
|
1193
|
+
self.expr(ex);
|
|
1194
|
+
self.buf.push(']');
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
Expr::Index {
|
|
1199
|
+
object,
|
|
1200
|
+
index,
|
|
1201
|
+
optional,
|
|
1202
|
+
..
|
|
1203
|
+
} => {
|
|
1204
|
+
self.child(object, PREC_POSTFIX);
|
|
1205
|
+
if *optional {
|
|
1206
|
+
self.buf.push_str("?.[");
|
|
1207
|
+
} else {
|
|
1208
|
+
self.buf.push('[');
|
|
1209
|
+
}
|
|
1210
|
+
self.expr(index);
|
|
1211
|
+
self.buf.push(']');
|
|
1212
|
+
}
|
|
1213
|
+
Expr::Conditional {
|
|
1214
|
+
cond,
|
|
1215
|
+
then_branch,
|
|
1216
|
+
else_branch,
|
|
1217
|
+
..
|
|
1218
|
+
} => {
|
|
1219
|
+
// cond binds tighter than `?:`; the else chains (right-assoc).
|
|
1220
|
+
self.child(cond, PREC_NULLISH);
|
|
1221
|
+
self.buf.push_str(" ? ");
|
|
1222
|
+
self.expr(then_branch);
|
|
1223
|
+
self.buf.push_str(" : ");
|
|
1224
|
+
self.child(else_branch, PREC_CONDITIONAL);
|
|
1225
|
+
}
|
|
1226
|
+
Expr::NullishCoalesce { left, right, .. } => {
|
|
1227
|
+
self.child(left, PREC_NULLISH);
|
|
1228
|
+
self.buf.push_str(" ?? ");
|
|
1229
|
+
self.child(right, PREC_NULLISH + 1);
|
|
1230
|
+
}
|
|
1231
|
+
Expr::Array { elements, .. } => self.emit_array(elements),
|
|
1232
|
+
Expr::Object { props, .. } => self.emit_object(props),
|
|
1233
|
+
Expr::Assign { name, value, .. } => {
|
|
1234
|
+
self.buf.push_str(name.as_ref());
|
|
1235
|
+
self.buf.push_str(" = ");
|
|
1236
|
+
self.expr(value);
|
|
1237
|
+
}
|
|
1238
|
+
Expr::TypeOf { operand, .. } => {
|
|
1239
|
+
self.buf.push_str("typeof ");
|
|
1240
|
+
self.child(operand, PREC_POSTFIX);
|
|
1241
|
+
}
|
|
1242
|
+
Expr::Delete { target, .. } => {
|
|
1243
|
+
self.buf.push_str("delete ");
|
|
1244
|
+
self.child(target, PREC_POSTFIX);
|
|
1245
|
+
}
|
|
1246
|
+
Expr::PostfixInc { name, .. } => {
|
|
1247
|
+
self.buf.push_str(name.as_ref());
|
|
1248
|
+
self.buf.push_str("++");
|
|
1249
|
+
}
|
|
1250
|
+
Expr::PostfixDec { name, .. } => {
|
|
1251
|
+
self.buf.push_str(name.as_ref());
|
|
1252
|
+
self.buf.push_str("--");
|
|
1253
|
+
}
|
|
1254
|
+
Expr::PrefixInc { name, .. } => {
|
|
1255
|
+
self.buf.push_str("++");
|
|
1256
|
+
self.buf.push_str(name.as_ref());
|
|
1257
|
+
}
|
|
1258
|
+
Expr::PrefixDec { name, .. } => {
|
|
1259
|
+
self.buf.push_str("--");
|
|
1260
|
+
self.buf.push_str(name.as_ref());
|
|
1261
|
+
}
|
|
1262
|
+
Expr::CompoundAssign {
|
|
1263
|
+
name, op, value, ..
|
|
1264
|
+
} => {
|
|
1265
|
+
self.buf.push_str(name.as_ref());
|
|
1266
|
+
self.buf.push_str(compound(*op));
|
|
1267
|
+
self.expr(value);
|
|
1268
|
+
}
|
|
1269
|
+
Expr::LogicalAssign {
|
|
1270
|
+
name, op, value, ..
|
|
1271
|
+
} => {
|
|
1272
|
+
self.buf.push_str(name.as_ref());
|
|
1273
|
+
self.buf.push_str(logical_assign(*op));
|
|
1274
|
+
self.expr(value);
|
|
1275
|
+
}
|
|
1276
|
+
Expr::MemberAssign {
|
|
1277
|
+
object,
|
|
1278
|
+
prop,
|
|
1279
|
+
value,
|
|
1280
|
+
..
|
|
1281
|
+
} => {
|
|
1282
|
+
self.child(object, PREC_POSTFIX);
|
|
1283
|
+
self.buf.push('.');
|
|
1284
|
+
self.buf.push_str(prop.as_ref());
|
|
1285
|
+
self.buf.push_str(" = ");
|
|
1286
|
+
self.expr(value);
|
|
1287
|
+
}
|
|
1288
|
+
Expr::IndexAssign {
|
|
1289
|
+
object,
|
|
1290
|
+
index,
|
|
1291
|
+
value,
|
|
1292
|
+
..
|
|
1293
|
+
} => {
|
|
1294
|
+
self.child(object, PREC_POSTFIX);
|
|
1295
|
+
self.buf.push('[');
|
|
1296
|
+
self.expr(index);
|
|
1297
|
+
self.buf.push_str("] = ");
|
|
1298
|
+
self.expr(value);
|
|
1299
|
+
}
|
|
1300
|
+
Expr::ArrowFunction { params, body, .. } => {
|
|
1301
|
+
self.buf.push('(');
|
|
1302
|
+
self.param_list(params, &None);
|
|
1303
|
+
self.buf.push_str(") => ");
|
|
1304
|
+
match body {
|
|
1305
|
+
// A bare object-literal body must be parenthesized, else `=> {`
|
|
1306
|
+
// re-parses as a block.
|
|
1307
|
+
ArrowBody::Expr(e) => {
|
|
1308
|
+
if matches!(e.as_ref(), Expr::Object { .. }) {
|
|
1309
|
+
self.buf.push('(');
|
|
1310
|
+
self.expr(e);
|
|
1311
|
+
self.buf.push(')');
|
|
1312
|
+
} else {
|
|
1313
|
+
self.expr(e);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
// Indent the block body relative to the arrow's own line
|
|
1317
|
+
// (`self.depth`), printed inline after `=> ` (no leading indent).
|
|
1318
|
+
ArrowBody::Block(b) => self.stmt_inline_or_block(b, self.depth),
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
Expr::TemplateLiteral { quasis, exprs, .. } => {
|
|
1322
|
+
self.buf.push('`');
|
|
1323
|
+
for (i, q) in quasis.iter().enumerate() {
|
|
1324
|
+
self.buf.push_str(&escape_template(q.as_ref()));
|
|
1325
|
+
if i < exprs.len() {
|
|
1326
|
+
self.buf.push_str("${");
|
|
1327
|
+
self.expr(&exprs[i]);
|
|
1328
|
+
self.buf.push('}');
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
self.buf.push('`');
|
|
1332
|
+
}
|
|
1333
|
+
Expr::Await { operand, .. } => {
|
|
1334
|
+
self.buf.push_str("await ");
|
|
1335
|
+
self.child(operand, PREC_POSTFIX);
|
|
1336
|
+
}
|
|
1337
|
+
Expr::JsxElement {
|
|
1338
|
+
tag,
|
|
1339
|
+
props,
|
|
1340
|
+
children,
|
|
1341
|
+
..
|
|
1342
|
+
} => {
|
|
1343
|
+
self.buf.push('<');
|
|
1344
|
+
self.buf.push_str(tag.as_ref());
|
|
1345
|
+
for pr in props {
|
|
1346
|
+
match pr {
|
|
1347
|
+
JsxProp::Attr { name, value } => {
|
|
1348
|
+
self.buf.push(' ');
|
|
1349
|
+
self.buf.push_str(name.as_ref());
|
|
1350
|
+
match value {
|
|
1351
|
+
JsxAttrValue::String(s) => {
|
|
1352
|
+
self.buf.push('=');
|
|
1353
|
+
self.string_lit(s.as_ref());
|
|
1354
|
+
}
|
|
1355
|
+
JsxAttrValue::Expr(e) => {
|
|
1356
|
+
self.buf.push_str("={");
|
|
1357
|
+
self.expr(e);
|
|
1358
|
+
self.buf.push('}');
|
|
1359
|
+
}
|
|
1360
|
+
JsxAttrValue::ImplicitTrue => {}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
JsxProp::Spread(e) => {
|
|
1364
|
+
self.buf.push_str(" {...");
|
|
1365
|
+
self.expr(e);
|
|
1366
|
+
self.buf.push_str("} ");
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if children.is_empty() {
|
|
1371
|
+
self.buf.push_str(" />");
|
|
1372
|
+
} else {
|
|
1373
|
+
let compact = children.len() == 1
|
|
1374
|
+
&& matches!(
|
|
1375
|
+
&children[0],
|
|
1376
|
+
JsxChild::Text(t) if !t.as_ref().contains('\n')
|
|
1377
|
+
);
|
|
1378
|
+
if compact {
|
|
1379
|
+
self.buf.push('>');
|
|
1380
|
+
if let JsxChild::Text(t) = &children[0] {
|
|
1381
|
+
self.buf.push_str(t.as_ref());
|
|
1382
|
+
}
|
|
1383
|
+
self.buf.push_str("</");
|
|
1384
|
+
self.buf.push_str(tag.as_ref());
|
|
1385
|
+
self.buf.push('>');
|
|
1386
|
+
} else {
|
|
1387
|
+
self.buf.push('>');
|
|
1388
|
+
self.buf.push('\n');
|
|
1389
|
+
for ch in children {
|
|
1390
|
+
self.buf.push_str(" ");
|
|
1391
|
+
match ch {
|
|
1392
|
+
JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
|
|
1393
|
+
JsxChild::Expr(e) => {
|
|
1394
|
+
self.buf.push('{');
|
|
1395
|
+
self.expr(e);
|
|
1396
|
+
self.buf.push('}');
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
self.buf.push('\n');
|
|
1400
|
+
}
|
|
1401
|
+
self.buf.push_str(" </");
|
|
1402
|
+
self.buf.push_str(tag.as_ref());
|
|
1403
|
+
self.buf.push('>');
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
Expr::JsxFragment { children, .. } => {
|
|
1408
|
+
self.buf.push_str("<>");
|
|
1409
|
+
if children.is_empty() {
|
|
1410
|
+
self.buf.push_str("</>");
|
|
1411
|
+
} else {
|
|
1412
|
+
let compact = children.len() == 1
|
|
1413
|
+
&& matches!(
|
|
1414
|
+
&children[0],
|
|
1415
|
+
JsxChild::Text(t) if !t.as_ref().contains('\n')
|
|
1416
|
+
);
|
|
1417
|
+
if compact {
|
|
1418
|
+
if let JsxChild::Text(t) = &children[0] {
|
|
1419
|
+
self.buf.push_str(t.as_ref());
|
|
1420
|
+
}
|
|
1421
|
+
self.buf.push_str("</>");
|
|
1422
|
+
} else {
|
|
1423
|
+
self.buf.push('\n');
|
|
1424
|
+
for ch in children {
|
|
1425
|
+
self.buf.push_str(" ");
|
|
1426
|
+
match ch {
|
|
1427
|
+
JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
|
|
1428
|
+
JsxChild::Expr(e) => {
|
|
1429
|
+
self.buf.push('{');
|
|
1430
|
+
self.expr(e);
|
|
1431
|
+
self.buf.push('}');
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
self.buf.push('\n');
|
|
1435
|
+
}
|
|
1436
|
+
self.buf.push_str("</>");
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
Expr::NativeModuleLoad {
|
|
1441
|
+
spec, export_name, ..
|
|
1442
|
+
} => {
|
|
1443
|
+
self.buf.push_str("import { ");
|
|
1444
|
+
self.buf.push_str(export_name.as_ref());
|
|
1445
|
+
self.buf.push_str(" } from ");
|
|
1446
|
+
self.string_lit(spec.as_ref());
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
fn string_lit(&mut self, s: &str) {
|
|
1452
|
+
self.buf.push('"');
|
|
1453
|
+
for c in s.chars() {
|
|
1454
|
+
match c {
|
|
1455
|
+
'\\' => self.buf.push_str("\\\\"),
|
|
1456
|
+
'"' => self.buf.push_str("\\\""),
|
|
1457
|
+
'\n' => self.buf.push_str("\\n"),
|
|
1458
|
+
'\r' => self.buf.push_str("\\r"),
|
|
1459
|
+
'\t' => self.buf.push_str("\\t"),
|
|
1460
|
+
c if c.is_control() => self.buf.push_str(&format!("\\u{:04x}", c as u32)),
|
|
1461
|
+
c => self.buf.push(c),
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
self.buf.push('"');
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
fn escape_template(s: &str) -> String {
|
|
1469
|
+
s.replace('\\', "\\\\")
|
|
1470
|
+
.replace('`', "\\`")
|
|
1471
|
+
.replace('$', "\\$")
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Expression precedence levels (higher binds tighter), mirroring the parser's
|
|
1475
|
+
// descent chain (parse_conditional → … → parse_unary → primary). Used to decide
|
|
1476
|
+
// when a sub-expression needs parentheses.
|
|
1477
|
+
const PREC_CONDITIONAL: u8 = 1;
|
|
1478
|
+
const PREC_NULLISH: u8 = 2;
|
|
1479
|
+
const PREC_POSTFIX: u8 = 15; // call / member / index — tighter than any operator
|
|
1480
|
+
const PREC_ATOM: u8 = 16; // literals, identifiers, array/object/template/jsx
|
|
1481
|
+
|
|
1482
|
+
/// True when a comment is exactly the ignore marker — `// tish-fmt-ignore` or the
|
|
1483
|
+
/// `/* tish-fmt-ignore */` block form.
|
|
1484
|
+
fn is_ignore_marker(text: &str) -> bool {
|
|
1485
|
+
let t = text.trim();
|
|
1486
|
+
let inner = if let Some(r) = t.strip_prefix("//") {
|
|
1487
|
+
r
|
|
1488
|
+
} else if let Some(r) = t.strip_prefix("/*").and_then(|x| x.strip_suffix("*/")) {
|
|
1489
|
+
r
|
|
1490
|
+
} else {
|
|
1491
|
+
return false;
|
|
1492
|
+
};
|
|
1493
|
+
inner.trim() == IGNORE_MARKER
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/// Precedence of a binary operator, matching the parser (parser.rs `parse_*`).
|
|
1497
|
+
fn binop_prec(op: BinOp) -> u8 {
|
|
1498
|
+
match op {
|
|
1499
|
+
BinOp::Or => 3,
|
|
1500
|
+
BinOp::And => 4,
|
|
1501
|
+
BinOp::BitOr => 5,
|
|
1502
|
+
BinOp::BitXor => 6,
|
|
1503
|
+
BinOp::BitAnd => 7,
|
|
1504
|
+
BinOp::Shl | BinOp::Shr | BinOp::UShr => 8,
|
|
1505
|
+
BinOp::Eq | BinOp::Ne | BinOp::StrictEq | BinOp::StrictNe => 9,
|
|
1506
|
+
BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge | BinOp::In => 10,
|
|
1507
|
+
BinOp::Add | BinOp::Sub => 11,
|
|
1508
|
+
BinOp::Mul | BinOp::Div | BinOp::Mod => 12,
|
|
1509
|
+
BinOp::Pow => 13,
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/// Precedence of an expression's outermost operator (atoms bind tightest).
|
|
1514
|
+
fn expr_prec(e: &Expr) -> u8 {
|
|
1515
|
+
match e {
|
|
1516
|
+
Expr::Assign { .. }
|
|
1517
|
+
| Expr::MemberAssign { .. }
|
|
1518
|
+
| Expr::IndexAssign { .. }
|
|
1519
|
+
| Expr::CompoundAssign { .. }
|
|
1520
|
+
| Expr::LogicalAssign { .. } => 0,
|
|
1521
|
+
Expr::Conditional { .. } => PREC_CONDITIONAL,
|
|
1522
|
+
Expr::NullishCoalesce { .. } => PREC_NULLISH,
|
|
1523
|
+
Expr::Binary { op, .. } => binop_prec(*op),
|
|
1524
|
+
Expr::Unary { .. }
|
|
1525
|
+
| Expr::TypeOf { .. }
|
|
1526
|
+
| Expr::Delete { .. }
|
|
1527
|
+
| Expr::Await { .. }
|
|
1528
|
+
| Expr::PrefixInc { .. }
|
|
1529
|
+
| Expr::PrefixDec { .. } => 14,
|
|
1530
|
+
Expr::Call { .. }
|
|
1531
|
+
| Expr::New { .. }
|
|
1532
|
+
| Expr::Member { .. }
|
|
1533
|
+
| Expr::Index { .. }
|
|
1534
|
+
| Expr::PostfixInc { .. }
|
|
1535
|
+
| Expr::PostfixDec { .. } => PREC_POSTFIX,
|
|
1536
|
+
Expr::ArrowFunction { .. } => PREC_ATOM,
|
|
1537
|
+
_ => PREC_ATOM,
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
fn binop(op: BinOp) -> &'static str {
|
|
1542
|
+
match op {
|
|
1543
|
+
BinOp::Add => "+",
|
|
1544
|
+
BinOp::Sub => "-",
|
|
1545
|
+
BinOp::Mul => "*",
|
|
1546
|
+
BinOp::Div => "/",
|
|
1547
|
+
BinOp::Mod => "%",
|
|
1548
|
+
BinOp::Pow => "**",
|
|
1549
|
+
BinOp::Eq => "==",
|
|
1550
|
+
BinOp::Ne => "!=",
|
|
1551
|
+
BinOp::StrictEq => "===",
|
|
1552
|
+
BinOp::StrictNe => "!==",
|
|
1553
|
+
BinOp::Lt => "<",
|
|
1554
|
+
BinOp::Le => "<=",
|
|
1555
|
+
BinOp::Gt => ">",
|
|
1556
|
+
BinOp::Ge => ">=",
|
|
1557
|
+
BinOp::And => "&&",
|
|
1558
|
+
BinOp::Or => "||",
|
|
1559
|
+
BinOp::BitAnd => "&",
|
|
1560
|
+
BinOp::BitOr => "|",
|
|
1561
|
+
BinOp::BitXor => "^",
|
|
1562
|
+
BinOp::Shl => "<<",
|
|
1563
|
+
BinOp::Shr => ">>",
|
|
1564
|
+
BinOp::UShr => ">>>",
|
|
1565
|
+
BinOp::In => "in",
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
fn compound(op: CompoundOp) -> &'static str {
|
|
1570
|
+
match op {
|
|
1571
|
+
CompoundOp::Add => " += ",
|
|
1572
|
+
CompoundOp::Sub => " -= ",
|
|
1573
|
+
CompoundOp::Mul => " *= ",
|
|
1574
|
+
CompoundOp::Div => " /= ",
|
|
1575
|
+
CompoundOp::Mod => " %= ",
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
fn logical_assign(op: LogicalAssignOp) -> &'static str {
|
|
1580
|
+
match op {
|
|
1581
|
+
LogicalAssignOp::AndAnd => " &&= ",
|
|
1582
|
+
LogicalAssignOp::OrOr => " ||= ",
|
|
1583
|
+
LogicalAssignOp::Nullish => " ??= ",
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/// Recover `//` and `/* */` comments from source, in order, with their 1-based
|
|
1588
|
+
/// (line, col) positions — matching `ast::Span`'s convention so the printer can
|
|
1589
|
+
/// re-insert them by position. The lexer discards comments, so this is a separate
|
|
1590
|
+
/// pass; it skips string and template literals so a `//` inside `"…"` or `` `…` ``
|
|
1591
|
+
/// is never mistaken for a comment. (Tish has no regex literals, so a bare `/` is
|
|
1592
|
+
/// only division or a comment opener — no further disambiguation is needed.)
|
|
1593
|
+
fn scan_comments(source: &str) -> (Vec<CommentTok>, BraceMap, Vec<(usize, usize)>) {
|
|
1594
|
+
let mut s = Scanner {
|
|
1595
|
+
chars: source.chars().collect(),
|
|
1596
|
+
i: 0,
|
|
1597
|
+
line: 1,
|
|
1598
|
+
col: 1,
|
|
1599
|
+
seen_nonws: false,
|
|
1600
|
+
out: Vec::new(),
|
|
1601
|
+
brace_stack: Vec::new(),
|
|
1602
|
+
braces: BraceMap::new(),
|
|
1603
|
+
bracket_open: Vec::new(),
|
|
1604
|
+
bracket_spans: Vec::new(),
|
|
1605
|
+
};
|
|
1606
|
+
s.scan_code(false);
|
|
1607
|
+
(s.out, s.braces, s.bracket_spans)
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
struct Scanner {
|
|
1611
|
+
chars: Vec<char>,
|
|
1612
|
+
i: usize,
|
|
1613
|
+
line: usize,
|
|
1614
|
+
col: usize,
|
|
1615
|
+
/// Whether any non-whitespace has appeared on the current line yet.
|
|
1616
|
+
seen_nonws: bool,
|
|
1617
|
+
out: Vec<CommentTok>,
|
|
1618
|
+
/// Open `{` positions awaiting their match, for building `braces`.
|
|
1619
|
+
brace_stack: Vec<(usize, usize)>,
|
|
1620
|
+
/// `{`→`}` position pairs (see [`BraceMap`]).
|
|
1621
|
+
braces: BraceMap,
|
|
1622
|
+
/// Open lines of every bracket (`{ [ (`) awaiting its match.
|
|
1623
|
+
bracket_open: Vec<usize>,
|
|
1624
|
+
/// `(open_line, close_line)` for every bracket pair — lets an ignored statement's
|
|
1625
|
+
/// verbatim slice extend over the full lines of any `{}`/`[]`/`()` it opens.
|
|
1626
|
+
bracket_spans: Vec<(usize, usize)>,
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
impl Scanner {
|
|
1630
|
+
fn peek(&self) -> Option<char> {
|
|
1631
|
+
self.chars.get(self.i).copied()
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
fn peek2(&self) -> Option<char> {
|
|
1635
|
+
self.chars.get(self.i + 1).copied()
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/// Consume one char, tracking line/col like the lexer (col resets after `\n`).
|
|
1639
|
+
fn bump(&mut self) -> Option<char> {
|
|
1640
|
+
let c = *self.chars.get(self.i)?;
|
|
1641
|
+
self.i += 1;
|
|
1642
|
+
if c == '\n' {
|
|
1643
|
+
self.line += 1;
|
|
1644
|
+
self.col = 1;
|
|
1645
|
+
self.seen_nonws = false;
|
|
1646
|
+
} else {
|
|
1647
|
+
self.col += 1;
|
|
1648
|
+
}
|
|
1649
|
+
Some(c)
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/// Scan ordinary code, collecting comments. When `stop_at_close_brace` is set
|
|
1653
|
+
/// (inside a `${ … }` template interpolation), returns at the matching `}`
|
|
1654
|
+
/// without consuming it.
|
|
1655
|
+
fn scan_code(&mut self, stop_at_close_brace: bool) {
|
|
1656
|
+
let mut depth = 0usize;
|
|
1657
|
+
while let Some(c) = self.peek() {
|
|
1658
|
+
match c {
|
|
1659
|
+
'}' if stop_at_close_brace && depth == 0 => return,
|
|
1660
|
+
'{' => {
|
|
1661
|
+
self.brace_stack.push((self.line, self.col));
|
|
1662
|
+
self.bracket_open.push(self.line);
|
|
1663
|
+
depth += 1;
|
|
1664
|
+
self.bump();
|
|
1665
|
+
self.seen_nonws = true;
|
|
1666
|
+
}
|
|
1667
|
+
'}' => {
|
|
1668
|
+
let close = (self.line, self.col);
|
|
1669
|
+
if let Some(open) = self.brace_stack.pop() {
|
|
1670
|
+
self.braces.insert(open, close);
|
|
1671
|
+
}
|
|
1672
|
+
if let Some(open_line) = self.bracket_open.pop() {
|
|
1673
|
+
self.bracket_spans.push((open_line, self.line));
|
|
1674
|
+
}
|
|
1675
|
+
depth = depth.saturating_sub(1);
|
|
1676
|
+
self.bump();
|
|
1677
|
+
self.seen_nonws = true;
|
|
1678
|
+
}
|
|
1679
|
+
'(' | '[' => {
|
|
1680
|
+
self.bracket_open.push(self.line);
|
|
1681
|
+
self.bump();
|
|
1682
|
+
self.seen_nonws = true;
|
|
1683
|
+
}
|
|
1684
|
+
')' | ']' => {
|
|
1685
|
+
if let Some(open_line) = self.bracket_open.pop() {
|
|
1686
|
+
self.bracket_spans.push((open_line, self.line));
|
|
1687
|
+
}
|
|
1688
|
+
self.bump();
|
|
1689
|
+
self.seen_nonws = true;
|
|
1690
|
+
}
|
|
1691
|
+
'"' | '\'' => self.scan_string(c),
|
|
1692
|
+
'`' => {
|
|
1693
|
+
self.bump();
|
|
1694
|
+
self.seen_nonws = true;
|
|
1695
|
+
self.scan_template();
|
|
1696
|
+
}
|
|
1697
|
+
'/' if self.peek2() == Some('/') => self.scan_line_comment(),
|
|
1698
|
+
'/' if self.peek2() == Some('*') => self.scan_block_comment(),
|
|
1699
|
+
' ' | '\t' | '\r' | '\n' => {
|
|
1700
|
+
self.bump();
|
|
1701
|
+
}
|
|
1702
|
+
_ => {
|
|
1703
|
+
self.bump();
|
|
1704
|
+
self.seen_nonws = true;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
fn scan_string(&mut self, quote: char) {
|
|
1711
|
+
self.bump(); // opening quote
|
|
1712
|
+
self.seen_nonws = true;
|
|
1713
|
+
while let Some(c) = self.bump() {
|
|
1714
|
+
if c == '\\' {
|
|
1715
|
+
self.bump(); // escaped char
|
|
1716
|
+
} else if c == quote {
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/// Scan a template literal body (opening backtick already consumed), recursing
|
|
1723
|
+
/// into `${ … }` interpolations so their strings/comments are handled too.
|
|
1724
|
+
fn scan_template(&mut self) {
|
|
1725
|
+
while let Some(c) = self.peek() {
|
|
1726
|
+
match c {
|
|
1727
|
+
'`' => {
|
|
1728
|
+
self.bump();
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
'\\' => {
|
|
1732
|
+
self.bump();
|
|
1733
|
+
self.bump();
|
|
1734
|
+
}
|
|
1735
|
+
'$' if self.peek2() == Some('{') => {
|
|
1736
|
+
self.bump();
|
|
1737
|
+
self.bump();
|
|
1738
|
+
self.scan_code(true);
|
|
1739
|
+
if self.peek() == Some('}') {
|
|
1740
|
+
self.bump();
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
_ => {
|
|
1744
|
+
self.bump();
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
fn scan_line_comment(&mut self) {
|
|
1751
|
+
let start = (self.line, self.col);
|
|
1752
|
+
let own_line = !self.seen_nonws;
|
|
1753
|
+
let mut text = String::new();
|
|
1754
|
+
while let Some(c) = self.peek() {
|
|
1755
|
+
if c == '\n' {
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
text.push(c);
|
|
1759
|
+
self.bump();
|
|
1760
|
+
}
|
|
1761
|
+
self.out.push(CommentTok {
|
|
1762
|
+
start,
|
|
1763
|
+
text: text.trim_end().to_string(),
|
|
1764
|
+
own_line,
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
fn scan_block_comment(&mut self) {
|
|
1769
|
+
let start = (self.line, self.col);
|
|
1770
|
+
let own_line = !self.seen_nonws;
|
|
1771
|
+
let mut text = String::new();
|
|
1772
|
+
let mut depth = 0usize;
|
|
1773
|
+
loop {
|
|
1774
|
+
if self.peek() == Some('/') && self.peek2() == Some('*') {
|
|
1775
|
+
text.push('/');
|
|
1776
|
+
text.push('*');
|
|
1777
|
+
self.bump();
|
|
1778
|
+
self.bump();
|
|
1779
|
+
depth += 1;
|
|
1780
|
+
} else if self.peek() == Some('*') && self.peek2() == Some('/') {
|
|
1781
|
+
text.push('*');
|
|
1782
|
+
text.push('/');
|
|
1783
|
+
self.bump();
|
|
1784
|
+
self.bump();
|
|
1785
|
+
depth -= 1;
|
|
1786
|
+
if depth == 0 {
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
} else {
|
|
1790
|
+
match self.bump() {
|
|
1791
|
+
Some(c) => text.push(c),
|
|
1792
|
+
None => break,
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
self.out.push(CommentTok {
|
|
1797
|
+
start,
|
|
1798
|
+
text,
|
|
1799
|
+
own_line,
|
|
1800
|
+
});
|
|
1801
|
+
self.seen_nonws = true;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
#[cfg(test)]
|
|
1806
|
+
mod tests {
|
|
1807
|
+
use super::*;
|
|
1808
|
+
|
|
1809
|
+
#[test]
|
|
1810
|
+
fn round_trip_simple() {
|
|
1811
|
+
let src = "fn add(a, b) {\n return a + b\n}\n";
|
|
1812
|
+
let out = format_source(src).unwrap();
|
|
1813
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
#[test]
|
|
1817
|
+
fn jsx_multiline_when_mixed_children() {
|
|
1818
|
+
let src = "let x = <div>a{b}</div>\n";
|
|
1819
|
+
let out = format_source(src).unwrap();
|
|
1820
|
+
assert!(
|
|
1821
|
+
out.contains('\n'),
|
|
1822
|
+
"expected line breaks in formatted JSX: {out:?}"
|
|
1823
|
+
);
|
|
1824
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
#[test]
|
|
1828
|
+
fn preserves_leading_and_section_comments() {
|
|
1829
|
+
let src = "\
|
|
1830
|
+
// file header
|
|
1831
|
+
// second line
|
|
1832
|
+
let a = 1
|
|
1833
|
+
|
|
1834
|
+
// a section
|
|
1835
|
+
let b = 2
|
|
1836
|
+
";
|
|
1837
|
+
let out = format_source(src).unwrap();
|
|
1838
|
+
assert!(out.contains("// file header"), "{out:?}");
|
|
1839
|
+
assert!(out.contains("// second line"), "{out:?}");
|
|
1840
|
+
assert!(out.contains("// a section"), "{out:?}");
|
|
1841
|
+
// header hugs the statement it documents; blank line before the section.
|
|
1842
|
+
assert!(out.contains("// a section\nlet b = 2"), "{out:?}");
|
|
1843
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
#[test]
|
|
1847
|
+
fn preserves_trailing_comment() {
|
|
1848
|
+
let src = "let a = 1 // inline note\n";
|
|
1849
|
+
let out = format_source(src).unwrap();
|
|
1850
|
+
assert!(out.contains("let a = 1 // inline note"), "{out:?}");
|
|
1851
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
#[test]
|
|
1855
|
+
fn preserves_comments_inside_block() {
|
|
1856
|
+
let src = "\
|
|
1857
|
+
fn f() {
|
|
1858
|
+
// step one
|
|
1859
|
+
let a = 1
|
|
1860
|
+
// step two
|
|
1861
|
+
return a
|
|
1862
|
+
}
|
|
1863
|
+
";
|
|
1864
|
+
let out = format_source(src).unwrap();
|
|
1865
|
+
assert!(out.contains(" // step one"), "{out:?}");
|
|
1866
|
+
assert!(out.contains(" // step two"), "{out:?}");
|
|
1867
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
#[test]
|
|
1871
|
+
fn preserves_dangling_comment_in_empty_block() {
|
|
1872
|
+
let src = "fn f() {\n // nothing yet\n}\n";
|
|
1873
|
+
let out = format_source(src).unwrap();
|
|
1874
|
+
assert!(out.contains("// nothing yet"), "{out:?}");
|
|
1875
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
#[test]
|
|
1879
|
+
fn preserves_block_comment() {
|
|
1880
|
+
let src = "/* a block comment */\nlet a = 1\n";
|
|
1881
|
+
let out = format_source(src).unwrap();
|
|
1882
|
+
assert!(out.contains("/* a block comment */"), "{out:?}");
|
|
1883
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
#[test]
|
|
1887
|
+
fn double_slash_in_string_is_not_a_comment() {
|
|
1888
|
+
let src = "let url = \"http://example.com\"\n";
|
|
1889
|
+
let out = format_source(src).unwrap();
|
|
1890
|
+
assert_eq!(out, "let url = \"http://example.com\"\n", "{out:?}");
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
#[test]
|
|
1894
|
+
fn idempotent_with_comments() {
|
|
1895
|
+
let src = "\
|
|
1896
|
+
// header
|
|
1897
|
+
let a = 1
|
|
1898
|
+
|
|
1899
|
+
fn f() {
|
|
1900
|
+
// body note
|
|
1901
|
+
let b = 2 // trailing
|
|
1902
|
+
return b
|
|
1903
|
+
}
|
|
1904
|
+
";
|
|
1905
|
+
let once = format_source(src).unwrap();
|
|
1906
|
+
let twice = format_source(&once).unwrap();
|
|
1907
|
+
assert_eq!(
|
|
1908
|
+
once, twice,
|
|
1909
|
+
"formatting is not idempotent:\n{once}\n---\n{twice}"
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
#[test]
|
|
1914
|
+
fn collapses_multiple_blank_lines_to_one() {
|
|
1915
|
+
let src = "let a = 1\n\n\n\nlet b = 2\n";
|
|
1916
|
+
let out = format_source(src).unwrap();
|
|
1917
|
+
assert_eq!(out, "let a = 1\n\nlet b = 2\n", "{out:?}");
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
#[test]
|
|
1921
|
+
fn comment_after_inner_block_stays_at_outer_level() {
|
|
1922
|
+
// Regression: a comment after an inner block's `}` belongs to the outer
|
|
1923
|
+
// scope, not inside the inner block (the parser's block span.end overshoots
|
|
1924
|
+
// past `}`, which previously pulled this comment in and broke idempotency).
|
|
1925
|
+
let src = "\
|
|
1926
|
+
fn f() {
|
|
1927
|
+
if (x) {
|
|
1928
|
+
a()
|
|
1929
|
+
}
|
|
1930
|
+
// after the if
|
|
1931
|
+
b()
|
|
1932
|
+
}
|
|
1933
|
+
";
|
|
1934
|
+
let out = format_source(src).unwrap();
|
|
1935
|
+
assert!(out.contains(" // after the if\n b()"), "{out:?}");
|
|
1936
|
+
let twice = format_source(&out).unwrap();
|
|
1937
|
+
assert_eq!(out, twice, "not idempotent:\n{out}\n---\n{twice}");
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
#[test]
|
|
1941
|
+
fn trailing_comment_inside_block_stays_inside() {
|
|
1942
|
+
let src = "\
|
|
1943
|
+
fn f() {
|
|
1944
|
+
a()
|
|
1945
|
+
// last note
|
|
1946
|
+
}
|
|
1947
|
+
";
|
|
1948
|
+
let out = format_source(src).unwrap();
|
|
1949
|
+
assert!(out.contains(" // last note\n}"), "{out:?}");
|
|
1950
|
+
let twice = format_source(&out).unwrap();
|
|
1951
|
+
assert_eq!(out, twice, "{out:?}");
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
#[test]
|
|
1955
|
+
fn preserves_operator_grouping() {
|
|
1956
|
+
// The AST has no parenthesis nodes, so the printer must re-derive parens
|
|
1957
|
+
// from precedence. Each `want` must re-parse to the same tree.
|
|
1958
|
+
let cases = [
|
|
1959
|
+
("let a = 1 / (b - c)\n", "1 / (b - c)"),
|
|
1960
|
+
("let a = 0 - (x + y + z)\n", "0 - (x + y + z)"),
|
|
1961
|
+
("let a = (1 - (p + q)) * s\n", "(1 - (p + q)) * s"),
|
|
1962
|
+
("let a = b * c + d\n", "b * c + d"),
|
|
1963
|
+
("let a = b + c * d\n", "b + c * d"),
|
|
1964
|
+
("let a = (b + c) * d\n", "(b + c) * d"),
|
|
1965
|
+
("let a = 1 - 2 - 3\n", "1 - 2 - 3"),
|
|
1966
|
+
("let a = 1 - (2 - 3)\n", "1 - (2 - 3)"),
|
|
1967
|
+
("let a = -(b + c)\n", "-(b + c)"),
|
|
1968
|
+
("let a = !(b && c)\n", "!(b && c)"),
|
|
1969
|
+
("let a = (a | b) & c\n", "(a | b) & c"),
|
|
1970
|
+
];
|
|
1971
|
+
for (src, want) in cases {
|
|
1972
|
+
let out = format_source(src).unwrap();
|
|
1973
|
+
assert!(
|
|
1974
|
+
out.contains(want),
|
|
1975
|
+
"for {src:?} expected to contain {want:?}, got {out:?}"
|
|
1976
|
+
);
|
|
1977
|
+
let twice = format_source(&out).unwrap();
|
|
1978
|
+
assert_eq!(out, twice, "not idempotent for {src:?}: {out:?}");
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
#[test]
|
|
1983
|
+
fn nested_control_flow_brace_spacing() {
|
|
1984
|
+
let src = "fn f() {\n if (a) {\n b()\n }\n}\n";
|
|
1985
|
+
let out = format_source(src).unwrap();
|
|
1986
|
+
assert!(!out.contains(") {"), "double-space before brace: {out:?}");
|
|
1987
|
+
assert!(out.contains(" if (a) {\n"), "{out:?}");
|
|
1988
|
+
let twice = format_source(&out).unwrap();
|
|
1989
|
+
assert_eq!(out, twice, "{out:?}");
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
#[test]
|
|
1993
|
+
fn short_object_stays_inline() {
|
|
1994
|
+
let src = "let a = { x: 1, y: 2 }\n";
|
|
1995
|
+
assert_eq!(format_source(src).unwrap(), src);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
#[test]
|
|
1999
|
+
fn long_object_breaks_one_per_line() {
|
|
2000
|
+
let props: Vec<String> = (0..12).map(|i| format!("key{i}: {i}")).collect();
|
|
2001
|
+
let src = format!("let a = {{ {} }}\n", props.join(", "));
|
|
2002
|
+
let out = format_source(&src).unwrap();
|
|
2003
|
+
assert!(
|
|
2004
|
+
out.contains("let a = {\n key0: 0,\n"),
|
|
2005
|
+
"expected broken object:\n{out}"
|
|
2006
|
+
);
|
|
2007
|
+
assert!(out.ends_with("\n}\n"), "{out:?}");
|
|
2008
|
+
// idempotent and re-parses
|
|
2009
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent:\n{out}");
|
|
2010
|
+
tishlang_parser::parse(&out).unwrap();
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
#[test]
|
|
2014
|
+
fn long_array_breaks_one_per_line() {
|
|
2015
|
+
let elems: Vec<String> = (0..40).map(|i| i.to_string()).collect();
|
|
2016
|
+
let src = format!("let a = [{}]\n", elems.join(", "));
|
|
2017
|
+
let out = format_source(&src).unwrap();
|
|
2018
|
+
assert!(
|
|
2019
|
+
out.contains("[\n 0,\n 1,\n"),
|
|
2020
|
+
"expected broken array:\n{out}"
|
|
2021
|
+
);
|
|
2022
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
#[test]
|
|
2026
|
+
fn last_arg_object_hugs_parens() {
|
|
2027
|
+
let props: Vec<String> = (0..20).map(|i| format!("k{i}: {i}")).collect();
|
|
2028
|
+
let src = format!("f(a, {{ {} }})\n", props.join(", "));
|
|
2029
|
+
let out = format_source(&src).unwrap();
|
|
2030
|
+
assert!(
|
|
2031
|
+
out.starts_with("f(a, {\n"),
|
|
2032
|
+
"expected hugged object:\n{out}"
|
|
2033
|
+
);
|
|
2034
|
+
assert!(out.contains("\n})\n"), "expected hugged close:\n{out}");
|
|
2035
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
2036
|
+
tishlang_parser::parse(&out).unwrap();
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
#[test]
|
|
2040
|
+
fn nested_containers_indent_progressively() {
|
|
2041
|
+
let inner: Vec<String> = (0..16).map(|i| format!("p{i}: {i}")).collect();
|
|
2042
|
+
let src = format!("let a = {{ outer: {{ {} }} }}\n", inner.join(", "));
|
|
2043
|
+
let out = format_source(&src).unwrap();
|
|
2044
|
+
// outer object at 2 spaces, inner props at 4 spaces
|
|
2045
|
+
assert!(
|
|
2046
|
+
out.contains("\n outer: {\n p0: 0,"),
|
|
2047
|
+
"expected nested indent:\n{out}"
|
|
2048
|
+
);
|
|
2049
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
#[test]
|
|
2053
|
+
fn arrow_block_body_indents_to_context() {
|
|
2054
|
+
let src =
|
|
2055
|
+
"export fn make() {\n let s = {}\n s.go = (x) => {\n foo(x)\n }\n return s\n}\n";
|
|
2056
|
+
let out = format_source(src).unwrap();
|
|
2057
|
+
// Arrow body one level past its `s.go` line (4 spaces); closing `}` at 2.
|
|
2058
|
+
assert!(
|
|
2059
|
+
out.contains(" s.go = (x) => {\n foo(x)\n }\n"),
|
|
2060
|
+
"arrow body mis-indented:\n{out}"
|
|
2061
|
+
);
|
|
2062
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
#[test]
|
|
2066
|
+
fn ignore_marker_preserves_statement_verbatim() {
|
|
2067
|
+
let src = "// tish-fmt-ignore\nexport fn m(out) {\n out[0]=1; out[1]=0\n out[2]=0; out[3]=1\n}\n\nlet x = {a:1,b:2}\n";
|
|
2068
|
+
let out = format_source(src).unwrap();
|
|
2069
|
+
// The ignored function keeps its exact source (aligned, no spaces around `=`).
|
|
2070
|
+
assert!(
|
|
2071
|
+
out.contains("export fn m(out) {\n out[0]=1; out[1]=0\n out[2]=0; out[3]=1\n}"),
|
|
2072
|
+
"ignored block not verbatim:\n{out}"
|
|
2073
|
+
);
|
|
2074
|
+
// Surrounding code is still formatted normally.
|
|
2075
|
+
assert!(
|
|
2076
|
+
out.contains("let x = { a: 1, b: 2 }"),
|
|
2077
|
+
"neighbour not formatted:\n{out}"
|
|
2078
|
+
);
|
|
2079
|
+
// The marker itself is kept.
|
|
2080
|
+
assert!(out.contains("// tish-fmt-ignore\n"), "{out}");
|
|
2081
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent:\n{out}");
|
|
2082
|
+
tishlang_parser::parse(&out).unwrap();
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
#[test]
|
|
2086
|
+
fn ignore_marker_block_comment_form() {
|
|
2087
|
+
let src = "/* tish-fmt-ignore */\nlet a = [1,2, 3]\n";
|
|
2088
|
+
let out = format_source(src).unwrap();
|
|
2089
|
+
assert!(
|
|
2090
|
+
out.contains("let a = [1,2, 3]"),
|
|
2091
|
+
"expected verbatim array:\n{out}"
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
#[test]
|
|
2096
|
+
fn ignore_in_switch_case_does_not_overrun() {
|
|
2097
|
+
// Regression: an ignored last statement of a non-final case must not swallow
|
|
2098
|
+
// the following cases / closing brace / trailing code.
|
|
2099
|
+
let src = "switch (x) {\n case 1:\n // tish-fmt-ignore\n foo( a,b )\n case 2:\n bar()\n}\nlet after = 1\n";
|
|
2100
|
+
let out = format_source(src).unwrap();
|
|
2101
|
+
assert!(out.contains("foo( a,b )"), "ignored not verbatim:\n{out}");
|
|
2102
|
+
assert!(out.contains("case 2:"), "case 2 was swallowed:\n{out}");
|
|
2103
|
+
assert!(out.contains("bar()"), "case 2 body swallowed:\n{out}");
|
|
2104
|
+
assert!(
|
|
2105
|
+
out.contains("let after = 1"),
|
|
2106
|
+
"trailing code swallowed:\n{out}"
|
|
2107
|
+
);
|
|
2108
|
+
tishlang_parser::parse(&out).unwrap();
|
|
2109
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent:\n{out}");
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
#[test]
|
|
2113
|
+
fn ignore_preserves_multiline_bracket_statement() {
|
|
2114
|
+
// The verbatim extent must follow `[]`/`()`, not just `{}`.
|
|
2115
|
+
let src = "// tish-fmt-ignore\nlet m = [\n 1,2,\n 3,4\n]\nlet n = 5\n";
|
|
2116
|
+
let out = format_source(src).unwrap();
|
|
2117
|
+
assert!(
|
|
2118
|
+
out.contains("let m = [\n 1,2,\n 3,4\n]"),
|
|
2119
|
+
"multiline array truncated:\n{out}"
|
|
2120
|
+
);
|
|
2121
|
+
assert!(out.contains("let n = 5"), "{out}");
|
|
2122
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
#[test]
|
|
2126
|
+
fn without_marker_is_formatted_normally() {
|
|
2127
|
+
let src = "let a = [1,2, 3]\n";
|
|
2128
|
+
let out = format_source(src).unwrap();
|
|
2129
|
+
assert_eq!(out, "let a = [1, 2, 3]\n", "{out:?}");
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
#[test]
|
|
2133
|
+
fn no_comments_round_trips_without_loss() {
|
|
2134
|
+
let src = "\
|
|
2135
|
+
fn add(a, b) {
|
|
2136
|
+
return a + b
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
let x = add(1, 2)
|
|
2140
|
+
";
|
|
2141
|
+
let out = format_source(src).unwrap();
|
|
2142
|
+
assert_eq!(out, src, "{out:?}");
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
#[test]
|
|
2146
|
+
fn formats_delete_expression() {
|
|
2147
|
+
// Regression: Expr::Delete (the `delete` operator) must be handled by the formatter —
|
|
2148
|
+
// a non-exhaustive `match` here broke the `tish-format` build once the delete feature landed.
|
|
2149
|
+
let src = "fn f(o, k) {\ndelete o.a\ndelete o[\"b\"]\nlet x = delete o[k]\nreturn x\n}\n";
|
|
2150
|
+
let out = format_source(src).unwrap();
|
|
2151
|
+
assert!(out.contains("delete o.a"), "{out}");
|
|
2152
|
+
assert!(out.contains("delete o[\"b\"]"), "{out}");
|
|
2153
|
+
assert!(out.contains("delete o[k]"), "{out}");
|
|
2154
|
+
tishlang_parser::parse(&out).unwrap();
|
|
2155
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent:\n{out}");
|
|
2156
|
+
}
|
|
2157
|
+
}
|