@tishlang/tish 2.2.0 → 2.2.4
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/tish/Cargo.toml +7 -3
- package/crates/tish/src/main.rs +26 -8
- package/crates/tish_ast/src/ast.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +0 -4
- package/crates/tish_build_utils/src/lib.rs +0 -41
- package/crates/tish_bytecode/Cargo.toml +0 -1
- package/crates/tish_compile/src/codegen.rs +21 -0
- package/crates/tish_cranelift/Cargo.toml +0 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +0 -1
- package/crates/tish_eval/Cargo.toml +1 -6
- package/crates/tish_fmt/src/lib.rs +156 -75
- package/crates/tish_lsp/src/import_goto.rs +25 -3
- package/crates/tish_lsp/src/main.rs +250 -5
- package/crates/tish_parser/src/parser.rs +20 -8
- package/crates/tish_resolve/src/lib.rs +775 -55
- package/crates/tish_runtime/Cargo.toml +0 -4
- package/crates/tishlang_cargo_bindgen/Cargo.toml +7 -0
- 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
|
@@ -72,6 +72,35 @@ fn synthetic_name_span(start: (usize, usize), name: &str) -> tishlang_ast::Span
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
thread_local! {
|
|
76
|
+
/// When active, accumulates the definition spans that identifier uses resolve to during one
|
|
77
|
+
/// `collect_unresolved_identifiers` scope-aware walk (`record_unresolved` already resolves every
|
|
78
|
+
/// use). `collect_unused_bindings` activates it to learn which declarations are referenced in a
|
|
79
|
+
/// single O(N) pass, instead of re-scanning the whole program per binding (which was ~O(N³) and
|
|
80
|
+
/// froze large files). Inactive for normal callers (the unresolved-name diagnostic), which see
|
|
81
|
+
/// no behavior change.
|
|
82
|
+
static REFERENCED_DEFS: std::cell::RefCell<Option<std::collections::HashSet<(usize, usize)>>> =
|
|
83
|
+
const { std::cell::RefCell::new(None) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// One scope-aware resolution pass: the set of definition span *starts* that at least one identifier
|
|
87
|
+
/// use resolves to (a name's declaration position is unique, so the start identifies it). Reuses the
|
|
88
|
+
/// exact walk and scope rules of [`collect_unresolved_identifiers`], so it stays consistent with the
|
|
89
|
+
/// unresolved-name diagnostic. O(N) (single walk; scope lookups O(1)).
|
|
90
|
+
fn collect_referenced_def_spans(program: &Program) -> std::collections::HashSet<(usize, usize)> {
|
|
91
|
+
struct Guard;
|
|
92
|
+
impl Drop for Guard {
|
|
93
|
+
fn drop(&mut self) {
|
|
94
|
+
REFERENCED_DEFS.with(|r| *r.borrow_mut() = None);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let _g = Guard;
|
|
98
|
+
REFERENCED_DEFS.with(|r| *r.borrow_mut() = Some(std::collections::HashSet::new()));
|
|
99
|
+
// Side effect: record_unresolved inserts each resolved def span into REFERENCED_DEFS.
|
|
100
|
+
let _ = collect_unresolved_identifiers(program);
|
|
101
|
+
REFERENCED_DEFS.with(|r| r.borrow_mut().take().unwrap_or_default())
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
fn collect_stmt(
|
|
76
105
|
stmt: &Statement,
|
|
77
106
|
source: &str,
|
|
@@ -160,6 +189,7 @@ fn collect_stmt(
|
|
|
160
189
|
name,
|
|
161
190
|
name_span,
|
|
162
191
|
params,
|
|
192
|
+
rest_param,
|
|
163
193
|
body,
|
|
164
194
|
..
|
|
165
195
|
} => {
|
|
@@ -167,6 +197,9 @@ fn collect_stmt(
|
|
|
167
197
|
for p in params {
|
|
168
198
|
collect_fun_param(p, source, lsp_line, lsp_char, best);
|
|
169
199
|
}
|
|
200
|
+
if let Some(rp) = rest_param {
|
|
201
|
+
consider(source, lsp_line, lsp_char, &rp.name_span, rp.name.clone(), best);
|
|
202
|
+
}
|
|
170
203
|
collect_stmt(body, source, lsp_line, lsp_char, best);
|
|
171
204
|
}
|
|
172
205
|
Statement::Switch {
|
|
@@ -256,12 +289,16 @@ fn collect_stmt(
|
|
|
256
289
|
name,
|
|
257
290
|
name_span,
|
|
258
291
|
params,
|
|
292
|
+
rest_param,
|
|
259
293
|
..
|
|
260
294
|
} => {
|
|
261
295
|
consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
|
|
262
296
|
for p in params {
|
|
263
297
|
collect_fun_param(p, source, lsp_line, lsp_char, best);
|
|
264
298
|
}
|
|
299
|
+
if let Some(rp) = rest_param {
|
|
300
|
+
consider(source, lsp_line, lsp_char, &rp.name_span, rp.name.clone(), best);
|
|
301
|
+
}
|
|
265
302
|
}
|
|
266
303
|
Statement::Break { .. } | Statement::Continue { .. } => {}
|
|
267
304
|
}
|
|
@@ -484,8 +521,21 @@ fn collect_expr(
|
|
|
484
521
|
}
|
|
485
522
|
Expr::Await { operand, .. } => collect_expr(operand, source, lsp_line, lsp_char, best),
|
|
486
523
|
Expr::JsxElement {
|
|
487
|
-
|
|
524
|
+
tag,
|
|
525
|
+
tag_span,
|
|
526
|
+
close_tag_span,
|
|
527
|
+
props,
|
|
528
|
+
children,
|
|
529
|
+
..
|
|
488
530
|
} => {
|
|
531
|
+
// A PascalCase tag references a component binding (`<Foo/>` lowers to `h(Foo, …)`); let
|
|
532
|
+
// the cursor land on it (opening or closing tag) for hover / go-to-def / rename.
|
|
533
|
+
if tag.chars().next().is_some_and(|c| c.is_uppercase()) {
|
|
534
|
+
consider(source, lsp_line, lsp_char, tag_span, tag.clone(), best);
|
|
535
|
+
if let Some(cs) = close_tag_span {
|
|
536
|
+
consider(source, lsp_line, lsp_char, cs, tag.clone(), best);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
489
539
|
for p in props {
|
|
490
540
|
match p {
|
|
491
541
|
tishlang_ast::JsxProp::Attr { value, .. } => if let tishlang_ast::JsxAttrValue::Expr(e) = value {
|
|
@@ -1021,6 +1071,48 @@ fn define_pattern_stack(pattern: &DestructPattern, scopes: &mut ScopeStack) {
|
|
|
1021
1071
|
}
|
|
1022
1072
|
}
|
|
1023
1073
|
|
|
1074
|
+
/// Pre-define hoisted function-declaration names (incl. `export fn` and ambient `declare fn`) for a
|
|
1075
|
+
/// statement list, so a reference to a function declared later in the same scope resolves. Function
|
|
1076
|
+
/// values capture the live scope, so calling a sibling function declared further down succeeds at
|
|
1077
|
+
/// runtime (e.g. mutual recursion, or a helper defined below its caller); the resolver mirrors that
|
|
1078
|
+
/// by defining the names before walking any sibling body. Only function names are hoisted — `let`
|
|
1079
|
+
/// is not, matching the interpreter.
|
|
1080
|
+
fn hoist_fn_names(stmts: &[Statement], scopes: &mut ScopeStack) {
|
|
1081
|
+
for s in stmts {
|
|
1082
|
+
match s {
|
|
1083
|
+
Statement::FunDecl {
|
|
1084
|
+
name, name_span, ..
|
|
1085
|
+
}
|
|
1086
|
+
| Statement::DeclareFun {
|
|
1087
|
+
name, name_span, ..
|
|
1088
|
+
} => {
|
|
1089
|
+
scopes.define(name.as_ref(), *name_span);
|
|
1090
|
+
}
|
|
1091
|
+
Statement::Export { declaration, .. } => {
|
|
1092
|
+
if let ExportDeclaration::Named(inner) = declaration.as_ref() {
|
|
1093
|
+
if let Statement::FunDecl {
|
|
1094
|
+
name, name_span, ..
|
|
1095
|
+
} = inner.as_ref()
|
|
1096
|
+
{
|
|
1097
|
+
scopes.define(name.as_ref(), *name_span);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
Statement::Multi { statements, .. } => hoist_fn_names(statements, scopes),
|
|
1102
|
+
_ => {}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/// The default-value expression of a parameter, if any (`fn f(a = EXPR)`). Both simple and
|
|
1108
|
+
/// destructuring parameters can carry a default; rest parameters never do (no default syntax).
|
|
1109
|
+
fn param_default(p: &FunParam) -> Option<&Expr> {
|
|
1110
|
+
match p {
|
|
1111
|
+
FunParam::Simple(tp) => tp.default.as_ref(),
|
|
1112
|
+
FunParam::Destructure { default, .. } => default.as_ref(),
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1024
1116
|
fn walk_expr_resolve(
|
|
1025
1117
|
expr: &Expr,
|
|
1026
1118
|
scopes: &ScopeStack,
|
|
@@ -1148,6 +1240,13 @@ fn walk_expr_resolve(
|
|
|
1148
1240
|
for p in params {
|
|
1149
1241
|
define_fun_param_stack(p, &mut inner);
|
|
1150
1242
|
}
|
|
1243
|
+
for p in params {
|
|
1244
|
+
if let Some(e) = param_default(p) {
|
|
1245
|
+
if let Some(s) = walk_expr_resolve(e, &inner, target) {
|
|
1246
|
+
return Some(s);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1151
1250
|
match body {
|
|
1152
1251
|
ArrowBody::Expr(e) => walk_expr_resolve(e, &inner, target),
|
|
1153
1252
|
ArrowBody::Block(b) => walk_stmt_resolve(b, &mut inner, target),
|
|
@@ -1163,8 +1262,20 @@ fn walk_expr_resolve(
|
|
|
1163
1262
|
}
|
|
1164
1263
|
Expr::Await { operand, .. } => walk_expr_resolve(operand, scopes, target),
|
|
1165
1264
|
Expr::JsxElement {
|
|
1166
|
-
|
|
1265
|
+
tag,
|
|
1266
|
+
tag_span,
|
|
1267
|
+
close_tag_span,
|
|
1268
|
+
props,
|
|
1269
|
+
children,
|
|
1270
|
+
..
|
|
1167
1271
|
} => {
|
|
1272
|
+
// Go-to-def on a PascalCase component tag (opening or closing) resolves to its binding.
|
|
1273
|
+
if tag.chars().next().is_some_and(|c| c.is_uppercase())
|
|
1274
|
+
&& (*tag_span == tgt || *close_tag_span == Some(tgt))
|
|
1275
|
+
&& tag.as_ref() == target.name.as_ref()
|
|
1276
|
+
{
|
|
1277
|
+
return scopes.resolve(tag.as_ref());
|
|
1278
|
+
}
|
|
1168
1279
|
for p in props {
|
|
1169
1280
|
match p {
|
|
1170
1281
|
tishlang_ast::JsxProp::Attr { value, .. } => {
|
|
@@ -1307,6 +1418,7 @@ fn walk_stmt_resolve(
|
|
|
1307
1418
|
.and_then(|e| walk_expr_resolve(e, scopes, target)),
|
|
1308
1419
|
Statement::Block { statements, .. } => {
|
|
1309
1420
|
scopes.push();
|
|
1421
|
+
hoist_fn_names(statements, scopes);
|
|
1310
1422
|
let mut out = None;
|
|
1311
1423
|
for s in statements {
|
|
1312
1424
|
if let Some(x) = walk_stmt_resolve(s, scopes, target) {
|
|
@@ -1332,6 +1444,7 @@ fn walk_stmt_resolve(
|
|
|
1332
1444
|
name,
|
|
1333
1445
|
name_span,
|
|
1334
1446
|
params,
|
|
1447
|
+
rest_param,
|
|
1335
1448
|
body,
|
|
1336
1449
|
..
|
|
1337
1450
|
} => {
|
|
@@ -1343,7 +1456,21 @@ fn walk_stmt_resolve(
|
|
|
1343
1456
|
for p in params {
|
|
1344
1457
|
define_fun_param_stack(p, scopes);
|
|
1345
1458
|
}
|
|
1346
|
-
let
|
|
1459
|
+
if let Some(rp) = rest_param {
|
|
1460
|
+
scopes.define(rp.name.as_ref(), rp.name_span);
|
|
1461
|
+
}
|
|
1462
|
+
let mut r = None;
|
|
1463
|
+
for p in params {
|
|
1464
|
+
if let Some(e) = param_default(p) {
|
|
1465
|
+
if let Some(s) = walk_expr_resolve(e, scopes, target) {
|
|
1466
|
+
r = Some(s);
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if r.is_none() {
|
|
1472
|
+
r = walk_stmt_resolve(body, scopes, target);
|
|
1473
|
+
}
|
|
1347
1474
|
scopes.pop();
|
|
1348
1475
|
if r.is_some() {
|
|
1349
1476
|
return r;
|
|
@@ -1482,6 +1609,7 @@ fn walk_stmt_resolve(
|
|
|
1482
1609
|
name,
|
|
1483
1610
|
name_span,
|
|
1484
1611
|
params,
|
|
1612
|
+
rest_param,
|
|
1485
1613
|
..
|
|
1486
1614
|
} => {
|
|
1487
1615
|
if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
|
|
@@ -1492,8 +1620,20 @@ fn walk_stmt_resolve(
|
|
|
1492
1620
|
for p in params {
|
|
1493
1621
|
define_fun_param_stack(p, scopes);
|
|
1494
1622
|
}
|
|
1623
|
+
if let Some(rp) = rest_param {
|
|
1624
|
+
scopes.define(rp.name.as_ref(), rp.name_span);
|
|
1625
|
+
}
|
|
1626
|
+
let mut r = None;
|
|
1627
|
+
for p in params {
|
|
1628
|
+
if let Some(e) = param_default(p) {
|
|
1629
|
+
if let Some(s) = walk_expr_resolve(e, scopes, target) {
|
|
1630
|
+
r = Some(s);
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1495
1635
|
scopes.pop();
|
|
1496
|
-
|
|
1636
|
+
r
|
|
1497
1637
|
}
|
|
1498
1638
|
Statement::Break { .. } | Statement::Continue { .. } => None,
|
|
1499
1639
|
}
|
|
@@ -1524,6 +1664,11 @@ pub struct UnresolvedIdentifier {
|
|
|
1524
1664
|
/// Names always present on the interpreter root scope (see `tishlang_eval`) and listed in
|
|
1525
1665
|
/// `stdlib/builtins.d.tish`. They must not produce "unresolved identifier" diagnostics when
|
|
1526
1666
|
/// used without a `let`/`import`.
|
|
1667
|
+
///
|
|
1668
|
+
/// NOTE: maintained by hand to track the globals the interpreter registers on its root scope
|
|
1669
|
+
/// (`tish_eval`). It is the only suppression the LSP has — the resolver does not load
|
|
1670
|
+
/// `builtins.d.tish` — so any global missing here surfaces as a false `tish-unresolved-name`
|
|
1671
|
+
/// error in the editor. Keep it in sync (see the `runtime_globals_not_unresolved` test).
|
|
1527
1672
|
pub fn is_runtime_global_ident(name: &str) -> bool {
|
|
1528
1673
|
matches!(
|
|
1529
1674
|
name,
|
|
@@ -1533,6 +1678,7 @@ pub fn is_runtime_global_ident(name: &str) -> bool {
|
|
|
1533
1678
|
| "decodeURI"
|
|
1534
1679
|
| "encodeURI"
|
|
1535
1680
|
| "Boolean"
|
|
1681
|
+
| "Number"
|
|
1536
1682
|
| "isFinite"
|
|
1537
1683
|
| "isNaN"
|
|
1538
1684
|
| "Infinity"
|
|
@@ -1542,6 +1688,7 @@ pub fn is_runtime_global_ident(name: &str) -> bool {
|
|
|
1542
1688
|
| "Object"
|
|
1543
1689
|
| "Array"
|
|
1544
1690
|
| "String"
|
|
1691
|
+
| "Symbol"
|
|
1545
1692
|
| "Date"
|
|
1546
1693
|
| "Set"
|
|
1547
1694
|
| "Map"
|
|
@@ -1556,6 +1703,15 @@ pub fn is_runtime_global_ident(name: &str) -> bool {
|
|
|
1556
1703
|
| "Uint32Array"
|
|
1557
1704
|
| "AudioContext"
|
|
1558
1705
|
| "RegExp"
|
|
1706
|
+
| "Error"
|
|
1707
|
+
| "TypeError"
|
|
1708
|
+
| "RangeError"
|
|
1709
|
+
| "SyntaxError"
|
|
1710
|
+
| "Promise"
|
|
1711
|
+
| "fetch"
|
|
1712
|
+
| "fetchAll"
|
|
1713
|
+
| "serve"
|
|
1714
|
+
| "htmlEscape"
|
|
1559
1715
|
| "setTimeout"
|
|
1560
1716
|
| "setInterval"
|
|
1561
1717
|
| "clearTimeout"
|
|
@@ -1569,14 +1725,25 @@ fn record_unresolved(
|
|
|
1569
1725
|
span: tishlang_ast::Span,
|
|
1570
1726
|
out: &mut Vec<UnresolvedIdentifier>,
|
|
1571
1727
|
) {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1728
|
+
match scopes.resolve(name.as_ref()) {
|
|
1729
|
+
// Resolved to an in-scope declaration. Record it (when collection is active) so
|
|
1730
|
+
// collect_unused_bindings learns this def is referenced — done before the global check so a
|
|
1731
|
+
// local that shadows a global name still counts as a use. Unresolved-name output unchanged.
|
|
1732
|
+
Some(def) => {
|
|
1733
|
+
REFERENCED_DEFS.with(|r| {
|
|
1734
|
+
if let Some(set) = r.borrow_mut().as_mut() {
|
|
1735
|
+
set.insert(def.start);
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
None => {
|
|
1740
|
+
if !is_runtime_global_ident(name.as_ref()) {
|
|
1741
|
+
out.push(UnresolvedIdentifier {
|
|
1742
|
+
name: name.clone(),
|
|
1743
|
+
span,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1580
1747
|
}
|
|
1581
1748
|
}
|
|
1582
1749
|
|
|
@@ -1695,6 +1862,11 @@ fn check_unresolved_expr(expr: &Expr, scopes: &ScopeStack, out: &mut Vec<Unresol
|
|
|
1695
1862
|
for p in params {
|
|
1696
1863
|
define_fun_param_stack(p, &mut inner);
|
|
1697
1864
|
}
|
|
1865
|
+
for p in params {
|
|
1866
|
+
if let Some(e) = param_default(p) {
|
|
1867
|
+
check_unresolved_expr(e, &inner, out);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1698
1870
|
match body {
|
|
1699
1871
|
ArrowBody::Expr(e) => check_unresolved_expr(e, &inner, out),
|
|
1700
1872
|
ArrowBody::Block(b) => check_unresolved_stmt(b, &mut inner, out),
|
|
@@ -1828,6 +2000,7 @@ fn check_unresolved_stmt(
|
|
|
1828
2000
|
}
|
|
1829
2001
|
Statement::Block { statements, .. } => {
|
|
1830
2002
|
scopes.push();
|
|
2003
|
+
hoist_fn_names(statements, scopes);
|
|
1831
2004
|
for s in statements {
|
|
1832
2005
|
check_unresolved_stmt(s, scopes, out);
|
|
1833
2006
|
}
|
|
@@ -1843,6 +2016,7 @@ fn check_unresolved_stmt(
|
|
|
1843
2016
|
name,
|
|
1844
2017
|
name_span,
|
|
1845
2018
|
params,
|
|
2019
|
+
rest_param,
|
|
1846
2020
|
body,
|
|
1847
2021
|
..
|
|
1848
2022
|
} => {
|
|
@@ -1851,6 +2025,14 @@ fn check_unresolved_stmt(
|
|
|
1851
2025
|
for p in params {
|
|
1852
2026
|
define_fun_param_stack(p, scopes);
|
|
1853
2027
|
}
|
|
2028
|
+
if let Some(rp) = rest_param {
|
|
2029
|
+
scopes.define(rp.name.as_ref(), rp.name_span);
|
|
2030
|
+
}
|
|
2031
|
+
for p in params {
|
|
2032
|
+
if let Some(e) = param_default(p) {
|
|
2033
|
+
check_unresolved_expr(e, scopes, out);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1854
2036
|
check_unresolved_stmt(body, scopes, out);
|
|
1855
2037
|
scopes.pop();
|
|
1856
2038
|
scopes.define(name.as_ref(), *name_span);
|
|
@@ -1944,6 +2126,7 @@ fn check_unresolved_stmt(
|
|
|
1944
2126
|
name,
|
|
1945
2127
|
name_span,
|
|
1946
2128
|
params,
|
|
2129
|
+
rest_param,
|
|
1947
2130
|
..
|
|
1948
2131
|
} => {
|
|
1949
2132
|
scopes.push();
|
|
@@ -1951,6 +2134,14 @@ fn check_unresolved_stmt(
|
|
|
1951
2134
|
for p in params {
|
|
1952
2135
|
define_fun_param_stack(p, scopes);
|
|
1953
2136
|
}
|
|
2137
|
+
if let Some(rp) = rest_param {
|
|
2138
|
+
scopes.define(rp.name.as_ref(), rp.name_span);
|
|
2139
|
+
}
|
|
2140
|
+
for p in params {
|
|
2141
|
+
if let Some(e) = param_default(p) {
|
|
2142
|
+
check_unresolved_expr(e, scopes, out);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1954
2145
|
scopes.pop();
|
|
1955
2146
|
scopes.define(name.as_ref(), *name_span);
|
|
1956
2147
|
}
|
|
@@ -1962,6 +2153,7 @@ fn check_unresolved_stmt(
|
|
|
1962
2153
|
pub fn collect_unresolved_identifiers(program: &Program) -> Vec<UnresolvedIdentifier> {
|
|
1963
2154
|
let mut out = Vec::new();
|
|
1964
2155
|
let mut scopes = ScopeStack::new();
|
|
2156
|
+
hoist_fn_names(&program.statements, &mut scopes);
|
|
1965
2157
|
for stmt in &program.statements {
|
|
1966
2158
|
check_unresolved_stmt(stmt, &mut scopes, &mut out);
|
|
1967
2159
|
}
|
|
@@ -2424,59 +2616,291 @@ fn enumerate_stmt(stmt: &Statement, exported: bool, out: &mut Vec<BindingSite>)
|
|
|
2424
2616
|
exported,
|
|
2425
2617
|
});
|
|
2426
2618
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2619
|
+
// Ambient `declare` declarations describe symbols defined elsewhere and have no body, so
|
|
2620
|
+
// nothing here is ever "unused" within this file (params can't be read; the name is API
|
|
2621
|
+
// surface for other modules). They contribute no unused-binding candidates.
|
|
2622
|
+
Statement::DeclareVar { .. } | Statement::DeclareFun { .. } => {}
|
|
2623
|
+
Statement::Break { .. } | Statement::Continue { .. } => {}
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
/// Collect PascalCase JSX component tag names used anywhere in `program`. A `<Foo/>` tag lowers to
|
|
2628
|
+
/// `h(Foo, …)` — a real reference to the `Foo` binding — but the tag is not a span-validated
|
|
2629
|
+
/// identifier use, so the reference walker misses it. Used by [`collect_unused_bindings`] to avoid
|
|
2630
|
+
/// flagging a component import/binding used only as a tag. (Lowercase tags lower to string
|
|
2631
|
+
/// literals, so they are not references.) Precise rename/find-references for tags still needs a
|
|
2632
|
+
/// real tag span in the AST and is handled separately.
|
|
2633
|
+
fn collect_jsx_component_tags(program: &Program) -> std::collections::HashSet<Arc<str>> {
|
|
2634
|
+
let mut out = std::collections::HashSet::new();
|
|
2635
|
+
for s in &program.statements {
|
|
2636
|
+
jsx_tags_stmt(s, &mut out);
|
|
2637
|
+
}
|
|
2638
|
+
out
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
fn jsx_tags_stmt(s: &Statement, out: &mut std::collections::HashSet<Arc<str>>) {
|
|
2642
|
+
match s {
|
|
2643
|
+
Statement::VarDecl { init, .. } => {
|
|
2644
|
+
if let Some(e) = init {
|
|
2645
|
+
jsx_tags_expr(e, out);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
Statement::VarDeclDestructure { init, .. } => jsx_tags_expr(init, out),
|
|
2649
|
+
Statement::ExprStmt { expr, .. } => jsx_tags_expr(expr, out),
|
|
2650
|
+
Statement::If {
|
|
2651
|
+
cond,
|
|
2652
|
+
then_branch,
|
|
2653
|
+
else_branch,
|
|
2654
|
+
..
|
|
2429
2655
|
} => {
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
});
|
|
2656
|
+
jsx_tags_expr(cond, out);
|
|
2657
|
+
jsx_tags_stmt(then_branch, out);
|
|
2658
|
+
if let Some(b) = else_branch {
|
|
2659
|
+
jsx_tags_stmt(b, out);
|
|
2660
|
+
}
|
|
2436
2661
|
}
|
|
2437
|
-
Statement::
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2662
|
+
Statement::While { cond, body, .. } => {
|
|
2663
|
+
jsx_tags_expr(cond, out);
|
|
2664
|
+
jsx_tags_stmt(body, out);
|
|
2665
|
+
}
|
|
2666
|
+
Statement::For {
|
|
2667
|
+
init,
|
|
2668
|
+
cond,
|
|
2669
|
+
update,
|
|
2670
|
+
body,
|
|
2442
2671
|
..
|
|
2443
2672
|
} => {
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
}
|
|
2673
|
+
if let Some(i) = init {
|
|
2674
|
+
jsx_tags_stmt(i, out);
|
|
2675
|
+
}
|
|
2676
|
+
if let Some(e) = cond {
|
|
2677
|
+
jsx_tags_expr(e, out);
|
|
2678
|
+
}
|
|
2679
|
+
if let Some(e) = update {
|
|
2680
|
+
jsx_tags_expr(e, out);
|
|
2681
|
+
}
|
|
2682
|
+
jsx_tags_stmt(body, out);
|
|
2683
|
+
}
|
|
2684
|
+
Statement::ForOf { iterable, body, .. } => {
|
|
2685
|
+
jsx_tags_expr(iterable, out);
|
|
2686
|
+
jsx_tags_stmt(body, out);
|
|
2687
|
+
}
|
|
2688
|
+
Statement::Return { value, .. } => {
|
|
2689
|
+
if let Some(e) = value {
|
|
2690
|
+
jsx_tags_expr(e, out);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
2694
|
+
for s in statements {
|
|
2695
|
+
jsx_tags_stmt(s, out);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
Statement::FunDecl { params, body, .. } => {
|
|
2450
2699
|
for p in params {
|
|
2451
|
-
|
|
2700
|
+
if let Some(e) = param_default(p) {
|
|
2701
|
+
jsx_tags_expr(e, out);
|
|
2702
|
+
}
|
|
2452
2703
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2704
|
+
jsx_tags_stmt(body, out);
|
|
2705
|
+
}
|
|
2706
|
+
Statement::Switch {
|
|
2707
|
+
expr,
|
|
2708
|
+
cases,
|
|
2709
|
+
default_body,
|
|
2710
|
+
..
|
|
2711
|
+
} => {
|
|
2712
|
+
jsx_tags_expr(expr, out);
|
|
2713
|
+
for (_, stmts) in cases {
|
|
2714
|
+
for s in stmts {
|
|
2715
|
+
jsx_tags_stmt(s, out);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if let Some(stmts) = default_body {
|
|
2719
|
+
for s in stmts {
|
|
2720
|
+
jsx_tags_stmt(s, out);
|
|
2721
|
+
}
|
|
2460
2722
|
}
|
|
2461
2723
|
}
|
|
2462
|
-
Statement::
|
|
2724
|
+
Statement::DoWhile { body, cond, .. } => {
|
|
2725
|
+
jsx_tags_stmt(body, out);
|
|
2726
|
+
jsx_tags_expr(cond, out);
|
|
2727
|
+
}
|
|
2728
|
+
Statement::Throw { value, .. } => jsx_tags_expr(value, out),
|
|
2729
|
+
Statement::Try {
|
|
2730
|
+
body,
|
|
2731
|
+
catch_body,
|
|
2732
|
+
finally_body,
|
|
2733
|
+
..
|
|
2734
|
+
} => {
|
|
2735
|
+
jsx_tags_stmt(body, out);
|
|
2736
|
+
if let Some(cb) = catch_body {
|
|
2737
|
+
jsx_tags_stmt(cb, out);
|
|
2738
|
+
}
|
|
2739
|
+
if let Some(fb) = finally_body {
|
|
2740
|
+
jsx_tags_stmt(fb, out);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
2744
|
+
ExportDeclaration::Named(inner) => jsx_tags_stmt(inner, out),
|
|
2745
|
+
ExportDeclaration::Default(e) => jsx_tags_expr(e, out),
|
|
2746
|
+
},
|
|
2747
|
+
Statement::Import { .. }
|
|
2748
|
+
| Statement::Break { .. }
|
|
2749
|
+
| Statement::Continue { .. }
|
|
2750
|
+
| Statement::TypeAlias { .. }
|
|
2751
|
+
| Statement::DeclareVar { .. }
|
|
2752
|
+
| Statement::DeclareFun { .. } => {}
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
fn jsx_tags_expr(e: &Expr, out: &mut std::collections::HashSet<Arc<str>>) {
|
|
2757
|
+
match e {
|
|
2758
|
+
Expr::JsxElement {
|
|
2759
|
+
tag,
|
|
2760
|
+
props,
|
|
2761
|
+
children,
|
|
2762
|
+
..
|
|
2763
|
+
} => {
|
|
2764
|
+
if tag.chars().next().is_some_and(|c| c.is_uppercase()) {
|
|
2765
|
+
out.insert(tag.clone());
|
|
2766
|
+
}
|
|
2767
|
+
for p in props {
|
|
2768
|
+
match p {
|
|
2769
|
+
tishlang_ast::JsxProp::Attr { value, .. } => {
|
|
2770
|
+
if let tishlang_ast::JsxAttrValue::Expr(e) = value {
|
|
2771
|
+
jsx_tags_expr(e, out);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
tishlang_ast::JsxProp::Spread(e) => jsx_tags_expr(e, out),
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
for ch in children {
|
|
2778
|
+
if let tishlang_ast::JsxChild::Expr(e) = ch {
|
|
2779
|
+
jsx_tags_expr(e, out);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
Expr::JsxFragment { children, .. } => {
|
|
2784
|
+
for ch in children {
|
|
2785
|
+
if let tishlang_ast::JsxChild::Expr(e) = ch {
|
|
2786
|
+
jsx_tags_expr(e, out);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
|
|
2791
|
+
jsx_tags_expr(left, out);
|
|
2792
|
+
jsx_tags_expr(right, out);
|
|
2793
|
+
}
|
|
2794
|
+
Expr::Unary { operand, .. }
|
|
2795
|
+
| Expr::TypeOf { operand, .. }
|
|
2796
|
+
| Expr::Await { operand, .. } => jsx_tags_expr(operand, out),
|
|
2797
|
+
Expr::Delete { target, .. } => jsx_tags_expr(target, out),
|
|
2798
|
+
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
2799
|
+
jsx_tags_expr(callee, out);
|
|
2800
|
+
for a in args {
|
|
2801
|
+
match a {
|
|
2802
|
+
CallArg::Expr(e) | CallArg::Spread(e) => jsx_tags_expr(e, out),
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
Expr::Member { object, .. } => jsx_tags_expr(object, out),
|
|
2807
|
+
Expr::Index { object, index, .. } => {
|
|
2808
|
+
jsx_tags_expr(object, out);
|
|
2809
|
+
jsx_tags_expr(index, out);
|
|
2810
|
+
}
|
|
2811
|
+
Expr::Conditional {
|
|
2812
|
+
cond,
|
|
2813
|
+
then_branch,
|
|
2814
|
+
else_branch,
|
|
2815
|
+
..
|
|
2816
|
+
} => {
|
|
2817
|
+
jsx_tags_expr(cond, out);
|
|
2818
|
+
jsx_tags_expr(then_branch, out);
|
|
2819
|
+
jsx_tags_expr(else_branch, out);
|
|
2820
|
+
}
|
|
2821
|
+
Expr::Array { elements, .. } => {
|
|
2822
|
+
for el in elements {
|
|
2823
|
+
match el {
|
|
2824
|
+
tishlang_ast::ArrayElement::Expr(e)
|
|
2825
|
+
| tishlang_ast::ArrayElement::Spread(e) => jsx_tags_expr(e, out),
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
Expr::Object { props, .. } => {
|
|
2830
|
+
for p in props {
|
|
2831
|
+
match p {
|
|
2832
|
+
tishlang_ast::ObjectProp::KeyValue(_, e)
|
|
2833
|
+
| tishlang_ast::ObjectProp::Spread(e) => jsx_tags_expr(e, out),
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
Expr::Assign { value, .. }
|
|
2838
|
+
| Expr::CompoundAssign { value, .. }
|
|
2839
|
+
| Expr::LogicalAssign { value, .. } => jsx_tags_expr(value, out),
|
|
2840
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
2841
|
+
jsx_tags_expr(object, out);
|
|
2842
|
+
jsx_tags_expr(value, out);
|
|
2843
|
+
}
|
|
2844
|
+
Expr::IndexAssign {
|
|
2845
|
+
object,
|
|
2846
|
+
index,
|
|
2847
|
+
value,
|
|
2848
|
+
..
|
|
2849
|
+
} => {
|
|
2850
|
+
jsx_tags_expr(object, out);
|
|
2851
|
+
jsx_tags_expr(index, out);
|
|
2852
|
+
jsx_tags_expr(value, out);
|
|
2853
|
+
}
|
|
2854
|
+
Expr::ArrowFunction { params, body, .. } => {
|
|
2855
|
+
for p in params {
|
|
2856
|
+
if let Some(e) = param_default(p) {
|
|
2857
|
+
jsx_tags_expr(e, out);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
match body {
|
|
2861
|
+
ArrowBody::Expr(e) => jsx_tags_expr(e, out),
|
|
2862
|
+
ArrowBody::Block(b) => jsx_tags_stmt(b, out),
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
Expr::TemplateLiteral { exprs, .. } => {
|
|
2866
|
+
for e in exprs {
|
|
2867
|
+
jsx_tags_expr(e, out);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
Expr::Ident { .. }
|
|
2871
|
+
| Expr::Literal { .. }
|
|
2872
|
+
| Expr::NativeModuleLoad { .. }
|
|
2873
|
+
| Expr::PostfixInc { .. }
|
|
2874
|
+
| Expr::PostfixDec { .. }
|
|
2875
|
+
| Expr::PrefixInc { .. }
|
|
2876
|
+
| Expr::PrefixDec { .. } => {}
|
|
2463
2877
|
}
|
|
2464
2878
|
}
|
|
2465
2879
|
|
|
2466
2880
|
/// Declarations whose values are never read (imports, locals, parameters). Skips `exported` module
|
|
2467
2881
|
/// bindings and names starting with `_` (common intentional-unused convention).
|
|
2468
|
-
pub fn collect_unused_bindings(program: &Program,
|
|
2882
|
+
pub fn collect_unused_bindings(program: &Program, _source: &str) -> Vec<UnusedBinding> {
|
|
2469
2883
|
let mut sites = Vec::new();
|
|
2470
2884
|
for s in &program.statements {
|
|
2471
2885
|
enumerate_stmt(s, false, &mut sites);
|
|
2472
2886
|
}
|
|
2887
|
+
// A binding used only as a PascalCase JSX component tag (`<Foo/>`) is genuinely used — the
|
|
2888
|
+
// resolution walk can't see the tag, so consult the tag set to avoid a false "unused" report
|
|
2889
|
+
// (deleting such an import would break the build).
|
|
2890
|
+
let jsx_tags = collect_jsx_component_tags(program);
|
|
2891
|
+
// Which declarations are referenced, learned in ONE scope-aware pass. The previous approach
|
|
2892
|
+
// re-scanned the whole program per binding (re-resolving each use), which was ~O(N³) and froze
|
|
2893
|
+
// large files; this is O(N). A binding is unused iff nothing resolves to its definition span.
|
|
2894
|
+
let referenced = collect_referenced_def_spans(program);
|
|
2473
2895
|
let mut out = Vec::new();
|
|
2474
2896
|
for site in sites {
|
|
2475
2897
|
if site.exported || site.name.as_ref().starts_with('_') {
|
|
2476
2898
|
continue;
|
|
2477
2899
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2900
|
+
if jsx_tags.contains(&site.name) {
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
if !referenced.contains(&site.span.start) {
|
|
2480
2904
|
out.push(UnusedBinding {
|
|
2481
2905
|
name: site.name,
|
|
2482
2906
|
span: site.span,
|
|
@@ -2496,6 +2920,7 @@ pub fn definition_span(
|
|
|
2496
2920
|
) -> Option<tishlang_ast::Span> {
|
|
2497
2921
|
let use_site = name_at_cursor(program, source, lsp_line, lsp_character)?;
|
|
2498
2922
|
let mut scopes = ScopeStack::new();
|
|
2923
|
+
hoist_fn_names(&program.statements, &mut scopes);
|
|
2499
2924
|
for stmt in &program.statements {
|
|
2500
2925
|
if let Some(s) = walk_stmt_resolve(stmt, &mut scopes, &use_site) {
|
|
2501
2926
|
return Some(s);
|
|
@@ -3233,19 +3658,20 @@ fn refs_stmt(
|
|
|
3233
3658
|
}
|
|
3234
3659
|
Statement::FunDecl { params, body, .. } => {
|
|
3235
3660
|
for p in params {
|
|
3236
|
-
if let
|
|
3237
|
-
if let Some(e) = &tp.default {
|
|
3238
|
-
refs_expr(e, program, source, name, def_span, out);
|
|
3239
|
-
}
|
|
3240
|
-
} else if let FunParam::Destructure {
|
|
3241
|
-
default: Some(e), ..
|
|
3242
|
-
} = p
|
|
3243
|
-
{
|
|
3661
|
+
if let Some(e) = param_default(p) {
|
|
3244
3662
|
refs_expr(e, program, source, name, def_span, out);
|
|
3245
3663
|
}
|
|
3246
3664
|
}
|
|
3247
3665
|
refs_stmt(body, program, source, name, def_span, out);
|
|
3248
3666
|
}
|
|
3667
|
+
// Ambient `declare fn` params can carry value-level defaults too.
|
|
3668
|
+
Statement::DeclareFun { params, .. } => {
|
|
3669
|
+
for p in params {
|
|
3670
|
+
if let Some(e) = param_default(p) {
|
|
3671
|
+
refs_expr(e, program, source, name, def_span, out);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3249
3675
|
Statement::Switch {
|
|
3250
3676
|
expr,
|
|
3251
3677
|
cases,
|
|
@@ -3283,13 +3709,19 @@ fn refs_stmt(
|
|
|
3283
3709
|
refs_stmt(fb, program, source, name, def_span, out);
|
|
3284
3710
|
}
|
|
3285
3711
|
}
|
|
3712
|
+
// Descend into exported declarations so references inside `export fn`/`export let`/
|
|
3713
|
+
// `export default` bodies are counted (otherwise a binding used only there looks unused).
|
|
3714
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
3715
|
+
ExportDeclaration::Named(inner) => {
|
|
3716
|
+
refs_stmt(inner, program, source, name, def_span, out)
|
|
3717
|
+
}
|
|
3718
|
+
ExportDeclaration::Default(e) => refs_expr(e, program, source, name, def_span, out),
|
|
3719
|
+
},
|
|
3286
3720
|
Statement::Import { .. }
|
|
3287
|
-
| Statement::Export { .. }
|
|
3288
3721
|
| Statement::Break { .. }
|
|
3289
3722
|
| Statement::Continue { .. }
|
|
3290
3723
|
| Statement::TypeAlias { .. }
|
|
3291
|
-
| Statement::DeclareVar { .. }
|
|
3292
|
-
| Statement::DeclareFun { .. } => {}
|
|
3724
|
+
| Statement::DeclareVar { .. } => {}
|
|
3293
3725
|
}
|
|
3294
3726
|
}
|
|
3295
3727
|
|
|
@@ -3458,8 +3890,22 @@ fn refs_expr(
|
|
|
3458
3890
|
}
|
|
3459
3891
|
Expr::Await { operand, .. } => refs_expr(operand, program, source, name, def_span, out),
|
|
3460
3892
|
Expr::JsxElement {
|
|
3461
|
-
|
|
3893
|
+
tag,
|
|
3894
|
+
tag_span,
|
|
3895
|
+
close_tag_span,
|
|
3896
|
+
props,
|
|
3897
|
+
children,
|
|
3898
|
+
..
|
|
3462
3899
|
} => {
|
|
3900
|
+
// A PascalCase tag is a reference to its component binding. Count BOTH the opening and
|
|
3901
|
+
// closing tag so find-references lists them and rename rewrites both (renaming only the
|
|
3902
|
+
// open tag would leave `<Bar></Foo>`). maybe_push validates each via definition_span.
|
|
3903
|
+
if tag.chars().next().is_some_and(|c| c.is_uppercase()) {
|
|
3904
|
+
maybe_push(tag_span, tag, out);
|
|
3905
|
+
if let Some(cs) = close_tag_span {
|
|
3906
|
+
maybe_push(cs, tag, out);
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3463
3909
|
for p in props {
|
|
3464
3910
|
match p {
|
|
3465
3911
|
tishlang_ast::JsxProp::Attr { value, .. } => {
|
|
@@ -3554,6 +4000,280 @@ mod tests {
|
|
|
3554
4000
|
assert_eq!(u[0].kind, UnusedBindingKind::Import);
|
|
3555
4001
|
}
|
|
3556
4002
|
|
|
4003
|
+
#[test]
|
|
4004
|
+
fn import_used_in_exported_fn_body_not_unused() {
|
|
4005
|
+
// Regression: a binding used only inside an `export fn` body was wrongly flagged unused
|
|
4006
|
+
// because refs_stmt did not descend into Statement::Export.
|
|
4007
|
+
let src = "import { publicUser } from \"./m\"\nexport fn toUserPublic(store, u) {\n let pub = publicUser(u)\n return pub\n}\n";
|
|
4008
|
+
let program = parse(src).expect("parse");
|
|
4009
|
+
let u = collect_unused_bindings(&program, src);
|
|
4010
|
+
assert!(u.is_empty(), "publicUser is used in the exported fn; u={u:?}");
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
#[test]
|
|
4014
|
+
fn import_used_in_export_default_not_unused() {
|
|
4015
|
+
let src = "import { p } from \"./m\"\nexport default p(1)\n";
|
|
4016
|
+
let program = parse(src).expect("parse");
|
|
4017
|
+
let u = collect_unused_bindings(&program, src);
|
|
4018
|
+
assert!(u.is_empty(), "p is used in export default; u={u:?}");
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
#[test]
|
|
4022
|
+
fn import_used_in_exported_let_init_not_unused() {
|
|
4023
|
+
let src = "import { p } from \"./m\"\nexport let x = p(1)\nx\n";
|
|
4024
|
+
let program = parse(src).expect("parse");
|
|
4025
|
+
let u = collect_unused_bindings(&program, src);
|
|
4026
|
+
assert!(u.is_empty(), "p is used in the exported let init; u={u:?}");
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
#[test]
|
|
4030
|
+
fn genuinely_unused_import_still_flagged_with_export_present() {
|
|
4031
|
+
// Guard against the fix over-counting: a truly-unused import is still reported even when an
|
|
4032
|
+
// exported declaration is present in the module.
|
|
4033
|
+
let src = "import { used, dead } from \"./m\"\nexport fn f() {\n return used()\n}\n";
|
|
4034
|
+
let program = parse(src).expect("parse");
|
|
4035
|
+
let u = collect_unused_bindings(&program, src);
|
|
4036
|
+
assert_eq!(u.len(), 1, "only `dead` is unused; u={u:?}");
|
|
4037
|
+
assert_eq!(u[0].name.as_ref(), "dead");
|
|
4038
|
+
assert_eq!(u[0].kind, UnusedBindingKind::Import);
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
#[test]
|
|
4042
|
+
fn reference_spans_include_uses_in_exported_fn_body() {
|
|
4043
|
+
// Directly exercises the fixed walker (also backs find-references / rename).
|
|
4044
|
+
let src = "import { publicUser } from \"./m\"\nexport fn f(u) {\n return publicUser(u)\n}\n";
|
|
4045
|
+
let program = parse(src).expect("parse");
|
|
4046
|
+
let def = tishlang_ast::Span {
|
|
4047
|
+
start: (1, 10),
|
|
4048
|
+
end: (1, 20),
|
|
4049
|
+
};
|
|
4050
|
+
let refs = reference_spans_for_def(&program, src, "publicUser", def);
|
|
4051
|
+
assert_eq!(refs.len(), 2, "def + one use in exported body; refs={refs:?}");
|
|
4052
|
+
assert!(refs.contains(&def));
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
#[test]
|
|
4056
|
+
fn import_used_only_in_fn_param_default_not_unused() {
|
|
4057
|
+
// A name referenced only inside a parameter default was wrongly flagged unused because the
|
|
4058
|
+
// resolve walkers never descended into `default` expressions.
|
|
4059
|
+
let src = "import { base } from \"./m\"\nexport fn f(a = base(1)) {\n return a\n}\n";
|
|
4060
|
+
let program = parse(src).expect("parse");
|
|
4061
|
+
let u = collect_unused_bindings(&program, src);
|
|
4062
|
+
assert!(u.is_empty(), "base is used in the param default; u={u:?}");
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
#[test]
|
|
4066
|
+
fn import_used_only_in_arrow_param_default_not_unused() {
|
|
4067
|
+
let src = "import { fallback } from \"./m\"\nlet make = (x = fallback) => x\nmake()\n";
|
|
4068
|
+
let program = parse(src).expect("parse");
|
|
4069
|
+
let u = collect_unused_bindings(&program, src);
|
|
4070
|
+
assert!(u.is_empty(), "fallback is used in the arrow param default; u={u:?}");
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
#[test]
|
|
4074
|
+
fn gotodef_resolves_name_used_in_param_default() {
|
|
4075
|
+
let src = "import { base } from \"./m\"\nfn f(a = base(1)) {\n return a\n}\n";
|
|
4076
|
+
let program = parse(src).expect("parse");
|
|
4077
|
+
// cursor on `base` inside the default (0-indexed line 1, char 9)
|
|
4078
|
+
let def = definition_span(&program, src, 1, 9);
|
|
4079
|
+
assert_eq!(def, Some(tishlang_ast::Span { start: (1, 10), end: (1, 14) }), "def={def:?}");
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
#[test]
|
|
4083
|
+
fn gotodef_resolves_rest_param_body_use() {
|
|
4084
|
+
let src = "fn f(...rest) {\n return rest\n}\n";
|
|
4085
|
+
let program = parse(src).expect("parse");
|
|
4086
|
+
// cursor on `rest` in the body (0-indexed line 1, char 9) -> the rest-param binding
|
|
4087
|
+
let def = definition_span(&program, src, 1, 9);
|
|
4088
|
+
assert_eq!(def, Some(tishlang_ast::Span { start: (1, 9), end: (1, 13) }), "def={def:?}");
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
#[test]
|
|
4092
|
+
fn name_at_cursor_finds_rest_param_decl() {
|
|
4093
|
+
let src = "fn f(...rest) {\n return rest\n}\n";
|
|
4094
|
+
let program = parse(src).expect("parse");
|
|
4095
|
+
// cursor on the `...rest` declaration name (0-indexed line 0, char 8)
|
|
4096
|
+
let nu = name_at_cursor(&program, src, 0, 8).expect("name under cursor");
|
|
4097
|
+
assert_eq!(nu.name.as_ref(), "rest");
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
#[test]
|
|
4101
|
+
fn param_default_referencing_sibling_param_resolves_to_param_not_outer() {
|
|
4102
|
+
// `a` exists both as an outer binding (line 0) and a param (line 1). The default `b = a`
|
|
4103
|
+
// must resolve to the PARAM, not the outer — i.e. defaults are checked with params in scope.
|
|
4104
|
+
let src = "let a = 1\nfn f(a, b = a) {\n return b\n}\nf(2)\n";
|
|
4105
|
+
let program = parse(src).expect("parse");
|
|
4106
|
+
// cursor on `a` inside `b = a` (0-indexed line 1, char 12)
|
|
4107
|
+
let def = definition_span(&program, src, 1, 12).expect("resolved");
|
|
4108
|
+
assert_eq!(def.start.0, 2, "should resolve to the param on line 2 (1-indexed); def={def:?}");
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
#[test]
|
|
4112
|
+
fn references_include_both_param_default_and_body_uses() {
|
|
4113
|
+
let src = "fn helper() { 1 }\nfn f(b = helper()) {\n return helper()\n}\nf()\n";
|
|
4114
|
+
let program = parse(src).expect("parse");
|
|
4115
|
+
let def = tishlang_ast::Span { start: (1, 4), end: (1, 10) };
|
|
4116
|
+
let refs = reference_spans_for_def(&program, src, "helper", def);
|
|
4117
|
+
// def + use in param default + use in body
|
|
4118
|
+
assert_eq!(refs.len(), 3, "decl + default-use + body-use; refs={refs:?}");
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
#[test]
|
|
4122
|
+
fn unresolved_name_in_param_default_reported() {
|
|
4123
|
+
let src = "fn f(a = mispelt) {\n return a\n}\n";
|
|
4124
|
+
let program = parse(src).expect("parse");
|
|
4125
|
+
let u = collect_unresolved_identifiers(&program);
|
|
4126
|
+
assert_eq!(u.len(), 1, "u={u:?}");
|
|
4127
|
+
assert_eq!(u[0].name.as_ref(), "mispelt");
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
#[test]
|
|
4131
|
+
fn import_used_only_in_declare_fn_default_not_unused() {
|
|
4132
|
+
let src = "import { DEF } from \"./m\"\ndeclare fn connect(port = DEF): void\nconnect()\n";
|
|
4133
|
+
let program = parse(src).expect("parse");
|
|
4134
|
+
let u = collect_unused_bindings(&program, src);
|
|
4135
|
+
// The import used in the declare-fn param default must not be reported unused.
|
|
4136
|
+
// (Unrelated pre-existing declare-fn quirks may flag `connect`/`port`; not asserted here.)
|
|
4137
|
+
assert!(
|
|
4138
|
+
!u.iter().any(|b| b.name.as_ref() == "DEF"),
|
|
4139
|
+
"DEF is used in the declare-fn param default; u={u:?}"
|
|
4140
|
+
);
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
#[test]
|
|
4144
|
+
fn forward_reference_to_later_fn_not_unresolved() {
|
|
4145
|
+
let src = "fn caller() { return helper() }\nfn helper() { return 42 }\ncaller()\n";
|
|
4146
|
+
let program = parse(src).expect("parse");
|
|
4147
|
+
let u = collect_unresolved_identifiers(&program);
|
|
4148
|
+
assert!(u.is_empty(), "helper is declared later but referenced (hoisted); u={u:?}");
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
#[test]
|
|
4152
|
+
fn mutual_recursion_not_unresolved_or_unused() {
|
|
4153
|
+
let src = "fn even(n) { if (n == 0) { return true } return odd(n - 1) }\nfn odd(n) { if (n == 0) { return false } return even(n - 1) }\neven(10)\n";
|
|
4154
|
+
let program = parse(src).expect("parse");
|
|
4155
|
+
assert!(
|
|
4156
|
+
collect_unresolved_identifiers(&program).is_empty(),
|
|
4157
|
+
"mutual recursion should resolve"
|
|
4158
|
+
);
|
|
4159
|
+
let unused = collect_unused_bindings(&program, src);
|
|
4160
|
+
assert!(unused.is_empty(), "both fns are used; unused={unused:?}");
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
#[test]
|
|
4164
|
+
fn gotodef_resolves_forward_fn_reference() {
|
|
4165
|
+
let src = "fn caller() { return helper() }\nfn helper() { return 42 }\n";
|
|
4166
|
+
let program = parse(src).expect("parse");
|
|
4167
|
+
// cursor on `helper` inside caller's body (0-indexed line 0, char 21)
|
|
4168
|
+
let def = definition_span(&program, src, 0, 21).expect("resolves");
|
|
4169
|
+
assert_eq!(def.start.0, 2, "should resolve to helper decl on line 2; def={def:?}");
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
#[test]
|
|
4173
|
+
fn genuinely_undefined_still_unresolved_with_hoisting() {
|
|
4174
|
+
let src = "fn f() { return nope() }\nf()\n";
|
|
4175
|
+
let program = parse(src).expect("parse");
|
|
4176
|
+
let u = collect_unresolved_identifiers(&program);
|
|
4177
|
+
assert_eq!(u.len(), 1, "nope is undefined; u={u:?}");
|
|
4178
|
+
assert_eq!(u[0].name.as_ref(), "nope");
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
#[test]
|
|
4182
|
+
fn declare_fn_name_and_params_not_unused() {
|
|
4183
|
+
// Ambient declare-fn: the (called) name and its params must not be flagged unused.
|
|
4184
|
+
let src = "declare fn connect(port): void\nconnect()\n";
|
|
4185
|
+
let program = parse(src).expect("parse");
|
|
4186
|
+
let u = collect_unused_bindings(&program, src);
|
|
4187
|
+
assert!(u.is_empty(), "declare-fn name + ambient param must not be unused; u={u:?}");
|
|
4188
|
+
// and the call resolves (hoisted declare-fn name)
|
|
4189
|
+
assert!(collect_unresolved_identifiers(&program).is_empty(), "connect() should resolve");
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
#[test]
|
|
4193
|
+
fn runtime_globals_not_unresolved() {
|
|
4194
|
+
// Core interpreter globals must never be flagged as unresolved-name in the editor.
|
|
4195
|
+
let src = "let _ = [Number, Symbol, Error, TypeError, RangeError, SyntaxError, Promise, fetch, fetchAll, serve, htmlEscape, console, Math, JSON, Object, Array, String, Boolean, Date, RegExp, setTimeout]\n";
|
|
4196
|
+
let program = parse(src).expect("parse");
|
|
4197
|
+
let u = collect_unresolved_identifiers(&program);
|
|
4198
|
+
assert!(u.is_empty(), "no runtime global should be unresolved; u={u:?}");
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
#[test]
|
|
4202
|
+
fn jsx_component_tag_resolves_and_is_referenced() {
|
|
4203
|
+
// `fn Foo` used as a component. go-to-def on the tag, find-references over both tags.
|
|
4204
|
+
let src = "fn Foo() {}\nlet el = <Foo></Foo>\nel\n";
|
|
4205
|
+
let program = parse(src).expect("parse");
|
|
4206
|
+
// go-to-def on the opening `<Foo` (0-idx line 1, char 10) → the `fn Foo` decl (line 2, 1-idx).
|
|
4207
|
+
let open = definition_span(&program, src, 1, 10).expect("open tag resolves");
|
|
4208
|
+
assert_eq!(open.start, (1, 4), "resolves to fn Foo; got {open:?}");
|
|
4209
|
+
// and on the closing `</Foo` (char 16).
|
|
4210
|
+
let close = definition_span(&program, src, 1, 16).expect("close tag resolves");
|
|
4211
|
+
assert_eq!(close.start, (1, 4), "close tag resolves to fn Foo; got {close:?}");
|
|
4212
|
+
// find-references: the def + the opening tag + the closing tag.
|
|
4213
|
+
let def = tishlang_ast::Span { start: (1, 4), end: (1, 7) };
|
|
4214
|
+
let refs = reference_spans_for_def(&program, src, "Foo", def);
|
|
4215
|
+
assert_eq!(refs.len(), 3, "def + open tag + close tag; refs={refs:?}");
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
#[test]
|
|
4219
|
+
fn jsx_component_import_used_as_tag_not_unused() {
|
|
4220
|
+
let src = "import { Foo } from \"./c\"\nlet el = <Foo/>\nel\n";
|
|
4221
|
+
let program = parse(src).expect("parse");
|
|
4222
|
+
let u = collect_unused_bindings(&program, src);
|
|
4223
|
+
assert!(
|
|
4224
|
+
!u.iter().any(|b| b.name.as_ref() == "Foo"),
|
|
4225
|
+
"Foo is used as a component tag; u={u:?}"
|
|
4226
|
+
);
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
#[test]
|
|
4230
|
+
fn jsx_nested_component_tag_counted() {
|
|
4231
|
+
let src = "import { Row } from \"./c\"\nlet el = <div><Row/></div>\nel\n";
|
|
4232
|
+
let program = parse(src).expect("parse");
|
|
4233
|
+
let u = collect_unused_bindings(&program, src);
|
|
4234
|
+
assert!(
|
|
4235
|
+
!u.iter().any(|b| b.name.as_ref() == "Row"),
|
|
4236
|
+
"Row is used as a nested component; u={u:?}"
|
|
4237
|
+
);
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
#[test]
|
|
4241
|
+
fn lowercase_jsx_tag_does_not_mark_binding_used() {
|
|
4242
|
+
// Lowercase tags lower to string literals, not references — a same-named binding stays unused.
|
|
4243
|
+
let src = "let div = 1\nlet el = <div></div>\nel\n";
|
|
4244
|
+
let program = parse(src).expect("parse");
|
|
4245
|
+
let u = collect_unused_bindings(&program, src);
|
|
4246
|
+
assert!(
|
|
4247
|
+
u.iter().any(|b| b.name.as_ref() == "div"),
|
|
4248
|
+
"lowercase div binding is genuinely unused; u={u:?}"
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
#[test]
|
|
4253
|
+
fn collect_unused_bindings_handles_large_files() {
|
|
4254
|
+
// A long chain of mutually-referencing functions exercises the per-binding reference scan at
|
|
4255
|
+
// scale. Under the old per-binding/per-identifier definition_span re-resolution this was
|
|
4256
|
+
// ~O(N^3) and froze (~1.4s at 300 lines); with the memo it is fast. Also a correctness check:
|
|
4257
|
+
// every function is reachable, so none is unused.
|
|
4258
|
+
let n = 300;
|
|
4259
|
+
let mut src = String::new();
|
|
4260
|
+
for i in 0..n {
|
|
4261
|
+
if i + 1 < n {
|
|
4262
|
+
src.push_str(&format!("fn f{i}() {{ return f{}() }}\n", i + 1));
|
|
4263
|
+
} else {
|
|
4264
|
+
src.push_str(&format!("fn f{i}() {{ return 0 }}\n"));
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
src.push_str("f0()\n");
|
|
4268
|
+
let program = parse(&src).expect("parse");
|
|
4269
|
+
let u = collect_unused_bindings(&program, &src);
|
|
4270
|
+
assert!(
|
|
4271
|
+
u.is_empty(),
|
|
4272
|
+
"all chained fns are reachable; unused={:?}",
|
|
4273
|
+
u.iter().map(|b| b.name.as_ref()).collect::<Vec<_>>()
|
|
4274
|
+
);
|
|
4275
|
+
}
|
|
4276
|
+
|
|
3557
4277
|
#[test]
|
|
3558
4278
|
fn unused_let_underscore_ignored() {
|
|
3559
4279
|
let src = "let _x = 1\n";
|