@tishlang/tish 2.2.5 → 2.8.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 (53) 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 +1 -1
  4. package/crates/tish/src/cli_help.rs +12 -0
  5. package/crates/tish/src/main.rs +69 -11
  6. package/crates/tish_ast/src/ast.rs +12 -5
  7. package/crates/tish_build_utils/src/lib.rs +37 -0
  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 +11 -10
  17. package/crates/tish_compile/src/codegen.rs +1386 -42
  18. package/crates/tish_compile/src/infer.rs +16 -16
  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 +26 -9
  22. package/crates/tish_compile_js/src/codegen.rs +55 -4
  23. package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
  24. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  25. package/crates/tish_core/Cargo.toml +2 -0
  26. package/crates/tish_core/src/json.rs +135 -34
  27. package/crates/tish_core/src/lib.rs +23 -1
  28. package/crates/tish_core/src/value.rs +64 -11
  29. package/crates/tish_eval/src/eval.rs +144 -197
  30. package/crates/tish_eval/src/natives.rs +45 -45
  31. package/crates/tish_eval/src/regex.rs +35 -27
  32. package/crates/tish_eval/src/value.rs +8 -0
  33. package/crates/tish_fmt/src/lib.rs +46 -2
  34. package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
  35. package/crates/tish_lint/src/lib.rs +197 -21
  36. package/crates/tish_lsp/Cargo.toml +7 -1
  37. package/crates/tish_lsp/src/import_goto.rs +52 -7
  38. package/crates/tish_lsp/src/main.rs +913 -145
  39. package/crates/tish_opt/src/lib.rs +5 -3
  40. package/crates/tish_parser/src/lib.rs +23 -5
  41. package/crates/tish_parser/src/parser.rs +35 -35
  42. package/crates/tish_resolve/src/lib.rs +567 -17
  43. package/crates/tish_runtime/src/lib.rs +58 -18
  44. package/crates/tish_ui/src/jsx.rs +2 -2
  45. package/crates/tish_vm/src/jit.rs +212 -10
  46. package/crates/tish_vm/src/vm.rs +39 -18
  47. package/crates/tish_wasm/src/lib.rs +116 -22
  48. package/package.json +1 -1
  49. package/platform/darwin-arm64/tish +0 -0
  50. package/platform/darwin-x64/tish +0 -0
  51. package/platform/linux-arm64/tish +0 -0
  52. package/platform/linux-x64/tish +0 -0
  53. package/platform/win32-x64/tish.exe +0 -0
@@ -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);
@@ -3570,6 +3634,358 @@ pub fn shallow_module_bindings(program: &Program) -> Vec<(Arc<str>, tishlang_ast
3570
3634
  }
3571
3635
 
3572
3636
  /// All spans (definition + uses) that resolve to `def_span` for `name`.
