@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.
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +5 -1
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +12 -0
- package/crates/tish/src/main.rs +69 -11
- package/crates/tish_ast/src/ast.rs +12 -5
- package/crates/tish_build_utils/src/lib.rs +37 -0
- package/crates/tish_builtins/src/array.rs +82 -1
- package/crates/tish_builtins/src/globals.rs +50 -16
- package/crates/tish_builtins/src/math.rs +63 -9
- package/crates/tish_builtins/src/number.rs +20 -1
- package/crates/tish_builtins/src/string.rs +68 -4
- package/crates/tish_builtins/src/typedarrays.rs +23 -16
- package/crates/tish_bytecode/src/compiler.rs +94 -28
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
- package/crates/tish_compile/src/check.rs +11 -10
- package/crates/tish_compile/src/codegen.rs +1386 -42
- package/crates/tish_compile/src/infer.rs +16 -16
- package/crates/tish_compile/src/lib.rs +38 -0
- package/crates/tish_compile/src/resolve.rs +3 -2
- package/crates/tish_compile/src/types.rs +26 -9
- package/crates/tish_compile_js/src/codegen.rs +55 -4
- package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/Cargo.toml +2 -0
- package/crates/tish_core/src/json.rs +135 -34
- package/crates/tish_core/src/lib.rs +23 -1
- package/crates/tish_core/src/value.rs +64 -11
- package/crates/tish_eval/src/eval.rs +144 -197
- package/crates/tish_eval/src/natives.rs +45 -45
- package/crates/tish_eval/src/regex.rs +35 -27
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_fmt/src/lib.rs +46 -2
- package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
- package/crates/tish_lint/src/lib.rs +197 -21
- package/crates/tish_lsp/Cargo.toml +7 -1
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +913 -145
- package/crates/tish_opt/src/lib.rs +5 -3
- package/crates/tish_parser/src/lib.rs +23 -5
- package/crates/tish_parser/src/parser.rs +35 -35
- package/crates/tish_resolve/src/lib.rs +567 -17
- package/crates/tish_runtime/src/lib.rs +58 -18
- package/crates/tish_ui/src/jsx.rs +2 -2
- package/crates/tish_vm/src/jit.rs +212 -10
- package/crates/tish_vm/src/vm.rs +39 -18
- package/crates/tish_wasm/src/lib.rs +116 -22
- 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
|
@@ -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
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|