@tishlang/tish 2.2.7 → 2.9.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 (55) hide show
  1. package/bin/tish +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +5 -1
  3. package/crates/tish/Cargo.toml +2 -1
  4. package/crates/tish/src/cli_help.rs +15 -0
  5. package/crates/tish/src/main.rs +139 -12
  6. package/crates/tish/tests/integration_test.rs +65 -0
  7. package/crates/tish_ast/src/ast.rs +6 -2
  8. package/crates/tish_builtins/src/array.rs +82 -1
  9. package/crates/tish_builtins/src/globals.rs +50 -16
  10. package/crates/tish_builtins/src/math.rs +63 -9
  11. package/crates/tish_builtins/src/number.rs +20 -1
  12. package/crates/tish_builtins/src/string.rs +68 -4
  13. package/crates/tish_builtins/src/typedarrays.rs +23 -16
  14. package/crates/tish_bytecode/src/compiler.rs +94 -28
  15. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
  16. package/crates/tish_compile/src/check.rs +1 -1
  17. package/crates/tish_compile/src/codegen.rs +3133 -582
  18. package/crates/tish_compile/src/infer.rs +1950 -56
  19. package/crates/tish_compile/src/lib.rs +38 -0
  20. package/crates/tish_compile/src/resolve.rs +3 -2
  21. package/crates/tish_compile/src/types.rs +17 -0
  22. package/crates/tish_compile_js/Cargo.toml +3 -0
  23. package/crates/tish_compile_js/src/codegen.rs +365 -9
  24. package/crates/tish_compile_js/src/lib.rs +2 -1
  25. package/crates/tish_compile_js/src/tests_jsx.rs +229 -1
  26. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  27. package/crates/tish_core/Cargo.toml +2 -0
  28. package/crates/tish_core/src/json.rs +135 -34
  29. package/crates/tish_core/src/lib.rs +23 -1
  30. package/crates/tish_core/src/value.rs +64 -11
  31. package/crates/tish_eval/src/eval.rs +144 -197
  32. package/crates/tish_eval/src/natives.rs +45 -45
  33. package/crates/tish_eval/src/regex.rs +35 -27
  34. package/crates/tish_eval/src/value.rs +8 -0
  35. package/crates/tish_fmt/src/lib.rs +45 -1
  36. package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
  37. package/crates/tish_lint/src/lib.rs +197 -21
  38. package/crates/tish_lsp/Cargo.toml +6 -0
  39. package/crates/tish_lsp/src/import_goto.rs +52 -7
  40. package/crates/tish_lsp/src/main.rs +856 -140
  41. package/crates/tish_opt/src/lib.rs +5 -3
  42. package/crates/tish_parser/src/lib.rs +23 -5
  43. package/crates/tish_parser/src/parser.rs +15 -26
  44. package/crates/tish_resolve/src/lib.rs +188 -18
  45. package/crates/tish_runtime/src/lib.rs +58 -18
  46. package/crates/tish_ui/src/jsx.rs +2 -2
  47. package/crates/tish_vm/src/jit.rs +212 -10
  48. package/crates/tish_vm/src/vm.rs +39 -18
  49. package/crates/tish_wasm/src/lib.rs +116 -22
  50. package/package.json +1 -1
  51. package/platform/darwin-arm64/tish +0 -0
  52. package/platform/darwin-x64/tish +0 -0
  53. package/platform/linux-arm64/tish +0 -0
  54. package/platform/linux-x64/tish +0 -0
  55. package/platform/win32-x64/tish.exe +0 -0