3637
+ // ---- Type-alias rename (#131) ---------------------------------------------------------------
3638
+ // Type names live in a namespace separate from value bindings, so the value resolver
3639
+ // (`definition_span` / `reference_spans_for_def`) never sees `: T` annotation uses. These walkers
3640
+ // gather every type-name occurrence — the alias declaration plus each span-carrying
3641
+ // `TypeAnnotation::Simple` reference in any annotation — so renaming a type alias edits the
3642
+ // declaration and all its uses together (previously only the declaration was edited, silently
3643
+ // breaking the program).
3644
+
3645
+ struct TypeOcc {
3646
+ name: Arc<str>,
3647
+ span: tishlang_ast::Span,
3648
+ is_decl: bool,
3649
+ }
3650
+
3651
+ fn tref_ann(ann: &tishlang_ast::TypeAnnotation, out: &mut Vec<TypeOcc>) {
3652
+ use tishlang_ast::TypeAnnotation as T;
3653
+ match ann {
3654
+ T::Simple(name, span) => out.push(TypeOcc {
3655
+ name: name.clone(),
3656
+ span: *span,
3657
+ is_decl: false,
3658
+ }),
3659
+ T::Array(inner) => tref_ann(inner, out),
3660
+ T::Object(fields) => {
3661
+ for (_, t) in fields {
3662
+ tref_ann(t, out);
3663
+ }
3664
+ }
3665
+ T::Function { params, returns } => {
3666
+ for p in params {
3667
+ tref_ann(p, out);
3668
+ }
3669
+ tref_ann(returns, out);
3670
+ }
3671
+ T::Union(ts) | T::Tuple(ts) | T::Intersection(ts) => {
3672
+ for t in ts {
3673
+ tref_ann(t, out);
3674
+ }
3675
+ }
3676
+ T::Literal(_) => {}
3677
+ }
3678
+ }
3679
+
3680
+ fn tref_typed_param(tp: &TypedParam, out: &mut Vec<TypeOcc>) {
3681
+ if let Some(t) = &tp.type_ann {
3682
+ tref_ann(t, out);
3683
+ }
3684
+ if let Some(d) = &tp.default {
3685
+ tref_expr(d, out);
3686
+ }
3687
+ }
3688
+
3689
+ fn tref_param(p: &FunParam, out: &mut Vec<TypeOcc>) {
3690
+ match p {
3691
+ FunParam::Simple(tp) => tref_typed_param(tp, out),
3692
+ FunParam::Destructure {
3693
+ type_ann, default, ..
3694
+ } => {
3695
+ if let Some(t) = type_ann {
3696
+ tref_ann(t, out);
3697
+ }
3698
+ if let Some(d) = default {
3699
+ tref_expr(d, out);
3700
+ }
3701
+ }
3702
+ }
3703
+ }
3704
+
3705
+ fn tref_arrow_body(body: &ArrowBody, out: &mut Vec<TypeOcc>) {
3706
+ match body {
3707
+ ArrowBody::Expr(e) => tref_expr(e, out),
3708
+ ArrowBody::Block(s) => tref_stmt(s, out),
3709
+ }
3710
+ }
3711
+
3712
+ fn tref_expr(expr: &Expr, out: &mut Vec<TypeOcc>) {
3713
+ match expr {
3714
+ Expr::ArrowFunction { params, body, .. } => {
3715
+ for p in params {
3716
+ tref_param(p, out);
3717
+ }
3718
+ tref_arrow_body(body, out);
3719
+ }
3720
+ Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
3721
+ tref_expr(left, out);
3722
+ tref_expr(right, out);
3723
+ }
3724
+ Expr::Unary { operand, .. }
3725
+ | Expr::TypeOf { operand, .. }
3726
+ | Expr::Await { operand, .. } => tref_expr(operand, out),
3727
+ Expr::Delete { target, .. } => tref_expr(target, out),
3728
+ Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
3729
+ tref_expr(callee, out);
3730
+ for a in args {
3731
+ match a {
3732
+ CallArg::Expr(e) | CallArg::Spread(e) => tref_expr(e, out),
3733
+ }
3734
+ }
3735
+ }
3736
+ Expr::Member { object, .. } => tref_expr(object, out),
3737
+ Expr::Index { object, index, .. } => {
3738
+ tref_expr(object, out);
3739
+ tref_expr(index, out);
3740
+ }
3741
+ Expr::Conditional {
3742
+ cond,
3743
+ then_branch,
3744
+ else_branch,
3745
+ ..
3746
+ } => {
3747
+ tref_expr(cond, out);
3748
+ tref_expr(then_branch, out);
3749
+ tref_expr(else_branch, out);
3750
+ }
3751
+ Expr::Array { elements, .. } => {
3752
+ for el in elements {
3753
+ match el {
3754
+ tishlang_ast::ArrayElement::Expr(e)
3755
+ | tishlang_ast::ArrayElement::Spread(e) => tref_expr(e, out),
3756
+ }
3757
+ }
3758
+ }
3759
+ Expr::Object { props, .. } => {
3760
+ for p in props {
3761
+ match p {
3762
+ tishlang_ast::ObjectProp::KeyValue(_, e, _)
3763
+ | tishlang_ast::ObjectProp::Spread(e) => tref_expr(e, out),
3764
+ }
3765
+ }
3766
+ }
3767
+ Expr::Assign { value, .. }
3768
+ | Expr::CompoundAssign { value, .. }
3769
+ | Expr::LogicalAssign { value, .. } => tref_expr(value, out),
3770
+ Expr::MemberAssign { object, value, .. } => {
3771
+ tref_expr(object, out);
3772
+ tref_expr(value, out);
3773
+ }
3774
+ Expr::IndexAssign {
3775
+ object,
3776
+ index,
3777
+ value,
3778
+ ..
3779
+ } => {
3780
+ tref_expr(object, out);
3781
+ tref_expr(index, out);
3782
+ tref_expr(value, out);
3783
+ }
3784
+ Expr::TemplateLiteral { exprs, .. } => {
3785
+ for e in exprs {
3786
+ tref_expr(e, out);
3787
+ }
3788
+ }
3789
+ _ => {}
3790
+ }
3791
+ }
3792
+
3793
+ fn tref_stmt(stmt: &Statement, out: &mut Vec<TypeOcc>) {
3794
+ match stmt {
3795
+ Statement::TypeAlias {
3796
+ name,
3797
+ name_span,
3798
+ ty,
3799
+ ..
3800
+ } => {
3801
+ out.push(TypeOcc {
3802
+ name: name.clone(),
3803
+ span: *name_span,
3804
+ is_decl: true,
3805
+ });
3806
+ tref_ann(ty, out);
3807
+ }
3808
+ Statement::VarDecl { type_ann, init, .. } => {
3809
+ if let Some(t) = type_ann {
3810
+ tref_ann(t, out);
3811
+ }
3812
+ if let Some(e) = init {
3813
+ tref_expr(e, out);
3814
+ }
3815
+ }
3816
+ Statement::VarDeclDestructure { init, .. } => tref_expr(init, out),
3817
+ Statement::DeclareVar { type_ann, .. } => {
3818
+ if let Some(t) = type_ann {
3819
+ tref_ann(t, out);
3820
+ }
3821
+ }
3822
+ Statement::FunDecl {
3823
+ params,
3824
+ rest_param,
3825
+ return_type,
3826
+ body,
3827
+ ..
3828
+ } => {
3829
+ for p in params {
3830
+ tref_param(p, out);
3831
+ }
3832
+ if let Some(rp) = rest_param {
3833
+ tref_typed_param(rp, out);
3834
+ }
3835
+ if let Some(t) = return_type {
3836
+ tref_ann(t, out);
3837
+ }
3838
+ tref_stmt(body, out);
3839
+ }
3840
+ Statement::DeclareFun {
3841
+ params,
3842
+ rest_param,
3843
+ return_type,
3844
+ ..
3845
+ } => {
3846
+ for p in params {
3847
+ tref_param(p, out);
3848
+ }
3849
+ if let Some(rp) = rest_param {
3850
+ tref_typed_param(rp, out);
3851
+ }
3852
+ if let Some(t) = return_type {
3853
+ tref_ann(t, out);
3854
+ }
3855
+ }
3856
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
3857
+ for s in statements {
3858
+ tref_stmt(s, out);
3859
+ }
3860
+ }
3861
+ Statement::ExprStmt { expr, .. } => tref_expr(expr, out),
3862
+ Statement::If {
3863
+ cond,
3864
+ then_branch,
3865
+ else_branch,
3866
+ ..
3867
+ } => {
3868
+ tref_expr(cond, out);
3869
+ tref_stmt(then_branch, out);
3870
+ if let Some(e) = else_branch {
3871
+ tref_stmt(e, out);
3872
+ }
3873
+ }
3874
+ Statement::While { cond, body, .. } => {
3875
+ tref_expr(cond, out);
3876
+ tref_stmt(body, out);
3877
+ }
3878
+ Statement::For {
3879
+ init,
3880
+ cond,
3881
+ update,
3882
+ body,
3883
+ ..
3884
+ } => {
3885
+ if let Some(i) = init {
3886
+ tref_stmt(i, out);
3887
+ }
3888
+ if let Some(c) = cond {
3889
+ tref_expr(c, out);
3890
+ }
3891
+ if let Some(u) = update {
3892
+ tref_expr(u, out);
3893
+ }
3894
+ tref_stmt(body, out);
3895
+ }
3896
+ Statement::ForOf { iterable, body, .. } => {
3897
+ tref_expr(iterable, out);
3898
+ tref_stmt(body, out);
3899
+ }
3900
+ Statement::DoWhile { body, cond, .. } => {
3901
+ tref_stmt(body, out);
3902
+ tref_expr(cond, out);
3903
+ }
3904
+ Statement::Return { value, .. } => {
3905
+ if let Some(e) = value {
3906
+ tref_expr(e, out);
3907
+ }
3908
+ }
3909
+ Statement::Throw { value, .. } => tref_expr(value, out),
3910
+ Statement::Switch {
3911
+ expr,
3912
+ cases,
3913
+ default_body,
3914
+ ..
3915
+ } => {
3916
+ tref_expr(expr, out);
3917
+ for (c, body) in cases {
3918
+ if let Some(c) = c {
3919
+ tref_expr(c, out);
3920
+ }
3921
+ for s in body {
3922
+ tref_stmt(s, out);
3923
+ }
3924
+ }
3925
+ if let Some(body) = default_body {
3926
+ for s in body {
3927
+ tref_stmt(s, out);
3928
+ }
3929
+ }
3930
+ }
3931
+ Statement::Try {
3932
+ body,
3933
+ catch_body,
3934
+ finally_body,
3935
+ ..
3936
+ } => {
3937
+ tref_stmt(body, out);
3938
+ if let Some(c) = catch_body {
3939
+ tref_stmt(c, out);
3940
+ }
3941
+ if let Some(f) = finally_body {
3942
+ tref_stmt(f, out);
3943
+ }
3944
+ }
3945
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
3946
+ ExportDeclaration::Named(inner) => tref_stmt(inner, out),
3947
+ ExportDeclaration::Default(e) => tref_expr(e, out),
3948
+ },
3949
+ _ => {}
3950
+ }
3951
+ }
3952
+
3953
+ /// If the cursor sits on a type-alias declaration name or on a `: T` type-reference use, returns
3954
+ /// every source span that must be renamed together (the declaration plus all annotation uses).
3955
+ /// Returns `None` when the cursor is not on a user-declared type alias, so callers fall back to
3956
+ /// value-binding rename. Fixes the silent breakage where renaming a `type T` left every `: T`
3957
+ /// annotation stale (type names are in a namespace the value resolver does not see).
3958
+ pub fn type_alias_rename_spans(
3959
+ program: &Program,
3960
+ source: &str,
3961
+ lsp_line: u32,
3962
+ lsp_character: u32,
3963
+ ) -> Option<Vec<tishlang_ast::Span>> {
3964
+ let mut occ: Vec<TypeOcc> = Vec::new();
3965
+ for s in &program.statements {
3966
+ tref_stmt(s, &mut occ);
3967
+ }
3968
+ // The type name whose declaration or use span contains the cursor.
3969
+ let target = occ
3970
+ .iter()
3971
+ .find(|o| pos::span_contains_lsp_position(source, &o.span, lsp_line, lsp_character))?
3972
+ .name
3973
+ .clone();
3974
+ // Only rename user-declared aliases — never a builtin (`number`, `string`, …) used in an
3975
+ // annotation, which has no declaration to keep in sync.
3976
+ if !occ.iter().any(|o| o.is_decl && o.name == target) {
3977
+ return None;
3978
+ }
3979
+ let mut spans: Vec<tishlang_ast::Span> = occ
3980
+ .iter()
3981
+ .filter(|o| o.name == target)
3982
+ .map(|o| o.span)
3983
+ .collect();
3984
+ spans.sort_by_key(|s| (s.start.0, s.start.1, s.end.0, s.end.1));
3985
+ spans.dedup();
3986
+ Some(spans)
3987
+ }
3988
+
3573
3989
  pub fn reference_spans_for_def(
3574
3990
  program: &Program,
3575
3991
  source: &str,
@@ -3807,7 +4223,7 @@ fn refs_expr(
3807
4223
  Expr::Object { props, .. } => {
3808
4224
  for p in props {
3809
4225
  match p {
3810
- tishlang_ast::ObjectProp::KeyValue(_, e) => {
4226
+ tishlang_ast::ObjectProp::KeyValue(_, e, _) => {
3811
4227
  refs_expr(e, program, source, name, def_span, out)
3812
4228
  }
3813
4229
  tishlang_ast::ObjectProp::Spread(e) => {
@@ -3965,6 +4381,20 @@ mod tests {
3965
4381
  assert!(names.iter().any(|n| n.as_ref() == "bar"));
3966
4382
  }
3967
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
+
3968
4398
  #[test]
3969
4399
  fn unresolved_unknown_ident() {
3970
4400
  let src = "let x = nope\n";
@@ -4038,6 +4468,74 @@ mod tests {
4038
4468
  assert_eq!(u[0].kind, UnusedBindingKind::Import);
4039
4469
  }
4040
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
+
4041
4539
  #[test]
4042
4540
  fn reference_spans_include_uses_in_exported_fn_body() {
4043
4541
  // Directly exercises the fixed walker (also backs find-references / rename).
@@ -4329,4 +4827,56 @@ mod tests {
4329
4827
  assert_eq!(ch.members[0].as_ref(), "b");
4330
4828
  assert_eq!(ch.members[1].as_ref(), "c");
4331
4829
  }
4830
+
4831
+ #[test]
4832
+ fn type_alias_rename_collects_decl_and_all_annotation_uses() {
4833
+ // #131: renaming a type alias must edit the declaration AND every `: T` annotation, or the
4834
+ // program silently breaks. Type names are in a namespace the value resolver doesn't see.
4835
+ let src = "type T = number\nlet a: T = 1\nfn f(x: T): T { return x }\n";
4836
+ let program = parse(src).expect("parse");
4837
+ fn span_text(src: &str, sp: &tishlang_ast::Span) -> String {
4838
+ let lines: Vec<&str> = src.lines().collect();
4839
+ let (sl, sc) = sp.start;
4840
+ let (_el, ec) = sp.end;
4841
+ lines[sl - 1].chars().skip(sc - 1).take(ec - sc).collect()
4842
+ }
4843
+ // Cursor on the declaration name `T` (0-indexed line 0, char 5).
4844
+ let spans = type_alias_rename_spans(&program, src, 0, 5).expect("type rename spans");
4845
+ assert_eq!(spans.len(), 4, "decl + let-ann + param-ann + return-ann; spans={spans:?}");
4846
+ for sp in &spans {
4847
+ assert_eq!(span_text(src, sp), "T", "every rename span covers the `T` token; {sp:?}");
4848
+ }
4849
+ // Initiating from a `: T` use also works (the param annotation, line 2 char 8).
4850
+ let from_use = type_alias_rename_spans(&program, src, 2, 8).expect("rename from use");
4851
+ assert_eq!(from_use.len(), 4, "same set whether initiated from decl or use");
4852
+ // A builtin used in an annotation (`number`, line 0 char 9) is not a user alias -> None.
4853
+ assert!(
4854
+ type_alias_rename_spans(&program, src, 0, 9).is_none(),
4855
+ "builtin `number` has no declaration to rename"
4856
+ );
4857
+ }
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
+ }
4332
4882
  }