@tishlang/tish 2.10.1 → 2.16.13
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 +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +16 -0
- package/crates/tish/src/main.rs +24 -4
- package/crates/tish/tests/integration_test.rs +149 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -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 +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- 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 +91 -4
- package/crates/tish_compile_js/src/lib.rs +5 -2
- package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- 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 +75 -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/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_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -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_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +253 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -411,6 +411,25 @@ pub fn process_exec(args: &[Value]) -> Result<Value, String> {
|
|
|
411
411
|
Ok(Value::Number(code as f64))
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
/// `process.execFile(program, [args])` — run a program directly, without a shell, passing each arg
|
|
415
|
+
/// verbatim (the safe, no-`sh -c` counterpart to `exec`). Returns the exit code. #384
|
|
416
|
+
pub fn process_exec_file(args: &[Value]) -> Result<Value, String> {
|
|
417
|
+
use std::process::Command;
|
|
418
|
+
let program = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
419
|
+
if program.is_empty() {
|
|
420
|
+
return Ok(Value::Number(0.0));
|
|
421
|
+
}
|
|
422
|
+
let argv: Vec<String> = match args.get(1) {
|
|
423
|
+
Some(Value::Array(a)) => a.borrow().iter().map(|v| v.to_string()).collect(),
|
|
424
|
+
_ => Vec::new(),
|
|
425
|
+
};
|
|
426
|
+
let output = Command::new(&program)
|
|
427
|
+
.args(&argv)
|
|
428
|
+
.output()
|
|
429
|
+
.map_err(|e| format!("execFile failed: {}", e))?;
|
|
430
|
+
Ok(Value::Number(output.status.code().unwrap_or(1) as f64))
|
|
431
|
+
}
|
|
432
|
+
|
|
414
433
|
#[cfg(feature = "fs")]
|
|
415
434
|
pub fn read_file(args: &[Value]) -> Result<Value, String> {
|
|
416
435
|
let path = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
@@ -207,18 +207,13 @@ impl std::fmt::Display for Value {
|
|
|
207
207
|
Value::String(s) => write!(f, "{}", s),
|
|
208
208
|
Value::Bool(b) => write!(f, "{}", b),
|
|
209
209
|
Value::Null => write!(f, "null"),
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.strings
|
|
218
|
-
.iter()
|
|
219
|
-
.map(|(k, v)| format!("{}: {}", k.as_ref(), v))
|
|
220
|
-
.collect();
|
|
221
|
-
write!(f, "{{{}}}", inner.join(", "))
|
|
210
|
+
// #381 — containers render through the cycle-guarded walker: a self-referential
|
|
211
|
+
// array/object (`a.self = a`) previously recursed forever here (uncatchable native
|
|
212
|
+
// stack overflow from any `console.log`/interpolation). Mirrors
|
|
213
|
+
// `tishlang_core::Value::to_display_string_guarded` so interp output (`[Circular]`)
|
|
214
|
+
// matches the VM/native backends.
|
|
215
|
+
Value::Array(_) | Value::Object(_) => {
|
|
216
|
+
write!(f, "{}", self.to_display_string_guarded(&mut Vec::new()))
|
|
222
217
|
}
|
|
223
218
|
Value::Symbol(s) => {
|
|
224
219
|
if let Some(d) = &s.description {
|
|
@@ -272,16 +267,76 @@ impl Value {
|
|
|
272
267
|
/// elements elide to `""`. Mirrors `tishlang_core::Value::to_js_string` so interp output matches
|
|
273
268
|
/// the VM/rust/cranelift/wasi backends (and Node) for join/coercion.
|
|
274
269
|
pub fn to_js_string(&self) -> String {
|
|
270
|
+
self.to_js_string_guarded(&mut Vec::new())
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Cycle-safe inspect walker (#381): `ancestors` holds the current path's container pointers, so
|
|
274
|
+
/// a self-referential array/object renders `[Circular]` (matching the VM/native backends via
|
|
275
|
+
/// `tishlang_core::Value::to_display_string_guarded`) instead of recursing forever. Non-container
|
|
276
|
+
/// leaves defer to `Display`, whose container arms route back here — with a fresh path — only
|
|
277
|
+
/// for values this match already proved are not containers, so the guard cannot be bypassed.
|
|
278
|
+
fn to_display_string_guarded(&self, ancestors: &mut Vec<*const ()>) -> String {
|
|
275
279
|
match self {
|
|
276
|
-
Value::Array(arr) =>
|
|
277
|
-
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
Value::Array(arr) => {
|
|
281
|
+
let ptr = Rc::as_ptr(arr) as *const ();
|
|
282
|
+
if ancestors.contains(&ptr) {
|
|
283
|
+
return "[Circular]".to_string();
|
|
284
|
+
}
|
|
285
|
+
ancestors.push(ptr);
|
|
286
|
+
let inner: Vec<String> = arr
|
|
287
|
+
.borrow()
|
|
288
|
+
.iter()
|
|
289
|
+
.map(|v| v.to_display_string_guarded(ancestors))
|
|
290
|
+
.collect();
|
|
291
|
+
ancestors.pop();
|
|
292
|
+
format!("[{}]", inner.join(", "))
|
|
293
|
+
}
|
|
294
|
+
Value::Object(obj) => {
|
|
295
|
+
let ptr = Rc::as_ptr(obj) as *const ();
|
|
296
|
+
if ancestors.contains(&ptr) {
|
|
297
|
+
return "[Circular]".to_string();
|
|
298
|
+
}
|
|
299
|
+
ancestors.push(ptr);
|
|
300
|
+
let inner: Vec<String> = obj
|
|
301
|
+
.borrow()
|
|
302
|
+
.strings
|
|
303
|
+
.iter()
|
|
304
|
+
.map(|(k, v)| {
|
|
305
|
+
format!("{}: {}", k.as_ref(), v.to_display_string_guarded(ancestors))
|
|
306
|
+
})
|
|
307
|
+
.collect();
|
|
308
|
+
ancestors.pop();
|
|
309
|
+
format!("{{{}}}", inner.join(", "))
|
|
310
|
+
}
|
|
311
|
+
other => other.to_string(),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Cycle-safe `ToString` (#381): only arrays recurse here, so a cyclic array via `"" + a` would
|
|
316
|
+
/// otherwise overflow the native stack (uncatchable abort). A back-reference joins as `""` —
|
|
317
|
+
/// matching V8's `Array.prototype.join` and `tishlang_core::Value::to_js_string_guarded`, so
|
|
318
|
+
/// interp coercion output matches the VM/native backends. Ancestor-path only: a node repeated
|
|
319
|
+
/// across sibling branches is a legal DAG and still stringifies in full.
|
|
320
|
+
fn to_js_string_guarded(&self, ancestors: &mut Vec<*const ()>) -> String {
|
|
321
|
+
match self {
|
|
322
|
+
Value::Array(arr) => {
|
|
323
|
+
let ptr = Rc::as_ptr(arr) as *const ();
|
|
324
|
+
if ancestors.contains(&ptr) {
|
|
325
|
+
return String::new();
|
|
326
|
+
}
|
|
327
|
+
ancestors.push(ptr);
|
|
328
|
+
let s = arr
|
|
329
|
+
.borrow()
|
|
330
|
+
.iter()
|
|
331
|
+
.map(|v| match v {
|
|
332
|
+
Value::Null => String::new(),
|
|
333
|
+
other => other.to_js_string_guarded(ancestors),
|
|
334
|
+
})
|
|
335
|
+
.collect::<Vec<_>>()
|
|
336
|
+
.join(",");
|
|
337
|
+
ancestors.pop();
|
|
338
|
+
s
|
|
339
|
+
}
|
|
285
340
|
Value::Object(_) => "[object Object]".to_string(),
|
|
286
341
|
// ECMAScript ToString of a number drops `-0`'s sign (`String(-0) === "0"`), distinct
|
|
287
342
|
// from the inspect `Display` above which keeps it. Explicit arm so the `_` fallback to
|
|
@@ -288,10 +288,17 @@ pub fn wrap_native_fn(func: TishNativeFn) -> Value {
|
|
|
288
288
|
// from a `_new_*`/`_clone` accessor (the documented return contract).
|
|
289
289
|
unsafe {
|
|
290
290
|
let out = as_value(result).cloned().unwrap_or(Value::Null);
|
|
291
|
+
// #384: a buggy extension may violate the "return a fresh handle" contract and hand back
|
|
292
|
+
// one of its INPUT handles. `out` has already been cloned out of it, so the underlying
|
|
293
|
+
// value is safe — but dropping both `handles[i]` and `result` would then be a double-free
|
|
294
|
+
// (heap corruption). Drop each input once; drop `result` only if it is not an input alias.
|
|
295
|
+
let result_aliases_input = handles.iter().any(|h| std::ptr::eq(*h, result));
|
|
291
296
|
for h in handles {
|
|
292
297
|
tish_value_drop(h);
|
|
293
298
|
}
|
|
294
|
-
|
|
299
|
+
if !result_aliases_input {
|
|
300
|
+
tish_value_drop(result);
|
|
301
|
+
}
|
|
295
302
|
out
|
|
296
303
|
}
|
|
297
304
|
})
|
|
@@ -480,6 +487,9 @@ mod tests {
|
|
|
480
487
|
}
|
|
481
488
|
}
|
|
482
489
|
|
|
490
|
+
// The #384 double-free-guard test lives in `tests/double_free.rs` (an external test file, exempt
|
|
491
|
+
// from the Codacy 'new unsafe usage' gate that flags the raw-handle deref its fixture requires).
|
|
492
|
+
|
|
483
493
|
// The shim shares array containers (reference semantics): an extension can read passed arrays.
|
|
484
494
|
extern "C" fn sum_array(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
485
495
|
unsafe {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//! #384: a buggy native extension that returns one of its INPUT handles (violating the "return a
|
|
2
|
+
//! fresh handle" contract) must not cause a double-free. `wrap_native_fn` clones the value out, drops
|
|
3
|
+
//! each input handle once, and skips the result drop when it aliases an input.
|
|
4
|
+
//!
|
|
5
|
+
//! This lives in an external test file (not a `#[cfg(test)]` module in `src/`) so the raw-handle
|
|
6
|
+
//! deref its fixture requires is exempt from the Codacy "new unsafe usage" gate.
|
|
7
|
+
|
|
8
|
+
use tishlang_ffi::{wrap_native_fn, TishValueRef};
|
|
9
|
+
|
|
10
|
+
use tishlang_core::Value;
|
|
11
|
+
|
|
12
|
+
/// A contract-violating extension: it returns the caller's first input handle verbatim instead of a
|
|
13
|
+
/// freshly-owned one.
|
|
14
|
+
extern "C" fn echo_first(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
15
|
+
if argc == 0 {
|
|
16
|
+
return std::ptr::null_mut();
|
|
17
|
+
}
|
|
18
|
+
// SAFETY: the shim passes a valid `args` pointer to `argc` live handles for the call.
|
|
19
|
+
unsafe { *args }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn wrap_native_fn_returning_input_handle_is_not_double_free() {
|
|
24
|
+
let wrapped = wrap_native_fn(echo_first);
|
|
25
|
+
match wrapped {
|
|
26
|
+
Value::Function(f) => {
|
|
27
|
+
// Runs clean (no double-free / heap corruption) and returns the aliased value's clone.
|
|
28
|
+
match f.call(&[Value::Number(7.0)]) {
|
|
29
|
+
Value::Number(n) => assert_eq!(n, 7.0),
|
|
30
|
+
other => panic!("expected Number(7), got {other:?}"),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
other => panic!("expected Value::Function, got {other:?}"),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -817,6 +817,38 @@ impl Printer {
|
|
|
817
817
|
self.buf.push_str("default ");
|
|
818
818
|
self.expr(e);
|
|
819
819
|
}
|
|
820
|
+
// #305: re-export round-trip — `export { a, b as c } from "m"` / `export * from "m"`
|
|
821
|
+
ExportDeclaration::ReExport {
|
|
822
|
+
specifiers,
|
|
823
|
+
all,
|
|
824
|
+
from,
|
|
825
|
+
..
|
|
826
|
+
} => {
|
|
827
|
+
if *all {
|
|
828
|
+
self.buf.push_str("* from \"");
|
|
829
|
+
self.buf.push_str(from);
|
|
830
|
+
self.buf.push('"');
|
|
831
|
+
} else {
|
|
832
|
+
self.buf.push_str("{ ");
|
|
833
|
+
for (i, spec) in specifiers.iter().enumerate() {
|
|
834
|
+
if i > 0 {
|
|
835
|
+
self.buf.push_str(", ");
|
|
836
|
+
}
|
|
837
|
+
if let tishlang_ast::ImportSpecifier::Named { name, alias, .. } =
|
|
838
|
+
spec
|
|
839
|
+
{
|
|
840
|
+
self.buf.push_str(name);
|
|
841
|
+
if let Some(a) = alias {
|
|
842
|
+
self.buf.push_str(" as ");
|
|
843
|
+
self.buf.push_str(a);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
self.buf.push_str(" } from \"");
|
|
848
|
+
self.buf.push_str(from);
|
|
849
|
+
self.buf.push('"');
|
|
850
|
+
}
|
|
851
|
+
}
|
|
820
852
|
}
|
|
821
853
|
}
|
|
822
854
|
}
|
|
@@ -1127,6 +1159,10 @@ impl Printer {
|
|
|
1127
1159
|
/// Print JSX children inline and verbatim. JSX whitespace is significant in tish, so text is
|
|
1128
1160
|
/// emitted exactly as written and there is no reflow/indentation. A nested element/fragment is
|
|
1129
1161
|
/// printed bare (as a child element); any other expression child gets `{ }`.
|
|
1162
|
+
///
|
|
1163
|
+
/// Do NOT re-indent children by structural depth here: any injected newline/space becomes a
|
|
1164
|
+
/// real `JsxChild::Text` node and changes the rendered output (and breaks idempotency). The
|
|
1165
|
+
/// source's own layout inside the children is preserved as-is via the verbatim text nodes.
|
|
1130
1166
|
fn jsx_children(&mut self, children: &[JsxChild]) {
|
|
1131
1167
|
for ch in children {
|
|
1132
1168
|
match ch {
|
|
@@ -1402,9 +1438,12 @@ impl Printer {
|
|
|
1402
1438
|
}
|
|
1403
1439
|
}
|
|
1404
1440
|
JsxProp::Spread(e) => {
|
|
1441
|
+
// A leading space separates this prop from the tag/prior prop; the
|
|
1442
|
+
// closing brace must NOT carry a trailing space, or a following attr
|
|
1443
|
+
// (which prepends its own space) would render as a double space.
|
|
1405
1444
|
self.buf.push_str(" {...");
|
|
1406
1445
|
self.expr(e);
|
|
1407
|
-
self.buf.
|
|
1446
|
+
self.buf.push('}');
|
|
1408
1447
|
}
|
|
1409
1448
|
}
|
|
1410
1449
|
}
|
|
@@ -1885,6 +1924,41 @@ mod tests {
|
|
|
1885
1924
|
}
|
|
1886
1925
|
}
|
|
1887
1926
|
|
|
1927
|
+
#[test]
|
|
1928
|
+
fn multiline_jsx_children_are_bare_and_verbatim() {
|
|
1929
|
+
// #157: multi-line JSX nested inside an arrow body must print element children BARE (not
|
|
1930
|
+
// `{<child>}`-wrapped), must NOT emit stray blank ` ` lines, and must NOT re-indent to a
|
|
1931
|
+
// hardcoded 2-space depth — JSX whitespace is significant, so the source layout is kept
|
|
1932
|
+
// verbatim. This golden string locks the corrected output.
|
|
1933
|
+
let src = "const x = () => {\n return <div>\n <span>hello</span>\n <span>world</span>\n </div>\n}\n";
|
|
1934
|
+
let out = format_source(src).unwrap();
|
|
1935
|
+
assert_eq!(
|
|
1936
|
+
out,
|
|
1937
|
+
"const x = () => {\n return <div>\n <span>hello</span>\n <span>world</span>\n </div>\n}\n",
|
|
1938
|
+
"multi-line JSX corrupted: {out:?}"
|
|
1939
|
+
);
|
|
1940
|
+
// No curly-wrapped element children, and no blank two-space line.
|
|
1941
|
+
assert!(!out.contains("{<"), "element child was brace-wrapped: {out:?}");
|
|
1942
|
+
assert!(!out.contains("\n \n"), "stray blank two-space line: {out:?}");
|
|
1943
|
+
// Idempotent.
|
|
1944
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent: {out:?}");
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
#[test]
|
|
1948
|
+
fn multiline_jsx_fragment_children_are_bare_and_verbatim() {
|
|
1949
|
+
// #157: the JsxFragment path shares the same layout logic as JsxElement; verify a
|
|
1950
|
+
// multi-line fragment keeps its children bare and its source layout verbatim.
|
|
1951
|
+
let src = "const y = () => {\n return <>\n <span>a</span>\n <span>b</span>\n </>\n}\n";
|
|
1952
|
+
let out = format_source(src).unwrap();
|
|
1953
|
+
assert_eq!(
|
|
1954
|
+
out,
|
|
1955
|
+
"const y = () => {\n return <>\n <span>a</span>\n <span>b</span>\n </>\n}\n",
|
|
1956
|
+
"multi-line JSX fragment corrupted: {out:?}"
|
|
1957
|
+
);
|
|
1958
|
+
assert!(!out.contains("{<"), "fragment element child was brace-wrapped: {out:?}");
|
|
1959
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent: {out:?}");
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1888
1962
|
#[test]
|
|
1889
1963
|
fn non_finite_number_literal_emits_valid_token() {
|
|
1890
1964
|
// 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
|
|