@@ -429,8 +429,8 @@ fn optimize_expr(expr: &Expr) -> Expr {
429
429
  props: props
430
430
  .iter()
431
431
  .map(|p| match p {
432
- tishlang_ast::ObjectProp::KeyValue(k, v) => {
433
- tishlang_ast::ObjectProp::KeyValue(Arc::clone(k), optimize_expr(v))
432
+ tishlang_ast::ObjectProp::KeyValue(k, v, s) => {
433
+ tishlang_ast::ObjectProp::KeyValue(Arc::clone(k), optimize_expr(v), *s)
434
434
  }
435
435
  tishlang_ast::ObjectProp::Spread(e) => {
436
436
  tishlang_ast::ObjectProp::Spread(optimize_expr(e))
@@ -578,7 +578,9 @@ fn js_number_to_string(value: f64) -> String {
578
578
  return "-Infinity".to_string();
579
579
  }
580
580
  if value == 0.0 {
581
- return if value.is_sign_negative() { "-0" } else { "0" }.to_string();
581
+ // ECMAScript `Number::toString`: both `+0` and `-0` `"0"` (a constant-folded
582
+ // `"" + (-0)` must match the runtime ToString, not the inspect form). (#247)
583
+ return "0".to_string();
582
584
  }
583
585
  let negative = value < 0.0;
584
586
  let sci = format!("{:e}", value.abs());
@@ -45,6 +45,24 @@ mod tests {
45
45
  }
46
46
  }
47
47
 
48
+ // #158: a block's span — and the enclosing function's span — must end at the closing `}`, not
49
+ // overrun onto the statement on the following line.
50
+ #[test]
51
+ fn block_and_fn_spans_stop_at_closing_brace() {
52
+ // `}` is on line 3 (1-based); `let after` begins on line 4.
53
+ let program = parse("fn f(a) {\n let x = a\n}\nlet after = 1\n").expect("parse");
54
+ let Statement::FunDecl { span, body, .. } = &program.statements[0] else {
55
+ panic!("expected FunDecl");
56
+ };
57
+ assert_eq!(span.end.0, 3, "fn span must end on the `}}` line, not the next statement: {span:?}");
58
+ assert_eq!(
59
+ body.span().end.0,
60
+ 3,
61
+ "body block span must end on the `}}` line: {:?}",
62
+ body.span()
63
+ );
64
+ }
65
+
48
66
  #[test]
49
67
  fn test_object_literal_shorthand_single() {
50
68
  let program = parse("const o = { port }").expect("parse object shorthand");
@@ -62,7 +80,7 @@ mod tests {
62
80
  };
63
81
  assert_eq!(props.len(), 1);
64
82
  match &props[0] {
65
- ObjectProp::KeyValue(k, v) => {
83
+ ObjectProp::KeyValue(k, v, _) => {
66
84
  assert_eq!(k.as_ref(), "port");
67
85
  if let Expr::Ident { ref name, .. } = v {
68
86
  assert_eq!(name.as_ref(), "port");
@@ -92,11 +110,11 @@ mod tests {
92
110
  };
93
111
  assert_eq!(props.len(), 2);
94
112
  match &props[0] {
95
- ObjectProp::KeyValue(k, _) => assert_eq!(k.as_ref(), "ai-a"),
113
+ ObjectProp::KeyValue(k, _, _) => assert_eq!(k.as_ref(), "ai-a"),
96
114
  _ => panic!("expected KeyValue prop"),
97
115
  }
98
116
  match &props[1] {
99
- ObjectProp::KeyValue(k, _) => assert_eq!(k.as_ref(), "human"),
117
+ ObjectProp::KeyValue(k, _, _) => assert_eq!(k.as_ref(), "human"),
100
118
  _ => panic!("expected KeyValue prop"),
101
119
  }
102
120
  }
@@ -118,7 +136,7 @@ mod tests {
118
136
  };
119
137
  assert_eq!(props.len(), 2);
120
138
  match &props[0] {
121
- ObjectProp::KeyValue(k, v) => {
139
+ ObjectProp::KeyValue(k, v, _) => {
122
140
  assert_eq!(k.as_ref(), "port");
123
141
  if let Expr::Ident { ref name, .. } = v {
124
142
  assert_eq!(name.as_ref(), "port");
@@ -129,7 +147,7 @@ mod tests {
129
147
  _ => panic!("expected KeyValue prop"),
130
148
  }
131
149
  match &props[1] {
132
- ObjectProp::KeyValue(k, v) => {
150
+ ObjectProp::KeyValue(k, v, _) => {
133
151
  assert_eq!(k.as_ref(), "x");
134
152
  if let Expr::Literal { .. } = v {
135
153
  // x: 1
@@ -364,27 +364,21 @@ impl<'a> Parser<'a> {
364
364
  statements.push(self.parse_statement()?);
365
365
  }
366
366
 
367
+ // Block ends at its CLOSING `}`/`Dedent`. Capture that token's end BEFORE advancing past it:
368
+ // reading `self.peek()` *after* the advance pointed at the NEXT token, overrunning the block
369
+ // span onto the following statement — so a cursor on the line after `}` was still "inside"
370
+ // the block and completion offered the body's params/locals there (#158). Fall back to the
371
+ // last inner statement's end for an unterminated block at EOF, else the block start.
372
+ let mut closer_end: Option<(usize, usize)> = None;
367
373
  if matches!(
368
374
  self.peek_kind(),
369
375
  Some(TokenKind::RBrace | TokenKind::Dedent)
370
376
  ) {
377
+ closer_end = self.peek().map(|t| t.span.end);
371
378
  self.advance();
372
379
  }
373
-
374
- let peek_end = self.peek().map(|x| x.span.end);
375
380
  let last_end = statements.last().map(|s| s.span().end);
376
- let end = match (peek_end, last_end) {
377
- (Some(p), Some(l)) => {
378
- if p.0 > l.0 || (p.0 == l.0 && p.1 > l.1) {
379
- p
380
- } else {
381
- l
382
- }
383
- }
384
- (Some(p), None) => p,
385
- (None, Some(l)) => l,
386
- (None, None) => span_start,
387
- };
381
+ let end = closer_end.or(last_end).unwrap_or(span_start);
388
382
 
389
383
  Ok(Statement::Block {
390
384
  statements,
@@ -985,17 +979,12 @@ impl<'a> Parser<'a> {
985
979
  Box::new(self.parse_block()?)
986
980
  };
987
981
 
988
- // Span must cover the whole declaration through the body. `peek().start` alone can sit on
989
- // the opening `{` (same as `span_start` at EOF) or otherwise truncate before inner spans.
990
- let peek_start = self.peek().map(|t| t.span.start).unwrap_or(span_start);
991
- let body_end = body.as_ref().span().end;
992
- let end = if peek_start.0 > body_end.0
993
- || (peek_start.0 == body_end.0 && peek_start.1 > body_end.1)
994
- {
995
- peek_start
996
- } else {
997
- body_end
998
- };
982
+ // The declaration spans from `fn`/`async` through the END of its body. `parse_block` now
983
+ // ends the body block exactly at its closing `}` (#158), so the body's end IS the function's
984
+ // end. (This previously maxed with `peek().start` — the NEXT token after the function — to
985
+ // compensate for the old block-span truncation, which instead overran the function span past
986
+ // `}` and inflated its folding / document-symbol range onto the following line. #158)
987
+ let end = body.as_ref().span().end;
999
988
 
1000
989
  Ok(Statement::FunDecl {
1001
990
  async_,
@@ -2243,7 +2232,7 @@ impl<'a> Parser<'a> {
2243
2232
  return Err("String key in object literal requires explicit value (key: value)".to_string());
2244
2233
  }
2245
2234
  };
2246
- props.push(ObjectProp::KeyValue(key, value));
2235
+ props.push(ObjectProp::KeyValue(key, value, key_span));
2247
2236
  }
2248
2237
  if !matches!(self.peek_kind(), Some(TokenKind::RBrace)) {
2249
2238
  self.expect(TokenKind::Comma)?;
@@ -81,6 +81,15 @@ thread_local! {
81
81
  /// no behavior change.
82
82
  static REFERENCED_DEFS: std::cell::RefCell<Option<std::collections::HashSet<(usize, usize)>>> =
83
83
  const { std::cell::RefCell::new(None) };
84
+
85
+ /// Stack of definition-span starts of the functions whose bodies we are currently inside. A use
86
+ /// that resolves to one of these is a SELF-reference (e.g. `fn a() { return a() }`) — recursion,
87
+ /// not an external use — so it must not mark the binding "referenced" for the unused check, or a
88
+ /// function reachable only via its own recursion would never be flagged dead (#150). Only
89
+ /// consulted while `REFERENCED_DEFS` is active; find-references / go-to-definition (which want the
90
+ /// recursive call) use a different path and are unaffected.
91
+ static CURRENT_FN_DEFS: std::cell::RefCell<Vec<(usize, usize)>> =
92
+ const { std::cell::RefCell::new(Vec::new()) };
84
93
  }
85
94
 
86
95
  /// One scope-aware resolution pass: the set of definition span *starts* that at least one identifier
@@ -92,6 +101,9 @@ fn collect_referenced_def_spans(program: &Program) -> std::collections::HashSet<
92
101
  impl Drop for Guard {
93
102
  fn drop(&mut self) {
94
103
  REFERENCED_DEFS.with(|r| *r.borrow_mut() = None);
104
+ // Defensive: the FunDecl walk push/pops this in balanced pairs, but clear it here too so
105
+ // a panic mid-walk can't leave a stale enclosing-fn span for the next collection.
106
+ CURRENT_FN_DEFS.with(|s| s.borrow_mut().clear());
95
107
  }
96
108
  }
97
109
  let _g = Guard;
@@ -457,7 +469,7 @@ fn collect_expr(
457
469
  Expr::Object { props, .. } => {
458
470
  for p in props {
459
471
  match p {
460
- tishlang_ast::ObjectProp::KeyValue(_, e) => {
472
+ tishlang_ast::ObjectProp::KeyValue(_, e, _) => {
461
473
  collect_expr(e, source, lsp_line, lsp_char, best)
462
474
  }
463
475
  tishlang_ast::ObjectProp::Spread(e) => {
@@ -729,7 +741,7 @@ fn member_chain_collect_expr(
729
741
  Expr::Object { props, .. } => {
730
742
  for p in props {
731
743
  match p {
732
- tishlang_ast::ObjectProp::KeyValue(_, e) => {
744
+ tishlang_ast::ObjectProp::KeyValue(_, e, _) => {
733
745
  member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
734
746
  }
735
747
  tishlang_ast::ObjectProp::Spread(e) => {
@@ -1213,7 +1225,7 @@ fn walk_expr_resolve(
1213
1225
  Expr::Object { props, .. } => {
1214
1226
  for p in props {
1215
1227
  let e = match p {
1216
- tishlang_ast::ObjectProp::KeyValue(_, e) => e,
1228
+ tishlang_ast::ObjectProp::KeyValue(_, e, _) => e,
1217
1229
  tishlang_ast::ObjectProp::Spread(e) => e,
1218
1230
  };
1219
1231
  if let Some(s) = walk_expr_resolve(e, scopes, target) {
@@ -1730,11 +1742,17 @@ fn record_unresolved(
1730
1742
  // collect_unused_bindings learns this def is referenced — done before the global check so a
1731
1743
  // local that shadows a global name still counts as a use. Unresolved-name output unchanged.
1732
1744
  Some(def) => {
1733
- REFERENCED_DEFS.with(|r| {
1734
- if let Some(set) = r.borrow_mut().as_mut() {
1735
- set.insert(def.start);
1736
- }
1737
- });
1745
+ // A use that resolves to a function we're currently inside is a self-recursive call,
1746
+ // not an external use — skip it so a function reachable only via its own recursion is
1747
+ // still reported unused (#150).
1748
+ let is_self_ref = CURRENT_FN_DEFS.with(|s| s.borrow().contains(&def.start));
1749
+ if !is_self_ref {
1750
+ REFERENCED_DEFS.with(|r| {
1751
+ if let Some(set) = r.borrow_mut().as_mut() {
1752
+ set.insert(def.start);
1753
+ }
1754
+ });
1755
+ }
1738
1756
  }
1739
1757
  None => {
1740
1758
  if !is_runtime_global_ident(name.as_ref()) {
@@ -1747,12 +1765,32 @@ fn record_unresolved(
1747
1765
  }
1748
1766
  }
1749
1767
 
1768
+ /// Like [`record_unresolved`] for an assignment TARGET: still flags an unresolved name (writing to
1769
+ /// an undeclared variable is an error), but does NOT mark a resolved def as *referenced*. A pure
1770
+ /// write — and, like ESLint's `no-unused-vars`, a read-modify-write (`+=`/`++`/`||=`) — is not a
1771
+ /// "use", so a binding that is only ever written gets reported as unused. Reads elsewhere (incl. the
1772
+ /// RHS, e.g. `n = n + 1`) still go through `record_unresolved` and keep the binding live. (#149)
1773
+ fn record_write(
1774
+ scopes: &ScopeStack,
1775
+ name: &Arc<str>,
1776
+ span: tishlang_ast::Span,
1777
+ out: &mut Vec<UnresolvedIdentifier>,
1778
+ ) {
1779
+ if scopes.resolve(name.as_ref()).is_none() && !is_runtime_global_ident(name.as_ref()) {
1780
+ out.push(UnresolvedIdentifier {
1781
+ name: name.clone(),
1782
+ span,
1783
+ });
1784
+ }
1785
+ }
1786
+
1750
1787
  fn check_unresolved_expr(expr: &Expr, scopes: &ScopeStack, out: &mut Vec<UnresolvedIdentifier>) {
1751
1788
  match expr {
1752
1789
  Expr::Ident { name, span } => record_unresolved(scopes, name, *span, out),
1753
1790
  Expr::Assign { name, span, value } => {
1791
+ // Target is a WRITE (not a read) for unused detection; the RHS is read normally.
1754
1792
  let sp = synthetic_name_span(span.start, name.as_ref());
1755
- record_unresolved(scopes, name, sp, out);
1793
+ record_write(scopes, name, sp, out);
1756
1794
  check_unresolved_expr(value, scopes, out);
1757
1795
  }
1758
1796
  Expr::CompoundAssign {
@@ -1761,17 +1799,18 @@ fn check_unresolved_expr(expr: &Expr, scopes: &ScopeStack, out: &mut Vec<Unresol
1761
1799
  | Expr::LogicalAssign {
1762
1800
  name, span, value, ..
1763
1801
  } => {
1802
+ // Read-modify-write: ESLint counts the target as a write, not a use. RHS is read.
1764
1803
  let sp = synthetic_name_span(span.start, name.as_ref());
1765
- record_unresolved(scopes, name, sp, out);
1804
+ record_write(scopes, name, sp, out);
1766
1805
  check_unresolved_expr(value, scopes, out);
1767
1806
  }
1768
1807
  Expr::PostfixInc { name, span } | Expr::PostfixDec { name, span } => {
1769
1808
  let sp = synthetic_name_span(span.start, name.as_ref());
1770
- record_unresolved(scopes, name, sp, out);
1809
+ record_write(scopes, name, sp, out);
1771
1810
  }
1772
1811
  Expr::PrefixInc { name, span } | Expr::PrefixDec { name, span } => {
1773
1812
  let sp = synthetic_name_span(span.start, name.as_ref());
1774
- record_unresolved(scopes, name, sp, out);
1813
+ record_write(scopes, name, sp, out);
1775
1814
  }
1776
1815
  Expr::Binary { left, right, .. } => {
1777
1816
  check_unresolved_expr(left, scopes, out);
@@ -1834,7 +1873,7 @@ fn check_unresolved_expr(expr: &Expr, scopes: &ScopeStack, out: &mut Vec<Unresol
1834
1873
  Expr::Object { props, .. } => {
1835
1874
  for p in props {
1836
1875
  let e = match p {
1837
- tishlang_ast::ObjectProp::KeyValue(_, e) => e,
1876
+ tishlang_ast::ObjectProp::KeyValue(_, e, _) => e,
1838
1877
  tishlang_ast::ObjectProp::Spread(e) => e,
1839
1878
  };
1840
1879
  check_unresolved_expr(e, scopes, out);
@@ -2033,7 +2072,13 @@ fn check_unresolved_stmt(
2033
2072
  check_unresolved_expr(e, scopes, out);
2034
2073
  }
2035
2074
  }
2075
+ // Mark this function as "currently inside" so a recursive self-call in the body isn't
2076
+ // counted as an external use for the unused-binding check (#150).
2077
+ CURRENT_FN_DEFS.with(|s| s.borrow_mut().push(name_span.start));
2036
2078
  check_unresolved_stmt(body, scopes, out);
2079
+ CURRENT_FN_DEFS.with(|s| {
2080
+ s.borrow_mut().pop();
2081
+ });
2037
2082
  scopes.pop();
2038
2083
  scopes.define(name.as_ref(), *name_span);
2039
2084
  }
@@ -2324,7 +2369,7 @@ fn enumerate_expr(expr: &Expr, exported: bool, out: &mut Vec<BindingSite>) {
2324
2369
  Expr::Object { props, .. } => {
2325
2370
  for p in props {
2326
2371
  let e = match p {
2327
- tishlang_ast::ObjectProp::KeyValue(_, e) => e,
2372
+ tishlang_ast::ObjectProp::KeyValue(_, e, _) => e,
2328
2373
  tishlang_ast::ObjectProp::Spread(e) => e,
2329
2374
  };
2330
2375
  enumerate_expr(e, exported, out);
@@ -2829,7 +2874,7 @@ fn jsx_tags_expr(e: &Expr, out: &mut std::collections::HashSet<Arc<str>>) {
2829
2874
  Expr::Object { props, .. } => {
2830
2875
  for p in props {
2831
2876
  match p {
2832
- tishlang_ast::ObjectProp::KeyValue(_, e)
2877
+ tishlang_ast::ObjectProp::KeyValue(_, e, _)
2833
2878
  | tishlang_ast::ObjectProp::Spread(e) => jsx_tags_expr(e, out),
2834
2879
  }
2835
2880
  }
@@ -2877,6 +2922,19 @@ fn jsx_tags_expr(e: &Expr, out: &mut std::collections::HashSet<Arc<str>>) {
2877
2922
  }
2878
2923
  }
2879
2924
 
2925
+ /// Names referenced in a TYPE position — every `Simple(name)` use in a type annotation (param /
2926
+ /// var / return types, alias bodies, nested object/array/union/fn types). A binding (a type-only
2927
+ /// import or a type alias) used *only* as a type would otherwise be reported unused (#139), since
2928
+ /// the value-resolution walk never sees annotations. Name-based and intentionally conservative,
2929
+ /// mirroring the JSX-tag set.
2930
+ fn type_reference_names(program: &Program) -> std::collections::HashSet<Arc<str>> {
2931
+ let mut occ: Vec<TypeOcc> = Vec::new();
2932
+ for s in &program.statements {
2933
+ tref_stmt(s, &mut occ);
2934
+ }
2935
+ occ.into_iter().filter(|o| !o.is_decl).map(|o| o.name).collect()
2936
+ }
2937
+
2880
2938
  /// Declarations whose values are never read (imports, locals, parameters). Skips `exported` module
2881
2939
  /// bindings and names starting with `_` (common intentional-unused convention).
2882
2940
  pub fn collect_unused_bindings(program: &Program, _source: &str) -> Vec<UnusedBinding> {
@@ -2888,6 +2946,9 @@ pub fn collect_unused_bindings(program: &Program, _source: &str) -> Vec<UnusedBi
2888
2946
  // resolution walk can't see the tag, so consult the tag set to avoid a false "unused" report
2889
2947
  // (deleting such an import would break the build).
2890
2948
  let jsx_tags = collect_jsx_component_tags(program);
2949
+ // A binding referenced only in a type annotation (`: Foo`) is used; the value walk can't see
2950
+ // type positions, so consult the type-reference names to avoid a false "unused" report (#139).
2951
+ let type_refs = type_reference_names(program);
2891
2952
  // Which declarations are referenced, learned in ONE scope-aware pass. The previous approach
2892
2953
  // re-scanned the whole program per binding (re-resolving each use), which was ~O(N³) and froze
2893
2954
  // large files; this is O(N). A binding is unused iff nothing resolves to its definition span.
@@ -2900,6 +2961,9 @@ pub fn collect_unused_bindings(program: &Program, _source: &str) -> Vec<UnusedBi
2900
2961
  if jsx_tags.contains(&site.name) {
2901
2962
  continue;
2902
2963
  }
2964
+ if type_refs.contains(&site.name) {
2965
+ continue;
2966
+ }
2903
2967
  if !referenced.contains(&site.span.start) {
2904
2968
  out.push(UnusedBinding {
2905
2969
  name: site.name,
@@ -3219,7 +3283,7 @@ fn walk_expr_completion(
3219
3283
  Expr::Object { props, .. } => {
3220
3284
  for p in props {
3221
3285
  let e = match p {
3222
- tishlang_ast::ObjectProp::KeyValue(_, e) => e,
3286
+ tishlang_ast::ObjectProp::KeyValue(_, e, _) => e,
3223
3287
  tishlang_ast::ObjectProp::Spread(e) => e,
3224
3288
  };
3225
3289
  walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
@@ -3695,7 +3759,7 @@ fn tref_expr(expr: &Expr, out: &mut Vec<TypeOcc>) {
3695
3759
  Expr::Object { props, .. } => {
3696
3760
  for p in props {
3697
3761
  match p {
3698
- tishlang_ast::ObjectProp::KeyValue(_, e)
3762
+ tishlang_ast::ObjectProp::KeyValue(_, e, _)
3699
3763
  | tishlang_ast::ObjectProp::Spread(e) => tref_expr(e, out),
3700
3764
  }
3701
3765
  }
@@ -4159,7 +4223,7 @@ fn refs_expr(
4159
4223
  Expr::Object { props, .. } => {
4160
4224
  for p in props {
4161
4225
  match p {
4162
- tishlang_ast::ObjectProp::KeyValue(_, e) => {
4226
+ tishlang_ast::ObjectProp::KeyValue(_, e, _) => {
4163
4227
  refs_expr(e, program, source, name, def_span, out)
4164
4228
  }
4165
4229
  tishlang_ast::ObjectProp::Spread(e) => {
@@ -4317,6 +4381,20 @@ mod tests {
4317
4381
  assert!(names.iter().any(|n| n.as_ref() == "bar"));
4318
4382
  }
4319
4383
 
4384
+ // #158: on the line AFTER a function's closing brace, completion must not leak the function's
4385
+ // params or inner locals (the body Block span used to overrun onto that line). The top-level
4386
+ // function name is still in scope.
4387
+ #[test]
4388
+ fn completion_does_not_leak_past_function_brace() {
4389
+ let src = "fn f(paramA, paramB) {\n let secret = 1\n}\nlet after = 0\n";
4390
+ let program = parse(src).expect("parse");
4391
+ let names = completion_value_names_at_cursor(&program, src, 3, 0); // start of `let after`
4392
+ let has = |n: &str| names.iter().any(|x| x.as_ref() == n);
4393
+ assert!(!has("paramA") && !has("paramB"), "params leaked past `}}`: {names:?}");
4394
+ assert!(!has("secret"), "inner local leaked past `}}`: {names:?}");
4395
+ assert!(has("f"), "top-level fn must stay in scope: {names:?}");
4396
+ }
4397
+
4320
4398
  #[test]
4321
4399
  fn unresolved_unknown_ident() {
4322
4400
  let src = "let x = nope\n";
@@ -4390,6 +4468,74 @@ mod tests {
4390
4468
  assert_eq!(u[0].kind, UnusedBindingKind::Import);
4391
4469
  }
4392
4470
 
4471
+ // #149: a binding that is only ever WRITTEN (never read) is unused (matches ESLint).
4472
+ #[test]
4473
+ fn write_only_variables_are_flagged_unused() {
4474
+ for (src, who) in [
4475
+ ("let n = 0\nn += 5\n", "n"), // read-modify-write
4476
+ ("let w = 0\nw = 7\n", "w"), // plain write
4477
+ ("let p = 0\np++\n", "p"), // postfix inc
4478
+ ("let q = 0\n++q\n", "q"), // prefix inc
4479
+ ] {
4480
+ let program = parse(src).expect("parse");
4481
+ let u = collect_unused_bindings(&program, src);
4482
+ assert!(
4483
+ u.iter().any(|b| b.name.as_ref() == who
4484
+ && b.kind == UnusedBindingKind::Variable),
4485
+ "{who} is write-only and should be flagged unused; src={src:?} u={u:?}"
4486
+ );
4487
+ }
4488
+ }
4489
+
4490
+ #[test]
4491
+ fn variables_read_anywhere_are_not_flagged() {
4492
+ // A read on the RHS of its own assignment, or a bare use, keeps the binding live.
4493
+ for src in [
4494
+ "let count = 0\ncount = count + 1\ncount\n", // read in RHS + bare use
4495
+ "let acc = 0\nacc += 1\nacc\n", // compound write + later read
4496
+ "let x = 1\nx\n", // plain read (control)
4497
+ ] {
4498
+ let program = parse(src).expect("parse");
4499
+ let u = collect_unused_bindings(&program, src);
4500
+ assert!(u.is_empty(), "no unused expected; src={src:?} u={u:?}");
4501
+ }
4502
+ }
4503
+
4504
+ // #150: a function reachable only via its own recursion is dead code and should be flagged.
4505
+ #[test]
4506
+ fn self_recursive_only_function_is_flagged_unused() {
4507
+ for src in ["fn a() { return a() }\n", "fn loop_(n) { return loop_(n - 1) }\n"] {
4508
+ let program = parse(src).expect("parse");
4509
+ let u = collect_unused_bindings(&program, src);
4510
+ assert_eq!(u.len(), 1, "self-recursive-only fn is unused; src={src:?} u={u:?}");
4511
+ }
4512
+ }
4513
+
4514
+ #[test]
4515
+ fn externally_called_or_mutually_recursive_functions_not_flagged() {
4516
+ // External call keeps it live; mutual recursion (each refers to the OTHER) keeps both live —
4517
+ // only pure SELF-reference is excluded.
4518
+ for src in [
4519
+ "fn c() { return 1 }\nc()\n",
4520
+ "fn p() { return q() }\nfn q() { return p() }\np()\n",
4521
+ ] {
4522
+ let program = parse(src).expect("parse");
4523
+ let u = collect_unused_bindings(&program, src);
4524
+ assert!(u.is_empty(), "no unused expected; src={src:?} u={u:?}");
4525
+ }
4526
+ }
4527
+
4528
+ #[test]
4529
+ fn write_to_undeclared_still_unresolved() {
4530
+ // record_write must keep flagging a write to an undeclared name (not silently swallow it).
4531
+ let program = parse("undeclared = 5\n").expect("parse");
4532
+ let unresolved = collect_unresolved_identifiers(&program);
4533
+ assert!(
4534
+ unresolved.iter().any(|u| u.name.as_ref() == "undeclared"),
4535
+ "writing to an undeclared variable is still unresolved; got={unresolved:?}"
4536
+ );
4537
+ }
4538
+
4393
4539
  #[test]
4394
4540
  fn reference_spans_include_uses_in_exported_fn_body() {
4395
4541
  // Directly exercises the fixed walker (also backs find-references / rename).
@@ -4710,3 +4856,27 @@ mod tests {
4710
4856
  );
4711
4857
  }
4712
4858
  }
4859
+
4860
+ #[cfg(test)]
4861
+ mod type_ref_unused_tests {
4862
+ use super::*;
4863
+ fn unused(src: &str) -> Vec<String> {
4864
+ collect_unused_bindings(&tishlang_parser::parse(src).expect("parse"), src)
4865
+ .iter().map(|b| b.name.to_string()).collect()
4866
+ }
4867
+ // #139: an import used only in a type annotation must not be flagged unused.
4868
+ #[test] fn type_only_import_not_unused() {
4869
+ let u = unused("import { Foo } from \"./m\"\nlet x: Foo = bar()\nx\n");
4870
+ assert!(!u.contains(&"Foo".to_string()), "type-only import Foo wrongly unused; u={u:?}");
4871
+ }
4872
+ // #139: a type alias referenced only in an annotation must not be flagged unused.
4873
+ #[test] fn type_alias_used_in_annotation_not_unused() {
4874
+ let u = unused("type T = number\nfn f(x: T) { return x }\nf(1)\n");
4875
+ assert!(!u.contains(&"T".to_string()), "type alias T wrongly unused; u={u:?}");
4876
+ }
4877
+ // guard: a genuinely-unused import is still reported.
4878
+ #[test] fn genuinely_unused_import_still_flagged() {
4879
+ let u = unused("import { Dead } from \"./m\"\nlet y = 1\ny\n");
4880
+ assert!(u.contains(&"Dead".to_string()), "Dead should be unused; u={u:?}");
4881
+ }
4882
+ }