@tishlang/tish-lsp 2.16.13 → 2.36.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.
Files changed (52) hide show
  1. package/bin/tish-lsp +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  3. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  4. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  5. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  6. package/crates/tish/tests/fixtures/process_capture.tish +16 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/process_capture.rs +40 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +77 -3
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +195 -16
  13. package/crates/tish_builtins/src/collections.rs +37 -2
  14. package/crates/tish_builtins/src/globals.rs +215 -25
  15. package/crates/tish_builtins/src/math.rs +65 -7
  16. package/crates/tish_builtins/src/number.rs +173 -0
  17. package/crates/tish_builtins/src/string.rs +168 -15
  18. package/crates/tish_bytecode/src/compiler.rs +56 -4
  19. package/crates/tish_bytecode/src/opcode.rs +4 -2
  20. package/crates/tish_compile/src/codegen.rs +519 -46
  21. package/crates/tish_compile/src/infer.rs +26 -11
  22. package/crates/tish_compile/src/resolve.rs +87 -10
  23. package/crates/tish_compile_js/src/codegen.rs +82 -24
  24. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  25. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
  26. package/crates/tish_core/src/json.rs +5 -1
  27. package/crates/tish_core/src/lib.rs +48 -2
  28. package/crates/tish_core/src/uri.rs +45 -0
  29. package/crates/tish_core/src/value.rs +6 -0
  30. package/crates/tish_eval/Cargo.toml +2 -1
  31. package/crates/tish_eval/src/eval.rs +661 -27
  32. package/crates/tish_eval/src/natives.rs +318 -18
  33. package/crates/tish_eval/src/regex.rs +47 -1
  34. package/crates/tish_fmt/src/lib.rs +23 -4
  35. package/crates/tish_lsp/Cargo.toml +1 -1
  36. package/crates/tish_opt/src/lib.rs +20 -1
  37. package/crates/tish_parser/src/lib.rs +41 -0
  38. package/crates/tish_parser/src/parser.rs +180 -11
  39. package/crates/tish_resolve/src/lib.rs +66 -5
  40. package/crates/tish_runtime/Cargo.toml +5 -1
  41. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  42. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  43. package/crates/tish_runtime/src/lib.rs +236 -10
  44. package/crates/tish_ui/src/jsx.rs +18 -3
  45. package/crates/tish_vm/src/jit.rs +1 -1
  46. package/crates/tish_vm/src/vm.rs +294 -60
  47. package/package.json +1 -1
  48. package/platform/darwin-arm64/tish-lsp +0 -0
  49. package/platform/darwin-x64/tish-lsp +0 -0
  50. package/platform/linux-arm64/tish-lsp +0 -0
  51. package/platform/linux-x64/tish-lsp +0 -0
  52. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -118,18 +118,23 @@ pub use tishlang_builtins::array::{
118
118
  find_last_index as array_find_last_index, flat as array_flat_impl, flat_map as array_flat_map,
119
119
  for_each as array_for_each, includes as array_includes_impl, index_of as array_index_of_impl,
120
120
  join as array_join_impl, map as array_map, pop as array_pop, push as array_push_impl,
121
- fill as array_fill, reduce as array_reduce, reverse as array_reverse, shift as array_shift,
121
+ fill as array_fill, last_index_of as array_last_index_of, copy_within as array_copy_within,
122
+ reduce as array_reduce, reduce_right as array_reduce_right, reverse as array_reverse,
123
+ keys as array_keys, values as array_values, entries as array_entries, shift as array_shift,
122
124
  shuffle as array_shuffle, slice as array_slice_impl, snapshot_values as array_snapshot_values,
123
125
  as_f64_snapshot as array_as_f64_snapshot, some as array_some,
124
126
  sort_by_keys as array_sort_by_keys, sort_default as array_sort_default,
125
127
  sort_numeric_asc as array_sort_numeric_asc, sort_numeric_desc as array_sort_numeric_desc,
126
128
  sort_with_comparator as array_sort_with_comparator, splice as array_splice_impl,
129
+ to_reversed as array_to_reversed, to_sorted as array_to_sorted,
130
+ to_spliced as array_to_spliced, with as array_with,
127
131
  unshift as array_unshift_impl,
128
132
  };
129
133
 
130
134
  // Re-export string methods from tishlang_builtins
