@tishlang/tish-lsp 2.12.0 → 2.35.0
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 +3 -0
- package/bin/tish-lsp +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -613,6 +613,20 @@ impl Printer {
|
|
|
613
613
|
self.buf.push_str(") ");
|
|
614
614
|
self.stmt_inline_or_block(body, level);
|
|
615
615
|
}
|
|
616
|
+
Statement::ForIn {
|
|
617
|
+
name,
|
|
618
|
+
object,
|
|
619
|
+
body,
|
|
620
|
+
..
|
|
621
|
+
} => {
|
|
622
|
+
self.indent(level);
|
|
623
|
+
self.buf.push_str("for (let ");
|
|
624
|
+
self.buf.push_str(name);
|
|
625
|
+
self.buf.push_str(" in ");
|
|
626
|
+
self.expr(object);
|
|
627
|
+
self.buf.push_str(") ");
|
|
628
|
+
self.stmt_inline_or_block(body, level);
|
|
629
|
+
}
|
|
616
630
|
Statement::Return { value, .. } => {
|
|
617
631
|
self.indent(level);
|
|
618
632
|
self.buf.push_str("return");
|
|
@@ -817,6 +831,43 @@ impl Printer {
|
|
|
817
831
|
self.buf.push_str("default ");
|
|
818
832
|
self.expr(e);
|
|
819
833
|
}
|
|
834
|
+
// #305: re-export round-trip — `export { a, b as c } from "m"` / `export * from "m"`
|
|
835
|
+
ExportDeclaration::ReExport {
|
|
836
|
+
specifiers,
|
|
837
|
+
all,
|
|
838
|
+
from,
|
|
839
|
+
..
|
|
840
|
+
} => {
|
|
841
|
+
if *all {
|
|
842
|
+
// `export *` always has a source.
|
|
843
|
+
self.buf.push_str("* from \"");
|
|
844
|
+
self.buf.push_str(from.as_deref().unwrap_or(""));
|
|
845
|
+
self.buf.push('"');
|
|
846
|
+
} else {
|
|
847
|
+
self.buf.push_str("{ ");
|
|
848
|
+
for (i, spec) in specifiers.iter().enumerate() {
|
|
849
|
+
if i > 0 {
|
|
850
|
+
self.buf.push_str(", ");
|
|
851
|
+
}
|
|
852
|
+
if let tishlang_ast::ImportSpecifier::Named { name, alias, .. } =
|
|
853
|
+
spec
|
|
854
|
+
{
|
|
855
|
+
self.buf.push_str(name);
|
|
856
|
+
if let Some(a) = alias {
|
|
857
|
+
self.buf.push_str(" as ");
|
|
858
|
+
self.buf.push_str(a);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
self.buf.push_str(" }");
|
|
863
|
+
// #415: a local named export (`export { a }`) has no `from` clause.
|
|
864
|
+
if let Some(from) = from {
|
|
865
|
+
self.buf.push_str(" from \"");
|
|
866
|
+
self.buf.push_str(from);
|
|
867
|
+
self.buf.push('"');
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
820
871
|
}
|
|
821
872
|
}
|
|
822
873
|
}
|
|
@@ -1127,6 +1178,10 @@ impl Printer {
|
|
|
1127
1178
|
/// Print JSX children inline and verbatim. JSX whitespace is significant in tish, so text is
|
|
1128
1179
|
/// emitted exactly as written and there is no reflow/indentation. A nested element/fragment is
|
|
1129
1180
|
/// printed bare (as a child element); any other expression child gets `{ }`.
|
|
1181
|
+
///
|
|
1182
|
+
/// Do NOT re-indent children by structural depth here: any injected newline/space becomes a
|
|
1183
|
+
/// real `JsxChild::Text` node and changes the rendered output (and breaks idempotency). The
|
|
1184
|
+
/// source's own layout inside the children is preserved as-is via the verbatim text nodes.
|
|
1130
1185
|
fn jsx_children(&mut self, children: &[JsxChild]) {
|
|
1131
1186
|
for ch in children {
|
|
1132
1187
|
match ch {
|
|
@@ -1402,9 +1457,12 @@ impl Printer {
|
|
|
1402
1457
|
}
|
|
1403
1458
|
}
|
|
1404
1459
|
JsxProp::Spread(e) => {
|
|
1460
|
+
// A leading space separates this prop from the tag/prior prop; the
|
|
1461
|
+
// closing brace must NOT carry a trailing space, or a following attr
|
|
1462
|
+
// (which prepends its own space) would render as a double space.
|
|
1405
1463
|
self.buf.push_str(" {...");
|
|
1406
1464
|
self.expr(e);
|
|
1407
|
-
self.buf.
|
|
1465
|
+
self.buf.push('}');
|
|
1408
1466
|
}
|
|
1409
1467
|
}
|
|
1410
1468
|
}
|
|
@@ -1885,6 +1943,41 @@ mod tests {
|
|
|
1885
1943
|
}
|
|
1886
1944
|
}
|
|
1887
1945
|
|
|
1946
|
+
#[test]
|
|
1947
|
+
fn multiline_jsx_children_are_bare_and_verbatim() {
|
|
1948
|
+
// #157: multi-line JSX nested inside an arrow body must print element children BARE (not
|
|
1949
|
+
// `{<child>}`-wrapped), must NOT emit stray blank ` ` lines, and must NOT re-indent to a
|
|
1950
|
+
// hardcoded 2-space depth — JSX whitespace is significant, so the source layout is kept
|
|
1951
|
+
// verbatim. This golden string locks the corrected output.
|
|
1952
|
+
let src = "const x = () => {\n return <div>\n <span>hello</span>\n <span>world</span>\n </div>\n}\n";
|
|
1953
|
+
let out = format_source(src).unwrap();
|
|
1954
|
+
assert_eq!(
|
|
1955
|
+
out,
|
|
1956
|
+
"const x = () => {\n return <div>\n <span>hello</span>\n <span>world</span>\n </div>\n}\n",
|
|
1957
|
+
"multi-line JSX corrupted: {out:?}"
|
|
1958
|
+
);
|
|
1959
|
+
// No curly-wrapped element children, and no blank two-space line.
|
|
1960
|
+
assert!(!out.contains("{<"), "element child was brace-wrapped: {out:?}");
|
|
1961
|
+
assert!(!out.contains("\n \n"), "stray blank two-space line: {out:?}");
|
|
1962
|
+
// Idempotent.
|
|
1963
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent: {out:?}");
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
#[test]
|
|
1967
|
+
fn multiline_jsx_fragment_children_are_bare_and_verbatim() {
|
|
1968
|
+
// #157: the JsxFragment path shares the same layout logic as JsxElement; verify a
|
|
1969
|
+
// multi-line fragment keeps its children bare and its source layout verbatim.
|
|
1970
|
+
let src = "const y = () => {\n return <>\n <span>a</span>\n <span>b</span>\n </>\n}\n";
|
|
1971
|
+
let out = format_source(src).unwrap();
|
|
1972
|
+
assert_eq!(
|
|
1973
|
+
out,
|
|
1974
|
+
"const y = () => {\n return <>\n <span>a</span>\n <span>b</span>\n </>\n}\n",
|
|
1975
|
+
"multi-line JSX fragment corrupted: {out:?}"
|
|
1976
|
+
);
|
|
1977
|
+
assert!(!out.contains("{<"), "fragment element child was brace-wrapped: {out:?}");
|
|
1978
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent: {out:?}");
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1888
1981
|
#[test]
|
|
1889
1982
|
fn non_finite_number_literal_emits_valid_token() {
|
|
1890
1983
|
// 1e400 overflows f64 to infinity; it must format to the `Infinity` global, not Rust's bare
|
|
@@ -125,6 +125,65 @@ impl<'a> Lexer<'a> {
|
|
|
125
125
|
)
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/// #299 — true when the previous significant token ends a value, so a following `/` is DIVISION,
|
|
129
|
+
/// not the start of a regex literal. Superset of `last_is_value` (adds postfix `++`/`--` and
|
|
130
|
+
/// template-end tokens, which also end a value). When false, `/` begins a regex.
|
|
131
|
+
fn prev_ends_value(&self) -> bool {
|
|
132
|
+
self.last_is_value()
|
|
133
|
+
|| matches!(
|
|
134
|
+
self.last_significant_kind,
|
|
135
|
+
Some(
|
|
136
|
+
TokenKind::PlusPlus
|
|
137
|
+
| TokenKind::MinusMinus
|
|
138
|
+
| TokenKind::TemplateNoSub
|
|
139
|
+
| TokenKind::TemplateTail
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// #299 — scan a regex literal body. The opening `/` is already consumed. Reads the pattern
|
|
145
|
+
/// VERBATIM (no escape processing — `\d` must stay `\d`), honoring `\`-escapes (`\/` is literal)
|
|
146
|
+
/// and `[...]` character classes (a `/` inside a class is literal), until the closing `/`, then
|
|
147
|
+
/// the trailing ascii-alpha flags. A newline inside the body or EOF is an unterminated-regex error.
|
|
148
|
+
fn read_regex(&mut self) -> Result<(String, String), String> {
|
|
149
|
+
let mut pat = String::with_capacity(16);
|
|
150
|
+
let mut in_class = false;
|
|
151
|
+
loop {
|
|
152
|
+
match self.advance() {
|
|
153
|
+
None | Some('\n') => return Err("Unterminated regex literal".to_string()),
|
|
154
|
+
Some('\\') => {
|
|
155
|
+
pat.push('\\');
|
|
156
|
+
match self.advance() {
|
|
157
|
+
None | Some('\n') => {
|
|
158
|
+
return Err("Unterminated regex literal".to_string())
|
|
159
|
+
}
|
|
160
|
+
Some(c) => pat.push(c),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
Some('[') => {
|
|
164
|
+
in_class = true;
|
|
165
|
+
pat.push('[');
|
|
166
|
+
}
|
|
167
|
+
Some(']') => {
|
|
168
|
+
in_class = false;
|
|
169
|
+
pat.push(']');
|
|
170
|
+
}
|
|
171
|
+
Some('/') if !in_class => break,
|
|
172
|
+
Some(c) => pat.push(c),
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let mut flags = String::new();
|
|
176
|
+
while let Some(c) = self.peek() {
|
|
177
|
+
if c.is_ascii_alphabetic() {
|
|
178
|
+
flags.push(c);
|
|
179
|
+
self.advance();
|
|
180
|
+
} else {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
Ok((pat, flags))
|
|
185
|
+
}
|
|
186
|
+
|
|
128
187
|
#[inline]
|
|
129
188
|
fn jsx_sync_in_opening_tag(&mut self) {
|
|
130
189
|
self.jsx_in_opening_tag = self.jsx_stack.last().map(|e| e.in_opener).unwrap_or(false);
|
|
@@ -819,6 +878,23 @@ impl<'a> Lexer<'a> {
|
|
|
819
878
|
self.advance();
|
|
820
879
|
self.skip_block_comment()?;
|
|
821
880
|
return self.next_token();
|
|
881
|
+
} else if !self.prev_ends_value()
|
|
882
|
+
&& !self.jsx_in_opening_tag
|
|
883
|
+
&& !self.jsx_in_closing_tag
|
|
884
|
+
{
|
|
885
|
+
// #299: regex literal — the previous token does not end a value, so `/` starts a
|
|
886
|
+
// regex (`let re = /\d+/g`, `f(/x/)`, `return /x/`), not division. `//` and `/*`
|
|
887
|
+
// (handled above) stay comments even here. JSX tag contexts keep `/` as a tag
|
|
888
|
+
// slash, not a regex: `<div />` (jsx_in_opening_tag) and `</div>` (the `<` sets
|
|
889
|
+
// jsx_in_closing_tag). Desugared to `new RegExp(...)` by the parser.
|
|
890
|
+
let (pat, flags) = self.read_regex()?;
|
|
891
|
+
let end = self.span_start();
|
|
892
|
+
let lit = format!("{}\u{0}{}", pat, flags);
|
|
893
|
+
return Ok(Some(Token {
|
|
894
|
+
kind: TokenKind::Regex,
|
|
895
|
+
span: Span { start, end },
|
|
896
|
+
literal: Some(lit.into()),
|
|
897
|
+
}));
|
|
822
898
|
} else if self.peek() == Some('=') {
|
|
823
899
|
self.advance();
|
|
824
900
|
TokenKind::SlashAssign
|
|
@@ -123,6 +123,10 @@ pub enum TokenKind {
|
|
|
123
123
|
TemplateMiddle, // }text${ (middle part)
|
|
124
124
|
TemplateTail, // }text` (end part)
|
|
125
125
|
|
|
126
|
+
/// Regex literal `/pattern/flags` (#299). The token's `literal` carries `pattern\0flags`
|
|
127
|
+
/// (NUL-separated); the parser desugars it to `new RegExp(pattern, flags)`.
|
|
128
|
+
Regex,
|
|
129
|
+
|
|
126
130
|
JsxText, // Raw text in JSX children (emojis, etc.); only {}<> are special
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -26,6 +26,8 @@ pub fn lint_program(program: &Program) -> Vec<LintDiagnostic> {
|
|
|
26
26
|
let mut out = Vec::new();
|
|
27
27
|
check_unreachable(&program.statements, &mut out);
|
|
28
28
|
for s in &program.statements {
|
|
29
|
+
// Top-level bare `{}` blocks are standalone statements: flag them like ESLint no-empty.
|
|
30
|
+
check_empty_block(s, &mut out);
|
|
29
31
|
lint_stmt(s, &mut out);
|
|
30
32
|
}
|
|
31
33
|
out
|
|
@@ -94,7 +96,10 @@ fn lint_stmt(s: &Statement, out: &mut Vec<LintDiagnostic>) {
|
|
|
94
96
|
}
|
|
95
97
|
lint_stmt(cb, out);
|
|
96
98
|
}
|
|
99
|
+
// #153: ESLint no-empty flags an empty `finally` (but not the try body). Report before
|
|
100
|
+
// recursing so the finally's own bare-block children aren't double-counted below.
|
|
97
101
|
if let Some(fb) = finally_body {
|
|
102
|
+
check_empty_block(fb, out);
|
|
98
103
|
lint_stmt(fb, out);
|
|
99
104
|
}
|
|
100
105
|
}
|
|
@@ -102,6 +107,8 @@ fn lint_stmt(s: &Statement, out: &mut Vec<LintDiagnostic>) {
|
|
|
102
107
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
103
108
|
check_unreachable(statements, out);
|
|
104
109
|
for st in statements {
|
|
110
|
+
// #153: a bare `{}` nested inside another block is a standalone empty block.
|
|
111
|
+
check_empty_block(st, out);
|
|
105
112
|
lint_stmt(st, out);
|
|
106
113
|
}
|
|
107
114
|
}
|
|
@@ -113,17 +120,21 @@ fn lint_stmt(s: &Statement, out: &mut Vec<LintDiagnostic>) {
|
|
|
113
120
|
..
|
|
114
121
|
} => {
|
|
115
122
|
lint_expr(cond, out);
|
|
123
|
+
check_empty_block(then_branch, out); // #153: empty `if` branch
|
|
116
124
|
lint_stmt(then_branch, out);
|
|
117
125
|
if let Some(e) = else_branch {
|
|
126
|
+
check_empty_block(e, out); // #153: empty `else` branch
|
|
118
127
|
lint_stmt(e, out);
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
130
|
Statement::While { cond, body, .. } => {
|
|
122
131
|
lint_expr(cond, out);
|
|
132
|
+
check_empty_block(body, out); // #153: empty `while` body
|
|
123
133
|
lint_stmt(body, out);
|
|
124
134
|
}
|
|
125
135
|
Statement::ForOf { iterable, body, .. } => {
|
|
126
136
|
lint_expr(iterable, out);
|
|
137
|
+
check_empty_block(body, out); // #153: empty `for-of` body
|
|
127
138
|
lint_stmt(body, out);
|
|
128
139
|
}
|
|
129
140
|
Statement::For {
|
|
@@ -142,6 +153,7 @@ fn lint_stmt(s: &Statement, out: &mut Vec<LintDiagnostic>) {
|
|
|
142
153
|
if let Some(u) = update {
|
|
143
154
|
lint_expr(u, out);
|
|
144
155
|
}
|
|
156
|
+
check_empty_block(body, out); // #153: empty `for` body
|
|
145
157
|
lint_stmt(body, out);
|
|
146
158
|
}
|
|
147
159
|
Statement::FunDecl { body, .. } => lint_stmt(body, out),
|
|
@@ -169,6 +181,7 @@ fn lint_stmt(s: &Statement, out: &mut Vec<LintDiagnostic>) {
|
|
|
169
181
|
}
|
|
170
182
|
}
|
|
171
183
|
Statement::DoWhile { body, cond, .. } => {
|
|
184
|
+
check_empty_block(body, out); // #153: empty `do-while` body
|
|
172
185
|
lint_stmt(body, out);
|
|
173
186
|
lint_expr(cond, out);
|
|
174
187
|
}
|
|
@@ -177,6 +190,7 @@ fn lint_stmt(s: &Statement, out: &mut Vec<LintDiagnostic>) {
|
|
|
177
190
|
// Walk `export default <expr>` too — its subtree was previously never linted, so e.g.
|
|
178
191
|
// `export default { a: 1, a: 2 }` produced no tish-duplicate-key warning (#151).
|
|
179
192
|
tishlang_ast::ExportDeclaration::Default(expr) => lint_expr(expr, out),
|
|
193
|
+
tishlang_ast::ExportDeclaration::ReExport { .. } => {}
|
|
180
194
|
},
|
|
181
195
|
Statement::ExprStmt { expr, .. } => lint_expr(expr, out),
|
|
182
196
|
Statement::VarDecl { init: Some(e), .. } => lint_expr(e, out),
|
|
@@ -195,6 +209,25 @@ fn is_empty_block_or_stmt(s: &Statement) -> bool {
|
|
|
195
209
|
}
|
|
196
210
|
}
|
|
197
211
|
|
|
212
|
+
/// `tish-empty-block` (ESLint no-empty parity): flag `s` if it is an empty `{ }` block. Callers pass
|
|
213
|
+
/// only positions ESLint no-empty covers — `if`/`else`, loop bodies, `finally`, and standalone bare
|
|
214
|
+
/// blocks. Function bodies, the `try` body, and `catch` bodies are intentionally NOT routed here:
|
|
215
|
+
/// ESLint allows empty functions and empty try bodies, and an empty catch is its own
|
|
216
|
+
/// `tish-empty-catch` diagnostic (so this never double-reports a catch). The message/span/severity
|
|
217
|
+
/// mirror the empty-catch diagnostic exactly.
|
|
218
|
+
fn check_empty_block(s: &Statement, out: &mut Vec<LintDiagnostic>) {
|
|
219
|
+
if is_empty_block_or_stmt(s) {
|
|
220
|
+
let span = stmt_span(s);
|
|
221
|
+
out.push(LintDiagnostic {
|
|
222
|
+
code: "tish-empty-block",
|
|
223
|
+
message: "Empty block; add statements or remove it.".into(),
|
|
224
|
+
line: span.0,
|
|
225
|
+
col: span.1,
|
|
226
|
+
severity: Severity::Warning,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
198
231
|
fn stmt_span(s: &Statement) -> (u32, u32) {
|
|
199
232
|
// Use the AST's uniform accessor so every statement kind reports its real position (the old
|
|
200
233
|
// local match only handled Block/Try and defaulted everything else to (1,1)).
|
|
@@ -343,6 +376,10 @@ pub const RULES: &[(&str, &str)] = &[
|
|
|
343
376
|
"tish-empty-catch",
|
|
344
377
|
"Warns on catch blocks with no statements (likely mistake).",
|
|
345
378
|
),
|
|
379
|
+
(
|
|
380
|
+
"tish-empty-block",
|
|
381
|
+
"Warns on empty blocks in if/else, loops, finally, or as bare `{}` (ESLint no-empty; function bodies and try bodies are allowed).",
|
|
382
|
+
),
|
|
346
383
|
(
|
|
347
384
|
"tish-duplicate-key",
|
|
348
385
|
"Warns when an object literal repeats the same key.",
|
|
@@ -440,6 +477,95 @@ mod tests {
|
|
|
440
477
|
}
|
|
441
478
|
}
|
|
442
479
|
|
|
480
|
+
// #153: `tish-empty-block` (ESLint no-empty parity) — empty blocks outside catch are flagged, but
|
|
481
|
+
// function bodies and the try body are not, and an empty catch stays `tish-empty-catch`.
|
|
482
|
+
#[cfg(test)]
|
|
483
|
+
mod empty_block_tests {
|
|
484
|
+
use super::*;
|
|
485
|
+
|
|
486
|
+
fn count(src: &str, code: &str) -> usize {
|
|
487
|
+
lint_source(src)
|
|
488
|
+
.expect("parse")
|
|
489
|
+
.iter()
|
|
490
|
+
.filter(|d| d.code == code)
|
|
491
|
+
.count()
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
fn empty_blocks(src: &str) -> usize {
|
|
495
|
+
count(src, "tish-empty-block")
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#[test]
|
|
499
|
+
fn flags_empty_if_branch() {
|
|
500
|
+
assert_eq!(empty_blocks("if (c) {}\n"), 1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#[test]
|
|
504
|
+
fn flags_empty_if_and_else_branches() {
|
|
505
|
+
// Both the empty then-branch and the empty else-branch are flagged.
|
|
506
|
+
assert_eq!(empty_blocks("if (c) {} else {}\n"), 2);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#[test]
|
|
510
|
+
fn flags_empty_while_body() {
|
|
511
|
+
assert_eq!(empty_blocks("while (c) {}\n"), 1);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#[test]
|
|
515
|
+
fn flags_empty_for_body() {
|
|
516
|
+
assert_eq!(empty_blocks("for (;;) {}\n"), 1);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#[test]
|
|
520
|
+
fn flags_empty_for_of_body() {
|
|
521
|
+
assert_eq!(empty_blocks("for (const x of xs) {}\n"), 1);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[test]
|
|
525
|
+
fn flags_empty_do_while_body() {
|
|
526
|
+
assert_eq!(empty_blocks("do {} while (c)\n"), 1);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
#[test]
|
|
530
|
+
fn flags_bare_empty_block() {
|
|
531
|
+
assert_eq!(empty_blocks("{}\n"), 1);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#[test]
|
|
535
|
+
fn flags_empty_finally() {
|
|
536
|
+
// Try body is unflagged; only the empty `finally` is reported.
|
|
537
|
+
assert_eq!(empty_blocks("try { f() } catch (e) { g() } finally {}\n"), 1);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
#[test]
|
|
541
|
+
fn does_not_flag_empty_function_body() {
|
|
542
|
+
// ESLint no-empty allows empty functions.
|
|
543
|
+
assert_eq!(empty_blocks("function f() {}\n"), 0, "fn decl");
|
|
544
|
+
assert_eq!(empty_blocks("fn f() {}\n"), 0, "fn keyword");
|
|
545
|
+
assert_eq!(empty_blocks("const f = () => {}\n"), 0, "arrow");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#[test]
|
|
549
|
+
fn does_not_flag_empty_try_body() {
|
|
550
|
+
// ESLint no-empty does NOT flag the try body itself.
|
|
551
|
+
assert_eq!(empty_blocks("try {} catch (e) { g() }\n"), 0);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#[test]
|
|
555
|
+
fn does_not_flag_non_empty_blocks() {
|
|
556
|
+
assert_eq!(empty_blocks("if (c) { f() } else { g() }\n"), 0, "if/else");
|
|
557
|
+
assert_eq!(empty_blocks("while (c) { f() }\n"), 0, "while");
|
|
558
|
+
assert_eq!(empty_blocks("{ f() }\n"), 0, "bare block");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#[test]
|
|
562
|
+
fn empty_catch_stays_empty_catch_not_empty_block() {
|
|
563
|
+
// The catch keeps its dedicated code and is NOT also reported as an empty block.
|
|
564
|
+
assert_eq!(count("try { f() } catch (e) {}\n", "tish-empty-catch"), 1, "empty-catch");
|
|
565
|
+
assert_eq!(count("try { f() } catch (e) {}\n", "tish-empty-block"), 0, "not empty-block");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
443
569
|
#[cfg(test)]
|
|
444
570
|
mod dupkey_position_tests {
|
|
445
571
|
use super::*;
|
|
@@ -15,7 +15,8 @@ Binary: `target/release/tish-lsp` (stdio LSP).
|
|
|
15
15
|
- Parse diagnostics + lint warnings (via `tish_lint` **library** — use **`tish-lint`** CLI separately in CI)
|
|
16
16
|
- Document symbols, completion, formatting (via `tish_fmt` **library** — use **`tish-fmt`** CLI separately in CI)
|
|
17
17
|
- Go to definition (same file, relative `./` / `../`, bare `node_modules` packages like Node, and native `tish:` / `@scope/pkg` / `cargo:` → Rust `pub fn` via `syn` + `cargo metadata` where configured)
|
|
18
|
-
- Workspace symbol search (`**/*.tish`)
|
|
18
|
+
- Workspace symbol search (`**/*.{tish,d.tish}`), with `$/cancelRequest` support and work-done progress when the client advertises `window.workDoneProgress`
|
|
19
|
+
- Watches `.tish` / `.d.tish` on disk (`workspace/didChangeWatchedFiles`, registered dynamically) so ambient declaration files refresh without a restart
|
|
19
20
|
|
|
20
21
|
## Client configuration
|
|
21
22
|
|