@tishlang/tish 2.2.7 → 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 +6 -2
- 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 +1 -1
- package/crates/tish_compile/src/codegen.rs +1386 -42
- package/crates/tish_compile/src/infer.rs +4 -4
- 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 +17 -0
- 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 +45 -1
- 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 +6 -0
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +856 -140
- 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 +15 -26
- package/crates/tish_resolve/src/lib.rs +188 -18
- 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
|
@@ -429,8 +429,8 @@ fn optimize_expr(expr: &Expr) -> Expr {
|
|
|
429
429
|
props: props
|
|
430
430
|
.iter()
|
|
431
431
|
.map(|p| match p {
|
|
432
|
-
tishlang_ast::ObjectProp::KeyValue(k, v) => {
|
|
433
|
-
tishlang_ast::ObjectProp::KeyValue(Arc::clone(k), optimize_expr(v))
|
|
432
|
+
tishlang_ast::ObjectProp::KeyValue(k, v, s) => {
|
|
433
|
+
tishlang_ast::ObjectProp::KeyValue(Arc::clone(k), optimize_expr(v), *s)
|
|
434
434
|
}
|
|
435
435
|
tishlang_ast::ObjectProp::Spread(e) => {
|
|
436
436
|
tishlang_ast::ObjectProp::Spread(optimize_expr(e))
|
|
@@ -578,7 +578,9 @@ fn js_number_to_string(value: f64) -> String {
|
|
|
578
578
|
return "-Infinity".to_string();
|
|
579
579
|
}
|
|
580
580
|
if value == 0.0 {
|
|
581
|
-
|
|
581
|
+
// ECMAScript `Number::toString`: both `+0` and `-0` → `"0"` (a constant-folded
|
|
582
|
+
// `"" + (-0)` must match the runtime ToString, not the inspect form). (#247)
|
|
583
|
+
return "0".to_string();
|
|
582
584
|
}
|
|
583
585
|
let negative = value < 0.0;
|
|
584
586
|
let sci = format!("{:e}", value.abs());
|
|
@@ -45,6 +45,24 @@ mod tests {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// #158: a block's span — and the enclosing function's span — must end at the closing `}`, not
|
|
49
|
+
// overrun onto the statement on the following line.
|
|
50
|
+
#[test]
|
|
51
|
+
fn block_and_fn_spans_stop_at_closing_brace() {
|
|
52
|
+
// `}` is on line 3 (1-based); `let after` begins on line 4.
|
|
53
|
+
let program = parse("fn f(a) {\n let x = a\n}\nlet after = 1\n").expect("parse");
|
|
54
|
+
let Statement::FunDecl { span, body, .. } = &program.statements[0] else {
|
|
55
|
+
panic!("expected FunDecl");
|
|
56
|
+
};
|
|
57
|
+
assert_eq!(span.end.0, 3, "fn span must end on the `}}` line, not the next statement: {span:?}");
|
|
58
|
+
assert_eq!(
|
|
59
|
+
body.span().end.0,
|
|
60
|
+
3,
|
|
61
|
+
"body block span must end on the `}}` line: {:?}",
|
|
62
|
+
body.span()
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
#[test]
|
|
49
67
|
fn test_object_literal_shorthand_single() {
|
|
50
68
|
let program = parse("const o = { port }").expect("parse object shorthand");
|
|
@@ -62,7 +80,7 @@ mod tests {
|
|
|
62
80
|
};
|
|
63
81
|
assert_eq!(props.len(), 1);
|
|
64
82
|
match &props[0] {
|
|
65
|
-
ObjectProp::KeyValue(k, v) => {
|
|
83
|
+
ObjectProp::KeyValue(k, v, _) => {
|
|
66
84
|
assert_eq!(k.as_ref(), "port");
|
|
67
85
|
if let Expr::Ident { ref name, .. } = v {
|
|
68
86
|
assert_eq!(name.as_ref(), "port");
|
|
@@ -92,11 +110,11 @@ mod tests {
|
|
|
92
110
|
};
|
|
93
111
|
assert_eq!(props.len(), 2);
|
|
94
112
|
match &props[0] {
|
|
95
|
-
ObjectProp::KeyValue(k, _) => assert_eq!(k.as_ref(), "ai-a"),
|
|
113
|
+
ObjectProp::KeyValue(k, _, _) => assert_eq!(k.as_ref(), "ai-a"),
|
|
96
114
|
_ => panic!("expected KeyValue prop"),
|
|
97
115
|
}
|
|
98
116
|
match &props[1] {
|
|
99
|
-
ObjectProp::KeyValue(k, _) => assert_eq!(k.as_ref(), "human"),
|
|
117
|
+
ObjectProp::KeyValue(k, _, _) => assert_eq!(k.as_ref(), "human"),
|
|
100
118
|
_ => panic!("expected KeyValue prop"),
|
|
101
119
|
}
|
|
102
120
|
}
|
|
@@ -118,7 +136,7 @@ mod tests {
|
|
|
118
136
|
};
|
|
119
137
|
assert_eq!(props.len(), 2);
|
|
120
138
|
match &props[0] {
|
|
121
|
-
ObjectProp::KeyValue(k, v) => {
|
|
139
|
+
ObjectProp::KeyValue(k, v, _) => {
|
|
122
140
|
assert_eq!(k.as_ref(), "port");
|
|
123
141
|
if let Expr::Ident { ref name, .. } = v {
|
|
124
142
|
assert_eq!(name.as_ref(), "port");
|
|
@@ -129,7 +147,7 @@ mod tests {
|
|
|
129
147
|
_ => panic!("expected KeyValue prop"),
|
|
130
148
|
}
|
|
131
149
|
match &props[1] {
|
|
132
|
-
ObjectProp::KeyValue(k, v) => {
|
|
150
|
+
ObjectProp::KeyValue(k, v, _) => {
|
|
133
151
|
assert_eq!(k.as_ref(), "x");
|
|
134
152
|
if let Expr::Literal { .. } = v {
|
|
135
153
|
// x: 1
|
|
@@ -364,27 +364,21 @@ impl<'a> Parser<'a> {
|
|
|
364
364
|
statements.push(self.parse_statement()?);
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
// Block ends at its CLOSING `}`/`Dedent`. Capture that token's end BEFORE advancing past it:
|
|
368
|
+
// reading `self.peek()` *after* the advance pointed at the NEXT token, overrunning the block
|
|
369
|
+
// span onto the following statement — so a cursor on the line after `}` was still "inside"
|
|
370
|
+
// the block and completion offered the body's params/locals there (#158). Fall back to the
|
|
371
|
+
// last inner statement's end for an unterminated block at EOF, else the block start.
|
|
372
|
+
let mut closer_end: Option<(usize, usize)> = None;
|
|
367
373
|
if matches!(
|
|
368
374
|
self.peek_kind(),
|
|
369
375
|
Some(TokenKind::RBrace | TokenKind::Dedent)
|
|
370
376
|
) {
|
|
377
|
+
closer_end = self.peek().map(|t| t.span.end);
|
|
371
378
|
self.advance();
|
|
372
379
|
}
|
|
373
|
-
|
|
374
|
-
let peek_end = self.peek().map(|x| x.span.end);
|
|
375
380
|
let last_end = statements.last().map(|s| s.span().end);
|
|
376
|
-
let end =
|
|
377
|
-
(Some(p), Some(l)) => {
|
|
378
|
-
if p.0 > l.0 || (p.0 == l.0 && p.1 > l.1) {
|
|
379
|
-
p
|
|
380
|
-
} else {
|
|
381
|
-
l
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
(Some(p), None) => p,
|
|
385
|
-
(None, Some(l)) => l,
|
|
386
|
-
(None, None) => span_start,
|
|
387
|
-
};
|
|
381
|
+
let end = closer_end.or(last_end).unwrap_or(span_start);
|
|
388
382
|
|
|
389
383
|
Ok(Statement::Block {
|
|
390
384
|
statements,
|
|
@@ -985,17 +979,12 @@ impl<'a> Parser<'a> {
|
|
|
985
979
|
Box::new(self.parse_block()?)
|
|
986
980
|
};
|
|
987
981
|
|
|
988
|
-
//
|
|
989
|
-
// the
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
{
|
|
995
|
-
peek_start
|
|
996
|
-
} else {
|
|
997
|
-
body_end
|
|
998
|
-
};
|
|
982
|
+
// The declaration spans from `fn`/`async` through the END of its body. `parse_block` now
|
|
983
|
+
// ends the body block exactly at its closing `}` (#158), so the body's end IS the function's
|
|
984
|
+
// end. (This previously maxed with `peek().start` — the NEXT token after the function — to
|
|
985
|
+
// compensate for the old block-span truncation, which instead overran the function span past
|
|
986
|
+
// `}` and inflated its folding / document-symbol range onto the following line. #158)
|
|
987
|
+
let end = body.as_ref().span().end;
|
|
999
988
|
|
|
1000
989
|
Ok(Statement::FunDecl {
|
|
1001
990
|
async_,
|
|
@@ -2243,7 +2232,7 @@ impl<'a> Parser<'a> {
|
|
|
2243
2232
|
return Err("String key in object literal requires explicit value (key: value)".to_string());
|
|
2244
2233
|
}
|
|
2245
2234
|
};
|
|
2246
|
-
props.push(ObjectProp::KeyValue(key, value));
|
|
2235
|
+
props.push(ObjectProp::KeyValue(key, value, key_span));
|
|
2247
2236
|
}
|
|
2248
2237
|
if !matches!(self.peek_kind(), Some(TokenKind::RBrace)) {
|
|
2249
2238
|
self.expect(TokenKind::Comma)?;
|
|
@@ -81,6 +81,15 @@ thread_local! {
|
|
|
81
81
|
/// no behavior change.
|
|
82
82
|
static REFERENCED_DEFS: std::cell::RefCell<Option<std::collections::HashSet<(usize, usize)>>> =
|
|
83
83
|
const { std::cell::RefCell::new(None) };
|
|
84
|
+
|
|
85
|
+
/// Stack of definition-span starts of the functions whose bodies we are currently inside. A use
|
|
86
|
+
/// that resolves to one of these is a SELF-reference (e.g. `fn a() { return a() }`) — recursion,
|
|
87
|
+
/// not an external use — so it must not mark the binding "referenced" for the unused check, or a
|
|
88
|
+
/// function reachable only via its own recursion would never be flagged dead (#150). Only
|
|
89
|
+
/// consulted while `REFERENCED_DEFS` is active; find-references / go-to-definition (which want the
|
|
90
|
+
/// recursive call) use a different path and are unaffected.
|
|
91
|
+
static CURRENT_FN_DEFS: std::cell::RefCell<Vec<(usize, usize)>> =
|
|
92
|
+
const { std::cell::RefCell::new(Vec::new()) };
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
/// One scope-aware resolution pass: the set of definition span *starts* that at least one identifier
|
|
@@ -92,6 +101,9 @@ fn collect_referenced_def_spans(program: &Program) -> std::collections::HashSet<
|
|
|
92
101
|
impl Drop for Guard {
|
|
93
102
|
fn drop(&mut self) {
|
|
94
103
|
REFERENCED_DEFS.with(|r| *r.borrow_mut() = None);
|
|
104
|
+
// Defensive: the FunDecl walk push/pops this in balanced pairs, but clear it here too so
|
|
105
|
+
// a panic mid-walk can't leave a stale enclosing-fn span for the next collection.
|
|
106
|
+
CURRENT_FN_DEFS.with(|s| s.borrow_mut().clear());
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
let _g = Guard;
|
|
@@ -457,7 +469,7 @@ fn collect_expr(
|
|
|
457
469
|
Expr::Object { props, .. } => {
|
|
458
470
|
for p in props {
|
|
459
471
|
match p {
|
|
460
|
-
tishlang_ast::ObjectProp::KeyValue(_, e) => {
|
|
472
|
+
tishlang_ast::ObjectProp::KeyValue(_, e, _) => {
|
|
461
473
|
collect_expr(e, source, lsp_line, lsp_char, best)
|
|
462
474
|
}
|
|
463
475
|
tishlang_ast::ObjectProp::Spread(e) => {
|
|
@@ -729,7 +741,7 @@ fn member_chain_collect_expr(
|
|
|
729
741
|
Expr::Object { props, .. } => {
|
|
730
742
|
for p in props {
|
|
731
743
|
match p {
|
|
732
|
-
tishlang_ast::ObjectProp::KeyValue(_, e) => {
|
|
744
|
+
tishlang_ast::ObjectProp::KeyValue(_, e, _) => {
|
|
733
745
|
member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
|
|
734
746
|
}
|
|
735
747
|
tishlang_ast::ObjectProp::Spread(e) => {
|
|
@@ -1213,7 +1225,7 @@ fn walk_expr_resolve(
|
|
|
1213
1225
|
Expr::Object { props, .. } => {
|
|
1214
1226
|
for p in props {
|
|
1215
1227
|
let e = match p {
|
|
1216
|
-
tishlang_ast::ObjectProp::KeyValue(_, e) => e,
|
|
1228
|
+
tishlang_ast::ObjectProp::KeyValue(_, e, _) => e,
|
|
1217
1229
|
tishlang_ast::ObjectProp::Spread(e) => e,
|
|
1218
1230
|
};
|
|
1219
1231
|
if let Some(s) = walk_expr_resolve(e, scopes, target) {
|
|
@@ -1730,11 +1742,17 @@ fn record_unresolved(
|
|
|
1730
1742
|
// collect_unused_bindings learns this def is referenced — done before the global check so a
|
|
1731
1743
|
// local that shadows a global name still counts as a use. Unresolved-name output unchanged.
|
|
1732
1744
|
Some(def) => {
|
|
1733
|
-
|
|
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);
|
|
@@ -3695,7 +3759,7 @@ fn tref_expr(expr: &Expr, out: &mut Vec<TypeOcc>) {
|
|
|
3695
3759
|
Expr::Object { props, .. } => {
|
|
3696
3760
|
for p in props {
|
|
3697
3761
|
match p {
|
|
3698
|
-
tishlang_ast::ObjectProp::KeyValue(_, e)
|
|
3762
|
+
tishlang_ast::ObjectProp::KeyValue(_, e, _)
|
|
3699
3763
|
| tishlang_ast::ObjectProp::Spread(e) => tref_expr(e, out),
|
|
3700
3764
|
}
|
|
3701
3765
|
}
|
|
@@ -4159,7 +4223,7 @@ fn refs_expr(
|
|
|
4159
4223
|
Expr::Object { props, .. } => {
|
|
4160
4224
|
for p in props {
|
|
4161
4225
|
match p {
|
|
4162
|
-
tishlang_ast::ObjectProp::KeyValue(_, e) => {
|
|
4226
|
+
tishlang_ast::ObjectProp::KeyValue(_, e, _) => {
|
|
4163
4227
|
refs_expr(e, program, source, name, def_span, out)
|
|
4164
4228
|
}
|
|
4165
4229
|
tishlang_ast::ObjectProp::Spread(e) => {
|
|
@@ -4317,6 +4381,20 @@ mod tests {
|
|
|
4317
4381
|
assert!(names.iter().any(|n| n.as_ref() == "bar"));
|
|
4318
4382
|
}
|
|
4319
4383
|
|
|
4384
|
+
// #158: on the line AFTER a function's closing brace, completion must not leak the function's
|
|
4385
|
+
// params or inner locals (the body Block span used to overrun onto that line). The top-level
|
|
4386
|
+
// function name is still in scope.
|
|
4387
|
+
#[test]
|
|
4388
|
+
fn completion_does_not_leak_past_function_brace() {
|
|
4389
|
+
let src = "fn f(paramA, paramB) {\n let secret = 1\n}\nlet after = 0\n";
|
|
4390
|
+
let program = parse(src).expect("parse");
|
|
4391
|
+
let names = completion_value_names_at_cursor(&program, src, 3, 0); // start of `let after`
|
|
4392
|
+
let has = |n: &str| names.iter().any(|x| x.as_ref() == n);
|
|
4393
|
+
assert!(!has("paramA") && !has("paramB"), "params leaked past `}}`: {names:?}");
|
|
4394
|
+
assert!(!has("secret"), "inner local leaked past `}}`: {names:?}");
|
|
4395
|
+
assert!(has("f"), "top-level fn must stay in scope: {names:?}");
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4320
4398
|
#[test]
|
|
4321
4399
|
fn unresolved_unknown_ident() {
|
|
4322
4400
|
let src = "let x = nope\n";
|
|
@@ -4390,6 +4468,74 @@ mod tests {
|
|
|
4390
4468
|
assert_eq!(u[0].kind, UnusedBindingKind::Import);
|
|
4391
4469
|
}
|
|
4392
4470
|
|
|
4471
|
+
// #149: a binding that is only ever WRITTEN (never read) is unused (matches ESLint).
|
|
4472
|
+
#[test]
|
|
4473
|
+
fn write_only_variables_are_flagged_unused() {
|
|
4474
|
+
for (src, who) in [
|
|
4475
|
+
("let n = 0\nn += 5\n", "n"), // read-modify-write
|
|
4476
|
+
("let w = 0\nw = 7\n", "w"), // plain write
|
|
4477
|
+
("let p = 0\np++\n", "p"), // postfix inc
|
|
4478
|
+
("let q = 0\n++q\n", "q"), // prefix inc
|
|
4479
|
+
] {
|
|
4480
|
+
let program = parse(src).expect("parse");
|
|
4481
|
+
let u = collect_unused_bindings(&program, src);
|
|
4482
|
+
assert!(
|
|
4483
|
+
u.iter().any(|b| b.name.as_ref() == who
|
|
4484
|
+
&& b.kind == UnusedBindingKind::Variable),
|
|
4485
|
+
"{who} is write-only and should be flagged unused; src={src:?} u={u:?}"
|
|
4486
|
+
);
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
#[test]
|
|
4491
|
+
fn variables_read_anywhere_are_not_flagged() {
|
|
4492
|
+
// A read on the RHS of its own assignment, or a bare use, keeps the binding live.
|
|
4493
|
+
for src in [
|
|
4494
|
+
"let count = 0\ncount = count + 1\ncount\n", // read in RHS + bare use
|
|
4495
|
+
"let acc = 0\nacc += 1\nacc\n", // compound write + later read
|
|
4496
|
+
"let x = 1\nx\n", // plain read (control)
|
|
4497
|
+
] {
|
|
4498
|
+
let program = parse(src).expect("parse");
|
|
4499
|
+
let u = collect_unused_bindings(&program, src);
|
|
4500
|
+
assert!(u.is_empty(), "no unused expected; src={src:?} u={u:?}");
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
// #150: a function reachable only via its own recursion is dead code and should be flagged.
|
|
4505
|
+
#[test]
|
|
4506
|
+
fn self_recursive_only_function_is_flagged_unused() {
|
|
4507
|
+
for src in ["fn a() { return a() }\n", "fn loop_(n) { return loop_(n - 1) }\n"] {
|
|
4508
|
+
let program = parse(src).expect("parse");
|
|
4509
|
+
let u = collect_unused_bindings(&program, src);
|
|
4510
|
+
assert_eq!(u.len(), 1, "self-recursive-only fn is unused; src={src:?} u={u:?}");
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
#[test]
|
|
4515
|
+
fn externally_called_or_mutually_recursive_functions_not_flagged() {
|
|
4516
|
+
// External call keeps it live; mutual recursion (each refers to the OTHER) keeps both live —
|
|
4517
|
+
// only pure SELF-reference is excluded.
|
|
4518
|
+
for src in [
|
|
4519
|
+
"fn c() { return 1 }\nc()\n",
|
|
4520
|
+
"fn p() { return q() }\nfn q() { return p() }\np()\n",
|
|
4521
|
+
] {
|
|
4522
|
+
let program = parse(src).expect("parse");
|
|
4523
|
+
let u = collect_unused_bindings(&program, src);
|
|
4524
|
+
assert!(u.is_empty(), "no unused expected; src={src:?} u={u:?}");
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
#[test]
|
|
4529
|
+
fn write_to_undeclared_still_unresolved() {
|
|
4530
|
+
// record_write must keep flagging a write to an undeclared name (not silently swallow it).
|
|
4531
|
+
let program = parse("undeclared = 5\n").expect("parse");
|
|
4532
|
+
let unresolved = collect_unresolved_identifiers(&program);
|
|
4533
|
+
assert!(
|
|
4534
|
+
unresolved.iter().any(|u| u.name.as_ref() == "undeclared"),
|
|
4535
|
+
"writing to an undeclared variable is still unresolved; got={unresolved:?}"
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4393
4539
|
#[test]
|
|
4394
4540
|
fn reference_spans_include_uses_in_exported_fn_body() {
|
|
4395
4541
|
// Directly exercises the fixed walker (also backs find-references / rename).
|
|
@@ -4710,3 +4856,27 @@ mod tests {
|
|
|
4710
4856
|
);
|
|
4711
4857
|
}
|
|
4712
4858
|
}
|
|
4859
|
+
|
|
4860
|
+
#[cfg(test)]
|
|
4861
|
+
mod type_ref_unused_tests {
|
|
4862
|
+
use super::*;
|
|
4863
|
+
fn unused(src: &str) -> Vec<String> {
|
|
4864
|
+
collect_unused_bindings(&tishlang_parser::parse(src).expect("parse"), src)
|
|
4865
|
+
.iter().map(|b| b.name.to_string()).collect()
|
|
4866
|
+
}
|
|
4867
|
+
// #139: an import used only in a type annotation must not be flagged unused.
|
|
4868
|
+
#[test] fn type_only_import_not_unused() {
|
|
4869
|
+
let u = unused("import { Foo } from \"./m\"\nlet x: Foo = bar()\nx\n");
|
|
4870
|
+
assert!(!u.contains(&"Foo".to_string()), "type-only import Foo wrongly unused; u={u:?}");
|
|
4871
|
+
}
|
|
4872
|
+
// #139: a type alias referenced only in an annotation must not be flagged unused.
|
|
4873
|
+
#[test] fn type_alias_used_in_annotation_not_unused() {
|
|
4874
|
+
let u = unused("type T = number\nfn f(x: T) { return x }\nf(1)\n");
|
|
4875
|
+
assert!(!u.contains(&"T".to_string()), "type alias T wrongly unused; u={u:?}");
|
|
4876
|
+
}
|
|
4877
|
+
// guard: a genuinely-unused import is still reported.
|
|
4878
|
+
#[test] fn genuinely_unused_import_still_flagged() {
|
|
4879
|
+
let u = unused("import { Dead } from \"./m\"\nlet y = 1\ny\n");
|
|
4880
|
+
assert!(u.contains(&"Dead".to_string()), "Dead should be unused; u={u:?}");
|
|
4881
|
+
}
|
|
4882
|
+
}
|