131
135
  pub use tishlang_builtins::string::{
132
136
  at as string_at_impl, char_at as string_char_at_impl, char_code_at as string_char_code_at_impl,
137
+ code_point_at as string_code_point_at,
133
138
  ends_with as string_ends_with_impl, escape_html as string_escape_html_impl,
134
139
  includes as string_includes_impl, index_of as string_index_of_impl,
135
140
  last_index_of as string_last_index_of_impl, pad_end as string_pad_end_impl,
@@ -138,8 +143,9 @@ pub use tishlang_builtins::string::{
138
143
  slice as string_slice_impl, split as string_split_impl, split_limit as string_split_limit_impl,
139
144
  starts_with as string_starts_with_impl, substr as string_substr_impl,
140
145
  substring as string_substring_impl,
141
- to_lower_case as string_to_lower_case, to_upper_case as string_to_upper_case,
142
- trim as string_trim,
146
+ normalize as string_normalize, to_lower_case as string_to_lower_case,
147
+ to_upper_case as string_to_upper_case, trim as string_trim, trim_end as string_trim_end,
148
+ trim_start as string_trim_start,
143
149
  };
144
150
 
145
151
  // Wrapper functions to maintain API compatibility
@@ -149,8 +155,8 @@ pub fn array_push(arr: &Value, args: &[Value]) -> Value {
149
155
  pub fn array_unshift(arr: &Value, args: &[Value]) -> Value {
150
156
  array_unshift_impl(arr, args)
151
157
  }
152
- pub fn array_index_of(arr: &Value, search: &Value) -> Value {
153
- array_index_of_impl(arr, search)
158
+ pub fn array_index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
159
+ array_index_of_impl(arr, search, from)
154
160
  }
155
161
  pub fn array_includes(arr: &Value, search: &Value, from: &Value) -> Value {
156
162
  array_includes_impl(arr, search, Some(from))
@@ -217,11 +223,11 @@ pub fn string_split_limit(s: &Value, sep: &Value, limit: &Value) -> Value {
217
223
  }
218
224
  string_split_limit_impl(s, sep, max)
219
225
  }
220
- pub fn string_starts_with(s: &Value, search: &Value) -> Value {
221
- string_starts_with_impl(s, search)
226
+ pub fn string_starts_with(s: &Value, search: &Value, position: Option<&Value>) -> Value {
227
+ string_starts_with_impl(s, search, position)
222
228
  }
223
- pub fn string_ends_with(s: &Value, search: &Value) -> Value {
224
- string_ends_with_impl(s, search)
229
+ pub fn string_ends_with(s: &Value, search: &Value, end_position: Option<&Value>) -> Value {
230
+ string_ends_with_impl(s, search, end_position)
225
231
  }
226
232
  pub fn string_replace(s: &Value, search: &Value, replacement: &Value) -> Value {
227
233
  #[cfg(feature = "regex")]
@@ -391,6 +397,12 @@ pub fn string_last_index_of(s: &Value, search: &Value, position: &Value) -> Valu
391
397
  pub fn number_to_fixed(n: &Value, digits: &Value) -> Value {
392
398
  tishlang_builtins::number::to_fixed(n, digits)
393
399
  }
400
+ pub fn number_to_exponential(n: &Value, digits: &Value) -> Value {
401
+ tishlang_builtins::number::to_exponential(n, digits)
402
+ }
403
+ pub fn number_to_precision(n: &Value, precision: &Value) -> Value {
404
+ tishlang_builtins::number::to_precision(n, precision)
405
+ }
394
406
 
395
407
  /// `.toString([radix])` for the compiled backend (issue #59). A number receiver uses the
396
408
  /// shared radix formatter so it stays byte-identical with the VM / interpreter; any other
@@ -800,6 +812,15 @@ pub fn decode_uri(args: &[Value]) -> Value {
800
812
  pub fn encode_uri(args: &[Value]) -> Value {
801
813
  builtins_encode_uri(args)
802
814
  }
815
+ pub fn encode_uri_component(args: &[Value]) -> Value {
816
+ tishlang_builtins::globals::encode_uri_component(args)
817
+ }
818
+ pub fn decode_uri_component(args: &[Value]) -> Value {
819
+ tishlang_builtins::globals::decode_uri_component(args)
820
+ }
821
+ pub fn structured_clone(args: &[Value]) -> Value {
822
+ tishlang_builtins::globals::structured_clone(args)
823
+ }
803
824
 
804
825
  // Math functions - use tishlang_builtins::math
805
826
  pub use tishlang_builtins::math::{
@@ -903,6 +924,14 @@ runtime_math_unary!(math_atanh, atanh);
903
924
  runtime_math_unary!(math_cbrt, cbrt);
904
925
  runtime_math_unary!(math_log2, log2);
905
926
  runtime_math_unary!(math_log10, log10);
927
+ runtime_math_unary!(math_expm1, exp_m1);
928
+ runtime_math_unary!(math_log1p, ln_1p);
929
+ pub fn math_clz32(args: &[Value]) -> Value {
930
+ tishlang_builtins::math::clz32(args)
931
+ }
932
+ pub fn math_fround(args: &[Value]) -> Value {
933
+ tishlang_builtins::math::fround(args)
934
+ }
906
935
 
907
936
  pub fn json_stringify(args: &[Value]) -> Value {
908
937
  let v = args.first().cloned().unwrap_or(Value::Null);
@@ -921,6 +950,18 @@ pub fn json_parse(args: &[Value]) -> Value {
921
950
  pub fn array_is_array(args: &[Value]) -> Value {
922
951
  builtins_array_is_array(args)
923
952
  }
953
+ pub fn array_of(args: &[Value]) -> Value {
954
+ tishlang_builtins::globals::array_of(args)
955
+ }
956
+ pub fn array_from(args: &[Value]) -> Value {
957
+ tishlang_builtins::globals::array_from(args)
958
+ }
959
+ pub fn object_is(args: &[Value]) -> Value {
960
+ tishlang_builtins::globals::object_is(args)
961
+ }
962
+ pub fn object_has_own(args: &[Value]) -> Value {
963
+ tishlang_builtins::globals::object_has_own(args)
964
+ }
924
965
 
925
966
  pub fn string_from_char_code(args: &[Value]) -> Value {
926
967
  builtins_string_from_char_code(args)
@@ -935,6 +976,18 @@ pub fn string_convert(args: &[Value]) -> Value {
935
976
  pub fn number_convert(args: &[Value]) -> Value {
936
977
  builtins_number_convert(args)
937
978
  }
979
+ pub fn number_is_integer(args: &[Value]) -> Value {
980
+ tishlang_builtins::globals::number_is_integer(args)
981
+ }
982
+ pub fn number_is_safe_integer(args: &[Value]) -> Value {
983
+ tishlang_builtins::globals::number_is_safe_integer(args)
984
+ }
985
+ pub fn number_is_nan(args: &[Value]) -> Value {
986
+ tishlang_builtins::globals::number_is_nan(args)
987
+ }
988
+ pub fn number_is_finite(args: &[Value]) -> Value {
989
+ tishlang_builtins::globals::number_is_finite(args)
990
+ }
938
991
 
939
992
  #[cfg(feature = "process")]
940
993
  pub fn process_exit(args: &[Value]) -> Value {
@@ -996,9 +1049,75 @@ pub fn process_exec_file(args: &[Value]) -> Value {
996
1049
  }
997
1050
  }
998
1051
 
1052
+ /// Build the `{ code: number, stdout: string, stderr: string }` capture object (#475). stdout/stderr
1053
+ /// are lossy-UTF-8 decoded, matching the `fs` string convention (invalid bytes → replacement chars,
1054
+ /// never dropped output).
1055
+ #[cfg(feature = "process")]
1056
+ fn process_capture_obj(code: i32, stdout: String, stderr: String) -> Value {
1057
+ let mut m = tishlang_core::ObjectMap::default();
1058
+ m.insert("code".into(), Value::Number(code as f64));
1059
+ m.insert("stdout".into(), Value::String(stdout.into()));
1060
+ m.insert("stderr".into(), Value::String(stderr.into()));
1061
+ Value::object(m)
1062
+ }
1063
+
1064
+ /// Build the capture object from a `Command::output()` result. `code` is the exit status, or `-1`
1065
+ /// when the process was killed by a signal or failed to spawn (spawn failure puts the OS error in
1066
+ /// `stderr`).
1067
+ #[cfg(feature = "process")]
1068
+ fn process_capture_result(out: std::io::Result<std::process::Output>) -> Value {
1069
+ match out {
1070
+ Ok(o) => process_capture_obj(
1071
+ o.status.code().unwrap_or(-1),
1072
+ String::from_utf8_lossy(&o.stdout).into_owned(),
1073
+ String::from_utf8_lossy(&o.stderr).into_owned(),
1074
+ ),
1075
+ Err(e) => process_capture_obj(-1, String::new(), e.to_string()),
1076
+ }
1077
+ }
1078
+
1079
+ /// `process.execCapture(cmd)` — run `sh -c cmd` and CAPTURE its output as
1080
+ /// `{ code: number, stdout: string, stderr: string }` (Rust `Command::output`; Node
1081
+ /// `execSync`/`spawnSync({ encoding })`). Use `execFileCapture` for untrusted arguments — this shell
1082
+ /// form interprets metacharacters.
1083
+ #[cfg(feature = "process")]
1084
+ pub fn process_exec_capture(args: &[Value]) -> Value {
1085
+ use std::process::Command;
1086
+ let cmd = args
1087
+ .first()
1088
+ .map(|v| v.to_display_string())
1089
+ .unwrap_or_default();
1090
+ if cmd.is_empty() {
1091
+ return process_capture_obj(0, String::new(), String::new());
1092
+ }
1093
+ process_capture_result(Command::new("sh").arg("-c").arg(&cmd).output())
1094
+ }
1095
+
1096
+ /// `process.execFileCapture(program, [args])` — run a program directly, WITHOUT a shell, capturing
1097
+ /// output as `{ code, stdout, stderr }`. Arguments are passed verbatim, so shell metacharacters in
1098
+ /// untrusted data are never interpreted — the safe capturing counterpart for input-derived args
1099
+ /// (#384, #475). This is the primitive for shelling out to a CLI and reading its output (e.g.
1100
+ /// `git status --porcelain`).
1101
+ #[cfg(feature = "process")]
1102
+ pub fn process_exec_file_capture(args: &[Value]) -> Value {
1103
+ use std::process::Command;
1104
+ let program = args
1105
+ .first()
1106
+ .map(|v| v.to_display_string())
1107
+ .unwrap_or_default();
1108
+ if program.is_empty() {
1109
+ return process_capture_obj(0, String::new(), String::new());
1110
+ }
1111
+ let argv: Vec<String> = match args.get(1) {
1112
+ Some(Value::Array(a)) => a.borrow().iter().map(|v| v.to_display_string()).collect(),
1113
+ _ => Vec::new(),
1114
+ };
1115
+ process_capture_result(Command::new(&program).args(&argv).output())
1116
+ }
1117
+
999
1118
  #[cfg(all(test, feature = "process", unix))]
1000
1119
  mod execfile_tests_384 {
1001
- use super::process_exec_file;
1120
+ use super::{process_exec_file, process_exec_file_capture};
1002
1121
  use tishlang_core::{Value, VmRef};
1003
1122
 
1004
1123
  #[test]
@@ -1012,6 +1131,47 @@ mod execfile_tests_384 {
1012
1131
  // empty program is a no-op returning 0, matching `exec`.
1013
1132
  assert!(matches!(process_exec_file(&[]), Value::Number(n) if n == 0.0));
1014
1133
  }
1134
+
1135
+ // #475: `{ code, stdout, stderr }` capture.
1136
+ fn fields(v: &Value) -> (f64, String, String) {
1137
+ let Value::Object(o) = v else {
1138
+ panic!("execFileCapture must return an object, got {v:?}")
1139
+ };
1140
+ let b = o.borrow();
1141
+ let num = |k: &str| match b.strings.get(k) {
1142
+ Some(Value::Number(n)) => *n,
1143
+ _ => f64::NAN,
1144
+ };
1145
+ let string = |k: &str| match b.strings.get(k) {
1146
+ Some(Value::String(s)) => s.to_string(),
1147
+ _ => String::new(),
1148
+ };
1149
+ (num("code"), string("stdout"), string("stderr"))
1150
+ }
1151
+
1152
+ #[test]
1153
+ fn execfile_capture_returns_code_stdout_stderr() {
1154
+ // Success: stdout captured, code 0, stderr empty.
1155
+ let args = Value::Array(VmRef::new(vec![Value::String("captured".into())]));
1156
+ let (code, out, err) =
1157
+ fields(&process_exec_file_capture(&[Value::String("echo".into()), args]));
1158
+ assert_eq!(code, 0.0);
1159
+ assert_eq!(out.trim(), "captured");
1160
+ assert_eq!(err, "");
1161
+ // Non-zero exit is reported, not an error.
1162
+ let (code, _, _) = fields(&process_exec_file_capture(&[Value::String("false".into())]));
1163
+ assert_eq!(code, 1.0);
1164
+ // Spawn failure → code -1 with the OS error in stderr (never panics).
1165
+ let (code, out, err) =
1166
+ fields(&process_exec_file_capture(&[Value::String("/no/such/prog/xyz".into())]));
1167
+ assert_eq!(code, -1.0);
1168
+ assert!(out.is_empty());
1169
+ assert!(!err.is_empty());
1170
+ // Empty program → 0 / empty (mirrors execFile).
1171
+ let (code, out, err) = fields(&process_exec_file_capture(&[]));
1172
+ assert_eq!(code, 0.0);
1173
+ assert!(out.is_empty() && err.is_empty());
1174
+ }
1015
1175
  }
1016
1176
 
1017
1177
  #[cfg(feature = "fs")]
@@ -1308,6 +1468,11 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
1308
1468
  Value::Array(arr) => {
1309
1469
  let idx = match index {
1310
1470
  Value::Number(n) => *n as usize,
1471
+ // Canonical integer string key ("0","1",…) indexes the array (#432); other keys → Null.
1472
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
1473
+ Some(i) => i,
1474
+ None => return Value::Null,
1475
+ },
1311
1476
  _ => return Value::Null,
1312
1477
  };
1313
1478
  arr.borrow().get(idx).cloned().unwrap_or(Value::Null)
@@ -1316,6 +1481,10 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
1316
1481
  Value::NumberArray(arr) => {
1317
1482
  let idx = match index {
1318
1483
  Value::Number(n) => *n as usize,
1484
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
1485
+ Some(i) => i,
1486
+ None => return Value::Null,
1487
+ },
1319
1488
  _ => return Value::Null,
1320
1489
  };
1321
1490
  arr.borrow().get(idx).copied().map(Value::Number).unwrap_or(Value::Null)
@@ -1416,6 +1585,11 @@ pub fn set_index(obj: &Value, idx: &Value, val: Value) -> Value {
1416
1585
  Value::Array(arr) => {
1417
1586
  let index = match idx {
1418
1587
  Value::Number(n) => *n as usize,
1588
+ // Canonical integer string key → index (#432); a non-index string key is a no-op write.
1589
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
1590
+ Some(i) => i,
1591
+ None => return val,
1592
+ },
1419
1593
  _ => panic!("Array index must be number"),
1420
1594
  };
1421
1595
  let mut arr_mut = arr.borrow_mut();
@@ -1432,6 +1606,11 @@ pub fn set_index(obj: &Value, idx: &Value, val: Value) -> Value {
1432
1606
  Value::NumberArray(arr) => {
1433
1607
  let index = match idx {
1434
1608
  Value::Number(n) => *n as usize,
1609
+ // Canonical integer string key → index (#432); a non-index string key is a no-op write.
1610
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
1611
+ Some(i) => i,
1612
+ None => return val,
1613
+ },
1435
1614
  _ => panic!("Array index must be number"),
1436
1615
  };
1437
1616
  let mut arr_mut = arr.borrow_mut();
@@ -1530,6 +1709,11 @@ mod timers;
1530
1709
  #[cfg(any(feature = "http", feature = "promise"))]
1531
1710
  mod promise;
1532
1711
 
1712
+ /// Node-compatible fs surface (sync + `fs/promises`). Referenced by the backends via
1713
+ /// `tishlang_runtime::fs_ext::*`. See issue #122.
1714
+ #[cfg(feature = "fs")]
1715
+ pub mod fs_ext;
1716
+
1533
1717
  #[cfg(feature = "http")]
1534
1718
  mod native_promise;
1535
1719
 
@@ -1830,6 +2014,48 @@ pub fn string_match_regex(s: &Value, regexp: &Value) -> Value {
1830
2014
  }
1831
2015
  }
1832
2016
 
2017
+ /// `String.prototype.matchAll(regexp)` — an ITERATOR over every match, each an exec-style match object
2018
+ /// (`{ "0", ...groups, index, input, groups }`). Per ECMA the regexp must be global, else a `TypeError`
2019
+ /// is parked; a non-RegExp argument is coerced to a global regex. Reuses `regexp_exec_impl` so the
2020
+ /// match objects are byte-identical to `exec`/`match`, then wraps them in the shared iterator.
2021
+ #[cfg(feature = "regex")]
2022
+ pub fn string_match_all_regex(s: &Value, regexp: &Value) -> Value {
2023
+ let input = match s {
2024
+ Value::String(s) => s.as_ref(),
2025
+ _ => return Value::Null,
2026
+ };
2027
+ let re_cell = match regexp {
2028
+ Value::RegExp(re) => re.clone(),
2029
+ other => {
2030
+ let pattern = other.to_display_string();
2031
+ match tishlang_core::TishRegExp::new(&pattern, "g") {
2032
+ Ok(re) => tishlang_core::VmRef::new(re),
2033
+ Err(_) => return Value::Null,
2034
+ }
2035
+ }
2036
+ };
2037
+ if !re_cell.borrow().flags.global {
2038
+ tishlang_core::set_pending_throw(tishlang_core::type_error(
2039
+ "String.prototype.matchAll called with a non-global RegExp argument",
2040
+ ));
2041
+ return Value::Null;
2042
+ }
2043
+ let mut results: Vec<Value> = Vec::new();
2044
+ {
2045
+ let mut re = re_cell.borrow_mut();
2046
+ re.last_index = 0;
2047
+ loop {
2048
+ let m = regexp_exec_impl(&mut re, input);
2049
+ if matches!(m, Value::Null) {
2050
+ break;
2051
+ }
2052
+ results.push(m);
2053
+ }
2054
+ re.last_index = 0;
2055
+ }
2056
+ tishlang_builtins::iterator::array_iterator(results)
2057
+ }
2058
+
1833
2059
  #[cfg(feature = "regex")]
1834
2060
  fn string_replace_regex_or_callback(s: &Value, search: &Value, replacement: &Value) -> Value {
1835
2061
  let input = match s {
@@ -188,7 +188,12 @@ fn collect_fun_decl_names_stmt(stmt: &Statement, names: &mut HashSet<String>) {
188
188
  }
189
189
  collect_fun_decl_names_stmt(body, names);
190
190
  }
191
- Statement::ForOf { iterable, body, .. } => {
191
+ Statement::ForOf { iterable, body, .. }
192
+ | Statement::ForIn {
193
+ object: iterable,
194
+ body,
195
+ ..
196
+ } => {
192
197
  collect_fun_decl_names_expr(iterable, names);
193
198
  collect_fun_decl_names_stmt(body, names);
194
199
  }
@@ -646,7 +651,12 @@ fn stmt_contains_jsx_fragment(stmt: &tishlang_ast::Statement) -> bool {
646
651
  || update.as_ref().is_some_and(expr_contains_jsx_fragment)
647
652
  || stmt_contains_jsx_fragment(body)
648
653
  }
649
- Statement::ForOf { iterable, body, .. } => {
654
+ Statement::ForOf { iterable, body, .. }
655
+ | Statement::ForIn {
656
+ object: iterable,
657
+ body,
658
+ ..
659
+ } => {
650
660
  expr_contains_jsx_fragment(iterable) || stmt_contains_jsx_fragment(body)
651
661
  }
652
662
  Statement::Switch {
@@ -827,7 +837,12 @@ fn stmt_contains_jsx(stmt: &tishlang_ast::Statement) -> bool {
827
837
  || update.as_ref().is_some_and(expr_contains_jsx)
828
838
  || stmt_contains_jsx(body)
829
839
  }
830
- Statement::ForOf { iterable, body, .. } => {
840
+ Statement::ForOf { iterable, body, .. }
841
+ | Statement::ForIn {
842
+ object: iterable,
843
+ body,
844
+ ..
845
+ } => {
831
846
  expr_contains_jsx(iterable) || stmt_contains_jsx(body)
832
847
  }
833
848
  Statement::Switch {
@@ -3634,7 +3634,7 @@ mod tests {
3634
3634
  #[test]
3635
3635
  fn osr_region_handles_branches() {
3636
3636
  let chunk = top_chunk(
3637
- "let s = 0.0\nlet i = 0.0\nwhile (i < 20.0) { if (i % 2.0 == 0.0) { s = s + i }; i = i + 1.0 }\n",
3637
+ "let s = 0.0\nlet i = 0.0\nwhile (i < 20.0) { if (i % 2.0 === 0.0) { s = s + i }; i = i + 1.0 }\n",
3638
3638
  );
3639
3639
  let (header, end) = first_region(&chunk);
3640
3640
  let lf =