@tishlang/tish 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/Cargo.toml +2 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/transform/expr.rs +28 -8
  5. package/crates/js_to_tish/src/transform/stmt.rs +49 -22
  6. package/crates/tish/Cargo.toml +14 -5
  7. package/crates/tish/src/cargo_native_registry.rs +29 -0
  8. package/crates/tish/src/cli_help.rs +16 -10
  9. package/crates/tish/src/main.rs +87 -32
  10. package/crates/tish/src/repl_completion.rs +3 -3
  11. package/crates/tish/tests/cargo_example_compile.rs +1 -1
  12. package/crates/tish/tests/integration_test.rs +19 -7
  13. package/crates/tish/tests/shortcircuit.rs +1 -1
  14. package/crates/tish_ast/src/ast.rs +80 -9
  15. package/crates/tish_build_utils/Cargo.toml +4 -0
  16. package/crates/tish_build_utils/src/lib.rs +105 -2
  17. package/crates/tish_builtins/Cargo.toml +5 -1
  18. package/crates/tish_builtins/src/array.rs +13 -12
  19. package/crates/tish_builtins/src/construct.rs +34 -33
  20. package/crates/tish_builtins/src/globals.rs +12 -11
  21. package/crates/tish_builtins/src/helpers.rs +2 -1
  22. package/crates/tish_builtins/src/object.rs +3 -2
  23. package/crates/tish_builtins/src/string.rs +73 -3
  24. package/crates/tish_bytecode/src/compiler.rs +12 -14
  25. package/crates/tish_bytecode/src/opcode.rs +12 -3
  26. package/crates/tish_compile/Cargo.toml +1 -0
  27. package/crates/tish_compile/src/codegen.rs +745 -199
  28. package/crates/tish_compile/src/infer.rs +6 -0
  29. package/crates/tish_compile/src/lib.rs +4 -3
  30. package/crates/tish_compile/src/resolve.rs +180 -82
  31. package/crates/tish_compile/src/types.rs +175 -11
  32. package/crates/tish_compile_js/Cargo.toml +1 -0
  33. package/crates/tish_compile_js/src/codegen.rs +152 -29
  34. package/crates/tish_compile_js/src/lib.rs +3 -1
  35. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
  36. package/crates/tish_core/Cargo.toml +8 -0
  37. package/crates/tish_core/src/json.rs +102 -53
  38. package/crates/tish_core/src/lib.rs +3 -1
  39. package/crates/tish_core/src/macros.rs +5 -5
  40. package/crates/tish_core/src/value.rs +53 -15
  41. package/crates/tish_core/src/vmref.rs +178 -0
  42. package/crates/tish_eval/Cargo.toml +17 -2
  43. package/crates/tish_eval/src/eval.rs +90 -28
  44. package/crates/tish_eval/src/http.rs +61 -0
  45. package/crates/tish_eval/src/lib.rs +3 -3
  46. package/crates/tish_eval/src/natives.rs +41 -0
  47. package/crates/tish_eval/src/value.rs +7 -3
  48. package/crates/tish_eval/src/value_convert.rs +13 -5
  49. package/crates/tish_fmt/src/lib.rs +120 -30
  50. package/crates/tish_lexer/src/lib.rs +20 -5
  51. package/crates/tish_lexer/src/token.rs +4 -0
  52. package/crates/tish_llvm/src/lib.rs +3 -1
  53. package/crates/tish_lsp/Cargo.toml +4 -1
  54. package/crates/tish_lsp/README.md +1 -1
  55. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  56. package/crates/tish_lsp/src/import_goto.rs +549 -0
  57. package/crates/tish_lsp/src/main.rs +502 -102
  58. package/crates/tish_native/src/build.rs +3 -2
  59. package/crates/tish_native/src/lib.rs +6 -2
  60. package/crates/tish_opt/src/lib.rs +17 -2
  61. package/crates/tish_parser/src/lib.rs +10 -3
  62. package/crates/tish_parser/src/parser.rs +346 -56
  63. package/crates/tish_pg/Cargo.toml +34 -0
  64. package/crates/tish_pg/README.md +38 -0
  65. package/crates/tish_pg/src/error.rs +52 -0
  66. package/crates/tish_pg/src/lib.rs +967 -0
  67. package/crates/tish_resolve/Cargo.toml +13 -0
  68. package/crates/tish_resolve/src/lib.rs +3436 -0
  69. package/crates/tish_resolve/src/pos.rs +133 -0
  70. package/crates/tish_runtime/Cargo.toml +68 -3
  71. package/crates/tish_runtime/src/http.rs +1123 -141
  72. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  73. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  74. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  75. package/crates/tish_runtime/src/lib.rs +159 -29
  76. package/crates/tish_runtime/src/promise.rs +199 -36
  77. package/crates/tish_runtime/src/promise_io.rs +2 -1
  78. package/crates/tish_runtime/src/timers.rs +37 -1
  79. package/crates/tish_runtime/src/ws.rs +26 -28
  80. package/crates/tish_ui/src/jsx.rs +279 -8
  81. package/crates/tish_ui/src/lib.rs +5 -2
  82. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  83. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  84. package/crates/tish_vm/Cargo.toml +15 -5
  85. package/crates/tish_vm/src/vm.rs +506 -259
  86. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  87. package/crates/tish_wasm/src/lib.rs +17 -14
  88. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  89. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  90. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  91. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  92. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  93. package/justfile +8 -0
  94. package/package.json +1 -1
  95. package/platform/darwin-arm64/tish +0 -0
  96. package/platform/darwin-x64/tish +0 -0
  97. package/platform/linux-arm64/tish +0 -0
  98. package/platform/linux-x64/tish +0 -0
  99. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,3436 @@
1
+ //! Lexical name resolution for Tish (go-to-definition, hover, references).
2
+ //!
3
+ //! Coordinates: LSP uses 0-based lines and UTF-16 columns; [`tishlang_ast::Span`] uses 1-based
4
+ //! lines and 1-based **Unicode scalar** columns from the lexer. Conversion goes through byte
5
+ //! offsets in the original source string.
6
+
7
+ mod pos;
8
+
9
+ pub use pos::{lsp_position_for_span_start, span_contains_lsp_position, span_to_lsp_range_exclusive};
10
+
11
+ use std::collections::HashMap;
12
+ use std::sync::Arc;
13
+
14
+ use tishlang_ast::{
15
+ ArrowBody, CallArg, DestructElement, DestructPattern, Expr, ExportDeclaration, FunParam,
16
+ ImportSpecifier, MemberProp, Program, Statement, TypedParam,
17
+ };
18
+
19
+ /// Smallest source span covering the LSP cursor (definition site or reference).
20
+ #[derive(Debug, Clone)]
21
+ pub struct NameUse {
22
+ pub name: Arc<str>,
23
+ pub span: tishlang_ast::Span,
24
+ }
25
+
26
+ /// Find the tightest name under the cursor (identifier reference or binding).
27
+ pub fn name_at_cursor(program: &Program, source: &str, lsp_line: u32, lsp_character: u32) -> Option<NameUse> {
28
+ let mut best: Option<(u64, NameUse)> = None;
29
+ for s in &program.statements {
30
+ collect_stmt(s, source, lsp_line, lsp_character, &mut best);
31
+ }
32
+ best.map(|(_, u)| u)
33
+ }
34
+
35
+ fn span_size(source: &str, span: &tishlang_ast::Span) -> u64 {
36
+ pos::lex_span_byte_range(source, span)
37
+ .map(|(a, b)| b.saturating_sub(a) as u64)
38
+ .unwrap_or(u64::MAX)
39
+ }
40
+
41
+ fn consider(
42
+ source: &str,
43
+ lsp_line: u32,
44
+ lsp_char: u32,
45
+ span: &tishlang_ast::Span,
46
+ name: Arc<str>,
47
+ best: &mut Option<(u64, NameUse)>,
48
+ ) {
49
+ if !pos::span_contains_lsp_position(source, span, lsp_line, lsp_char) {
50
+ return;
51
+ }
52
+ let sz = span_size(source, span);
53
+ let nu = NameUse {
54
+ name,
55
+ span: *span,
56
+ };
57
+ match best {
58
+ None => *best = Some((sz, nu)),
59
+ Some((osz, _)) if sz < *osz => *best = Some((sz, nu)),
60
+ _ => {}
61
+ }
62
+ }
63
+
64
+ fn synthetic_name_span(start: (usize, usize), name: &str) -> tishlang_ast::Span {
65
+ tishlang_ast::Span {
66
+ start,
67
+ end: (start.0, start.1.saturating_add(name.chars().count())),
68
+ }
69
+ }
70
+
71
+ fn collect_stmt(
72
+ stmt: &Statement,
73
+ source: &str,
74
+ lsp_line: u32,
75
+ lsp_char: u32,
76
+ best: &mut Option<(u64, NameUse)>,
77
+ ) {
78
+ match stmt {
79
+ Statement::VarDecl {
80
+ name,
81
+ name_span,
82
+ init,
83
+ ..
84
+ } => {
85
+ consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
86
+ if let Some(e) = init {
87
+ collect_expr(e, source, lsp_line, lsp_char, best);
88
+ }
89
+ }
90
+ Statement::VarDeclDestructure { pattern, init, .. } => {
91
+ collect_destruct_pattern(pattern, source, lsp_line, lsp_char, best);
92
+ collect_expr(init, source, lsp_line, lsp_char, best);
93
+ }
94
+ Statement::ExprStmt { expr, .. } => collect_expr(expr, source, lsp_line, lsp_char, best),
95
+ Statement::If {
96
+ cond,
97
+ then_branch,
98
+ else_branch,
99
+ ..
100
+ } => {
101
+ collect_expr(cond, source, lsp_line, lsp_char, best);
102
+ collect_stmt(then_branch, source, lsp_line, lsp_char, best);
103
+ if let Some(e) = else_branch {
104
+ collect_stmt(e, source, lsp_line, lsp_char, best);
105
+ }
106
+ }
107
+ Statement::While { cond, body, .. } => {
108
+ collect_expr(cond, source, lsp_line, lsp_char, best);
109
+ collect_stmt(body, source, lsp_line, lsp_char, best);
110
+ }
111
+ Statement::For {
112
+ init,
113
+ cond,
114
+ update,
115
+ body,
116
+ ..
117
+ } => {
118
+ if let Some(i) = init {
119
+ collect_stmt(i, source, lsp_line, lsp_char, best);
120
+ }
121
+ if let Some(e) = cond {
122
+ collect_expr(e, source, lsp_line, lsp_char, best);
123
+ }
124
+ if let Some(e) = update {
125
+ collect_expr(e, source, lsp_line, lsp_char, best);
126
+ }
127
+ collect_stmt(body, source, lsp_line, lsp_char, best);
128
+ }
129
+ Statement::ForOf {
130
+ name,
131
+ name_span,
132
+ iterable,
133
+ body,
134
+ ..
135
+ } => {
136
+ consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
137
+ collect_expr(iterable, source, lsp_line, lsp_char, best);
138
+ collect_stmt(body, source, lsp_line, lsp_char, best);
139
+ }
140
+ Statement::Return { value, .. } => {
141
+ if let Some(e) = value {
142
+ collect_expr(e, source, lsp_line, lsp_char, best);
143
+ }
144
+ }
145
+ Statement::Block { statements, .. } => {
146
+ for s in statements {
147
+ collect_stmt(s, source, lsp_line, lsp_char, best);
148
+ }
149
+ }
150
+ Statement::FunDecl {
151
+ name,
152
+ name_span,
153
+ params,
154
+ body,
155
+ ..
156
+ } => {
157
+ consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
158
+ for p in params {
159
+ collect_fun_param(p, source, lsp_line, lsp_char, best);
160
+ }
161
+ collect_stmt(body, source, lsp_line, lsp_char, best);
162
+ }
163
+ Statement::Switch {
164
+ expr,
165
+ cases,
166
+ default_body,
167
+ ..
168
+ } => {
169
+ collect_expr(expr, source, lsp_line, lsp_char, best);
170
+ for (_ce, stmts) in cases {
171
+ for s in stmts {
172
+ collect_stmt(s, source, lsp_line, lsp_char, best);
173
+ }
174
+ }
175
+ if let Some(stmts) = default_body {
176
+ for s in stmts {
177
+ collect_stmt(s, source, lsp_line, lsp_char, best);
178
+ }
179
+ }
180
+ }
181
+ Statement::DoWhile { body, cond, .. } => {
182
+ collect_stmt(body, source, lsp_line, lsp_char, best);
183
+ collect_expr(cond, source, lsp_line, lsp_char, best);
184
+ }
185
+ Statement::Throw { value, .. } => collect_expr(value, source, lsp_line, lsp_char, best),
186
+ Statement::Try {
187
+ body,
188
+ catch_param,
189
+ catch_param_span,
190
+ catch_body,
191
+ finally_body,
192
+ ..
193
+ } => {
194
+ collect_stmt(body, source, lsp_line, lsp_char, best);
195
+ if let (Some(n), Some(sp)) = (catch_param, catch_param_span) {
196
+ consider(source, lsp_line, lsp_char, sp, n.clone(), best);
197
+ }
198
+ if let Some(cb) = catch_body {
199
+ collect_stmt(cb, source, lsp_line, lsp_char, best);
200
+ }
201
+ if let Some(fb) = finally_body {
202
+ collect_stmt(fb, source, lsp_line, lsp_char, best);
203
+ }
204
+ }
205
+ Statement::Import { specifiers, .. } => {
206
+ for sp in specifiers {
207
+ match sp {
208
+ ImportSpecifier::Named {
209
+ name,
210
+ name_span,
211
+ alias,
212
+ alias_span,
213
+ } => {
214
+ let local = alias.as_ref().map(|a| a.clone()).unwrap_or_else(|| name.clone());
215
+ let spn = alias_span.as_ref().unwrap_or(name_span);
216
+ consider(source, lsp_line, lsp_char, spn, local, best);
217
+ }
218
+ ImportSpecifier::Namespace { name, name_span } => {
219
+ consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
220
+ }
221
+ ImportSpecifier::Default { name, name_span } => {
222
+ consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
223
+ }
224
+ }
225
+ }
226
+ }
227
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
228
+ ExportDeclaration::Named(inner) => collect_stmt(inner, source, lsp_line, lsp_char, best),
229
+ ExportDeclaration::Default(e) => collect_expr(e, source, lsp_line, lsp_char, best),
230
+ },
231
+ Statement::TypeAlias { name, name_span, .. } => {
232
+ consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
233
+ }
234
+ Statement::DeclareVar { name, name_span, .. } => {
235
+ consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
236
+ }
237
+ Statement::DeclareFun {
238
+ name,
239
+ name_span,
240
+ params,
241
+ ..
242
+ } => {
243
+ consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
244
+ for p in params {
245
+ collect_fun_param(p, source, lsp_line, lsp_char, best);
246
+ }
247
+ }
248
+ Statement::Break { .. } | Statement::Continue { .. } => {}
249
+ }
250
+ }
251
+
252
+ fn collect_fun_param(
253
+ p: &FunParam,
254
+ source: &str,
255
+ lsp_line: u32,
256
+ lsp_char: u32,
257
+ best: &mut Option<(u64, NameUse)>,
258
+ ) {
259
+ match p {
260
+ FunParam::Simple(tp) => collect_typed_param(tp, source, lsp_line, lsp_char, best),
261
+ FunParam::Destructure { pattern, .. } => {
262
+ collect_destruct_pattern(pattern, source, lsp_line, lsp_char, best);
263
+ }
264
+ }
265
+ }
266
+
267
+ fn collect_typed_param(
268
+ tp: &TypedParam,
269
+ source: &str,
270
+ lsp_line: u32,
271
+ lsp_char: u32,
272
+ best: &mut Option<(u64, NameUse)>,
273
+ ) {
274
+ consider(
275
+ source,
276
+ lsp_line,
277
+ lsp_char,
278
+ &tp.name_span,
279
+ tp.name.clone(),
280
+ best,
281
+ );
282
+ if let Some(e) = &tp.default {
283
+ collect_expr(e, source, lsp_line, lsp_char, best);
284
+ }
285
+ }
286
+
287
+ fn collect_destruct_pattern(
288
+ p: &DestructPattern,
289
+ source: &str,
290
+ lsp_line: u32,
291
+ lsp_char: u32,
292
+ best: &mut Option<(u64, NameUse)>,
293
+ ) {
294
+ match p {
295
+ DestructPattern::Array(elements) => {
296
+ for el in elements {
297
+ if let Some(el) = el {
298
+ match el {
299
+ DestructElement::Ident(n, sp) => {
300
+ consider(source, lsp_line, lsp_char, sp, n.clone(), best);
301
+ }
302
+ DestructElement::Pattern(inner) => {
303
+ collect_destruct_pattern(inner, source, lsp_line, lsp_char, best);
304
+ }
305
+ DestructElement::Rest(n, sp) => {
306
+ consider(source, lsp_line, lsp_char, sp, n.clone(), best);
307
+ }
308
+ }
309
+ }
310
+ }
311
+ }
312
+ DestructPattern::Object(props) => {
313
+ for pr in props {
314
+ match &pr.value {
315
+ DestructElement::Ident(n, sp) => {
316
+ consider(source, lsp_line, lsp_char, sp, n.clone(), best);
317
+ }
318
+ DestructElement::Pattern(inner) => {
319
+ collect_destruct_pattern(inner, source, lsp_line, lsp_char, best);
320
+ }
321
+ DestructElement::Rest(n, sp) => {
322
+ consider(source, lsp_line, lsp_char, sp, n.clone(), best);
323
+ }
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ fn collect_expr(
331
+ expr: &Expr,
332
+ source: &str,
333
+ lsp_line: u32,
334
+ lsp_char: u32,
335
+ best: &mut Option<(u64, NameUse)>,
336
+ ) {
337
+ match expr {
338
+ Expr::Ident { name, span } => consider(source, lsp_line, lsp_char, span, name.clone(), best),
339
+ Expr::Literal { .. } => {}
340
+ Expr::Binary { left, right, .. } => {
341
+ collect_expr(left, source, lsp_line, lsp_char, best);
342
+ collect_expr(right, source, lsp_line, lsp_char, best);
343
+ }
344
+ Expr::Unary { operand, .. } => collect_expr(operand, source, lsp_line, lsp_char, best),
345
+ Expr::Call { callee, args, .. } => {
346
+ collect_expr(callee, source, lsp_line, lsp_char, best);
347
+ for a in args {
348
+ match a {
349
+ CallArg::Expr(e) => collect_expr(e, source, lsp_line, lsp_char, best),
350
+ CallArg::Spread(e) => collect_expr(e, source, lsp_line, lsp_char, best),
351
+ }
352
+ }
353
+ }
354
+ Expr::New { callee, args, .. } => {
355
+ collect_expr(callee, source, lsp_line, lsp_char, best);
356
+ for a in args {
357
+ match a {
358
+ CallArg::Expr(e) => collect_expr(e, source, lsp_line, lsp_char, best),
359
+ CallArg::Spread(e) => collect_expr(e, source, lsp_line, lsp_char, best),
360
+ }
361
+ }
362
+ }
363
+ Expr::Member { object, prop, .. } => {
364
+ collect_expr(object, source, lsp_line, lsp_char, best);
365
+ match prop {
366
+ MemberProp::Name { name, span } => {
367
+ consider(source, lsp_line, lsp_char, span, name.clone(), best);
368
+ }
369
+ MemberProp::Expr(ix) => collect_expr(ix, source, lsp_line, lsp_char, best),
370
+ }
371
+ }
372
+ Expr::Index { object, index, .. } => {
373
+ collect_expr(object, source, lsp_line, lsp_char, best);
374
+ collect_expr(index, source, lsp_line, lsp_char, best);
375
+ }
376
+ Expr::Conditional {
377
+ cond,
378
+ then_branch,
379
+ else_branch,
380
+ ..
381
+ } => {
382
+ collect_expr(cond, source, lsp_line, lsp_char, best);
383
+ collect_expr(then_branch, source, lsp_line, lsp_char, best);
384
+ collect_expr(else_branch, source, lsp_line, lsp_char, best);
385
+ }
386
+ Expr::NullishCoalesce { left, right, .. } => {
387
+ collect_expr(left, source, lsp_line, lsp_char, best);
388
+ collect_expr(right, source, lsp_line, lsp_char, best);
389
+ }
390
+ Expr::Array { elements, .. } => {
391
+ for el in elements {
392
+ match el {
393
+ tishlang_ast::ArrayElement::Expr(e) => {
394
+ collect_expr(e, source, lsp_line, lsp_char, best)
395
+ }
396
+ tishlang_ast::ArrayElement::Spread(e) => {
397
+ collect_expr(e, source, lsp_line, lsp_char, best)
398
+ }
399
+ }
400
+ }
401
+ }
402
+ Expr::Object { props, .. } => {
403
+ for p in props {
404
+ match p {
405
+ tishlang_ast::ObjectProp::KeyValue(_, e) => {
406
+ collect_expr(e, source, lsp_line, lsp_char, best)
407
+ }
408
+ tishlang_ast::ObjectProp::Spread(e) => {
409
+ collect_expr(e, source, lsp_line, lsp_char, best)
410
+ }
411
+ }
412
+ }
413
+ }
414
+ Expr::Assign { name, span, value } => {
415
+ let sp = synthetic_name_span(span.start, name.as_ref());
416
+ consider(source, lsp_line, lsp_char, &sp, name.clone(), best);
417
+ collect_expr(value, source, lsp_line, lsp_char, best);
418
+ }
419
+ Expr::TypeOf { operand, .. } => collect_expr(operand, source, lsp_line, lsp_char, best),
420
+ Expr::PostfixInc { name, span } | Expr::PostfixDec { name, span } => {
421
+ let sp = synthetic_name_span(span.start, name.as_ref());
422
+ consider(source, lsp_line, lsp_char, &sp, name.clone(), best);
423
+ }
424
+ Expr::PrefixInc { name, span } | Expr::PrefixDec { name, span } => {
425
+ let sp = synthetic_name_span(span.start, name.as_ref());
426
+ consider(source, lsp_line, lsp_char, &sp, name.clone(), best);
427
+ }
428
+ Expr::CompoundAssign { name, span, value, .. }
429
+ | Expr::LogicalAssign { name, span, value, .. } => {
430
+ let sp = synthetic_name_span(span.start, name.as_ref());
431
+ consider(source, lsp_line, lsp_char, &sp, name.clone(), best);
432
+ collect_expr(value, source, lsp_line, lsp_char, best);
433
+ }
434
+ Expr::MemberAssign {
435
+ object,
436
+ value,
437
+ ..
438
+ } => {
439
+ collect_expr(object, source, lsp_line, lsp_char, best);
440
+ collect_expr(value, source, lsp_line, lsp_char, best);
441
+ }
442
+ Expr::IndexAssign {
443
+ object,
444
+ index,
445
+ value,
446
+ ..
447
+ } => {
448
+ collect_expr(object, source, lsp_line, lsp_char, best);
449
+ collect_expr(index, source, lsp_line, lsp_char, best);
450
+ collect_expr(value, source, lsp_line, lsp_char, best);
451
+ }
452
+ Expr::ArrowFunction { params, body, .. } => {
453
+ for p in params {
454
+ collect_fun_param(p, source, lsp_line, lsp_char, best);
455
+ }
456
+ match body {
457
+ ArrowBody::Expr(e) => collect_expr(e, source, lsp_line, lsp_char, best),
458
+ ArrowBody::Block(b) => collect_stmt(b, source, lsp_line, lsp_char, best),
459
+ }
460
+ }
461
+ Expr::TemplateLiteral { exprs, .. } => {
462
+ for e in exprs {
463
+ collect_expr(e, source, lsp_line, lsp_char, best);
464
+ }
465
+ }
466
+ Expr::Await { operand, .. } => collect_expr(operand, source, lsp_line, lsp_char, best),
467
+ Expr::JsxElement { props, children, .. } => {
468
+ for p in props {
469
+ match p {
470
+ tishlang_ast::JsxProp::Attr { value, .. } => match value {
471
+ tishlang_ast::JsxAttrValue::Expr(e) => {
472
+ collect_expr(e, source, lsp_line, lsp_char, best)
473
+ }
474
+ _ => {}
475
+ },
476
+ tishlang_ast::JsxProp::Spread(e) => {
477
+ collect_expr(e, source, lsp_line, lsp_char, best)
478
+ }
479
+ }
480
+ }
481
+ for ch in children {
482
+ match ch {
483
+ tishlang_ast::JsxChild::Expr(e) => {
484
+ collect_expr(e, source, lsp_line, lsp_char, best)
485
+ }
486
+ tishlang_ast::JsxChild::Text(_) => {}
487
+ }
488
+ }
489
+ }
490
+ Expr::JsxFragment { children, .. } => {
491
+ for ch in children {
492
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
493
+ collect_expr(e, source, lsp_line, lsp_char, best);
494
+ }
495
+ }
496
+ }
497
+ Expr::NativeModuleLoad { .. } => {}
498
+ }
499
+ }
500
+
501
+ /// For `a.b.c` with the cursor on property `c`, the root binding local name and member path `[b, c]`.
502
+ #[derive(Debug, Clone)]
503
+ pub struct MemberAccessChain {
504
+ pub root_local: Arc<str>,
505
+ pub members: Vec<Arc<str>>,
506
+ }
507
+
508
+ /// When the cursor sits on a static member name, resolve `root.local` plus `members` left-to-right after the root.
509
+ pub fn member_access_chain_at_cursor(
510
+ program: &Program,
511
+ source: &str,
512
+ lsp_line: u32,
513
+ lsp_character: u32,
514
+ ) -> Option<MemberAccessChain> {
515
+ let mut best: Option<(u64, MemberAccessChain)> = None;
516
+ for s in &program.statements {
517
+ member_chain_collect_stmt(s, source, lsp_line, lsp_character, &mut best);
518
+ }
519
+ best.map(|(_, c)| c)
520
+ }
521
+
522
+ fn chain_from_member_object(object: &Expr, rightmost: Arc<str>) -> Option<MemberAccessChain> {
523
+ let mut members = vec![rightmost];
524
+ let mut cur = object;
525
+ loop {
526
+ match cur {
527
+ Expr::Member {
528
+ object: o,
529
+ prop: MemberProp::Name { name, .. },
530
+ ..
531
+ } => {
532
+ members.push(name.clone());
533
+ cur = o.as_ref();
534
+ }
535
+ Expr::Ident { name, .. } => {
536
+ members.reverse();
537
+ return Some(MemberAccessChain {
538
+ root_local: name.clone(),
539
+ members,
540
+ });
541
+ }
542
+ _ => return None,
543
+ }
544
+ }
545
+ }
546
+
547
+ fn member_chain_try_update(
548
+ source: &str,
549
+ lsp_line: u32,
550
+ lsp_char: u32,
551
+ prop_span: &tishlang_ast::Span,
552
+ object: &Expr,
553
+ name: Arc<str>,
554
+ best: &mut Option<(u64, MemberAccessChain)>,
555
+ ) {
556
+ if !pos::span_contains_lsp_position(source, prop_span, lsp_line, lsp_char) {
557
+ return;
558
+ }
559
+ let Some(chain) = chain_from_member_object(object, name) else {
560
+ return;
561
+ };
562
+ let sz = span_size(source, prop_span);
563
+ match best {
564
+ None => *best = Some((sz, chain)),
565
+ Some((osz, _)) if sz < *osz => *best = Some((sz, chain)),
566
+ _ => {}
567
+ }
568
+ }
569
+
570
+ fn member_chain_collect_expr(
571
+ expr: &Expr,
572
+ source: &str,
573
+ lsp_line: u32,
574
+ lsp_char: u32,
575
+ best: &mut Option<(u64, MemberAccessChain)>,
576
+ ) {
577
+ match expr {
578
+ Expr::Ident { .. } | Expr::Literal { .. } | Expr::NativeModuleLoad { .. } => {}
579
+ Expr::Binary { left, right, .. } => {
580
+ member_chain_collect_expr(left, source, lsp_line, lsp_char, best);
581
+ member_chain_collect_expr(right, source, lsp_line, lsp_char, best);
582
+ }
583
+ Expr::Unary { operand, .. } => member_chain_collect_expr(operand, source, lsp_line, lsp_char, best),
584
+ Expr::Call { callee, args, .. } => {
585
+ member_chain_collect_expr(callee, source, lsp_line, lsp_char, best);
586
+ for a in args {
587
+ match a {
588
+ CallArg::Expr(e) => member_chain_collect_expr(e, source, lsp_line, lsp_char, best),
589
+ CallArg::Spread(e) => member_chain_collect_expr(e, source, lsp_line, lsp_char, best),
590
+ }
591
+ }
592
+ }
593
+ Expr::New { callee, args, .. } => {
594
+ member_chain_collect_expr(callee, source, lsp_line, lsp_char, best);
595
+ for a in args {
596
+ match a {
597
+ CallArg::Expr(e) => member_chain_collect_expr(e, source, lsp_line, lsp_char, best),
598
+ CallArg::Spread(e) => member_chain_collect_expr(e, source, lsp_line, lsp_char, best),
599
+ }
600
+ }
601
+ }
602
+ Expr::Member { object, prop, .. } => {
603
+ member_chain_collect_expr(object.as_ref(), source, lsp_line, lsp_char, best);
604
+ match prop {
605
+ MemberProp::Name { name, span } => {
606
+ member_chain_try_update(
607
+ source,
608
+ lsp_line,
609
+ lsp_char,
610
+ span,
611
+ object.as_ref(),
612
+ name.clone(),
613
+ best,
614
+ );
615
+ }
616
+ MemberProp::Expr(ix) => {
617
+ member_chain_collect_expr(ix, source, lsp_line, lsp_char, best);
618
+ }
619
+ }
620
+ }
621
+ Expr::Index { object, index, .. } => {
622
+ member_chain_collect_expr(object, source, lsp_line, lsp_char, best);
623
+ member_chain_collect_expr(index, source, lsp_line, lsp_char, best);
624
+ }
625
+ Expr::Conditional {
626
+ cond,
627
+ then_branch,
628
+ else_branch,
629
+ ..
630
+ } => {
631
+ member_chain_collect_expr(cond, source, lsp_line, lsp_char, best);
632
+ member_chain_collect_expr(then_branch, source, lsp_line, lsp_char, best);
633
+ member_chain_collect_expr(else_branch, source, lsp_line, lsp_char, best);
634
+ }
635
+ Expr::NullishCoalesce { left, right, .. } => {
636
+ member_chain_collect_expr(left, source, lsp_line, lsp_char, best);
637
+ member_chain_collect_expr(right, source, lsp_line, lsp_char, best);
638
+ }
639
+ Expr::Array { elements, .. } => {
640
+ for el in elements {
641
+ match el {
642
+ tishlang_ast::ArrayElement::Expr(e) => {
643
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
644
+ }
645
+ tishlang_ast::ArrayElement::Spread(e) => {
646
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
647
+ }
648
+ }
649
+ }
650
+ }
651
+ Expr::Object { props, .. } => {
652
+ for p in props {
653
+ match p {
654
+ tishlang_ast::ObjectProp::KeyValue(_, e) => {
655
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
656
+ }
657
+ tishlang_ast::ObjectProp::Spread(e) => {
658
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
659
+ }
660
+ }
661
+ }
662
+ }
663
+ Expr::Assign { value, .. } | Expr::CompoundAssign { value, .. } | Expr::LogicalAssign { value, .. } => {
664
+ member_chain_collect_expr(value, source, lsp_line, lsp_char, best);
665
+ }
666
+ Expr::TypeOf { operand, .. } => member_chain_collect_expr(operand, source, lsp_line, lsp_char, best),
667
+ Expr::PostfixInc { .. }
668
+ | Expr::PostfixDec { .. }
669
+ | Expr::PrefixInc { .. }
670
+ | Expr::PrefixDec { .. } => {}
671
+ Expr::MemberAssign { object, value, .. } => {
672
+ member_chain_collect_expr(object, source, lsp_line, lsp_char, best);
673
+ member_chain_collect_expr(value, source, lsp_line, lsp_char, best);
674
+ }
675
+ Expr::IndexAssign {
676
+ object,
677
+ index,
678
+ value,
679
+ ..
680
+ } => {
681
+ member_chain_collect_expr(object, source, lsp_line, lsp_char, best);
682
+ member_chain_collect_expr(index, source, lsp_line, lsp_char, best);
683
+ member_chain_collect_expr(value, source, lsp_line, lsp_char, best);
684
+ }
685
+ Expr::ArrowFunction { params, body, .. } => {
686
+ for p in params {
687
+ member_chain_collect_fun_param(p, source, lsp_line, lsp_char, best);
688
+ }
689
+ match body {
690
+ ArrowBody::Expr(e) => member_chain_collect_expr(e, source, lsp_line, lsp_char, best),
691
+ ArrowBody::Block(b) => member_chain_collect_stmt(b, source, lsp_line, lsp_char, best),
692
+ }
693
+ }
694
+ Expr::TemplateLiteral { exprs, .. } => {
695
+ for e in exprs {
696
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
697
+ }
698
+ }
699
+ Expr::Await { operand, .. } => member_chain_collect_expr(operand, source, lsp_line, lsp_char, best),
700
+ Expr::JsxElement { props, children, .. } => {
701
+ for p in props {
702
+ match p {
703
+ tishlang_ast::JsxProp::Attr { value, .. } => {
704
+ if let tishlang_ast::JsxAttrValue::Expr(e) = value {
705
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
706
+ }
707
+ }
708
+ tishlang_ast::JsxProp::Spread(e) => {
709
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
710
+ }
711
+ }
712
+ }
713
+ for ch in children {
714
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
715
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
716
+ }
717
+ }
718
+ }
719
+ Expr::JsxFragment { children, .. } => {
720
+ for ch in children {
721
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
722
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
723
+ }
724
+ }
725
+ }
726
+ }
727
+ }
728
+
729
+ fn member_chain_collect_fun_param(
730
+ p: &FunParam,
731
+ source: &str,
732
+ lsp_line: u32,
733
+ lsp_char: u32,
734
+ best: &mut Option<(u64, MemberAccessChain)>,
735
+ ) {
736
+ match p {
737
+ FunParam::Simple(tp) => {
738
+ if let Some(e) = &tp.default {
739
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
740
+ }
741
+ }
742
+ FunParam::Destructure { pattern, .. } => {
743
+ member_chain_collect_destruct_pattern(pattern, source, lsp_line, lsp_char, best);
744
+ }
745
+ }
746
+ }
747
+
748
+ fn member_chain_collect_destruct_pattern(
749
+ pattern: &DestructPattern,
750
+ source: &str,
751
+ lsp_line: u32,
752
+ lsp_char: u32,
753
+ best: &mut Option<(u64, MemberAccessChain)>,
754
+ ) {
755
+ match pattern {
756
+ DestructPattern::Array(elements) => {
757
+ for el in elements {
758
+ if let Some(el) = el {
759
+ match el {
760
+ DestructElement::Ident(_, _) => {}
761
+ DestructElement::Pattern(inner) => {
762
+ member_chain_collect_destruct_pattern(inner, source, lsp_line, lsp_char, best)
763
+ }
764
+ DestructElement::Rest(_, _) => {}
765
+ }
766
+ }
767
+ }
768
+ }
769
+ DestructPattern::Object(props) => {
770
+ for pr in props {
771
+ match &pr.value {
772
+ DestructElement::Ident(_, _) => {}
773
+ DestructElement::Pattern(inner) => {
774
+ member_chain_collect_destruct_pattern(inner, source, lsp_line, lsp_char, best)
775
+ }
776
+ DestructElement::Rest(_, _) => {}
777
+ }
778
+ }
779
+ }
780
+ }
781
+ }
782
+
783
+ fn member_chain_collect_stmt(
784
+ stmt: &Statement,
785
+ source: &str,
786
+ lsp_line: u32,
787
+ lsp_char: u32,
788
+ best: &mut Option<(u64, MemberAccessChain)>,
789
+ ) {
790
+ match stmt {
791
+ Statement::VarDecl { init, .. } => {
792
+ if let Some(e) = init {
793
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
794
+ }
795
+ }
796
+ Statement::VarDeclDestructure { pattern, init, .. } => {
797
+ member_chain_collect_destruct_pattern(pattern, source, lsp_line, lsp_char, best);
798
+ member_chain_collect_expr(init, source, lsp_line, lsp_char, best);
799
+ }
800
+ Statement::ExprStmt { expr, .. } => member_chain_collect_expr(expr, source, lsp_line, lsp_char, best),
801
+ Statement::If {
802
+ cond,
803
+ then_branch,
804
+ else_branch,
805
+ ..
806
+ } => {
807
+ member_chain_collect_expr(cond, source, lsp_line, lsp_char, best);
808
+ member_chain_collect_stmt(then_branch, source, lsp_line, lsp_char, best);
809
+ if let Some(e) = else_branch {
810
+ member_chain_collect_stmt(e, source, lsp_line, lsp_char, best);
811
+ }
812
+ }
813
+ Statement::While { cond, body, .. } => {
814
+ member_chain_collect_expr(cond, source, lsp_line, lsp_char, best);
815
+ member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
816
+ }
817
+ Statement::For {
818
+ init,
819
+ cond,
820
+ update,
821
+ body,
822
+ ..
823
+ } => {
824
+ if let Some(i) = init {
825
+ member_chain_collect_stmt(i, source, lsp_line, lsp_char, best);
826
+ }
827
+ if let Some(e) = cond {
828
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
829
+ }
830
+ if let Some(e) = update {
831
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
832
+ }
833
+ member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
834
+ }
835
+ Statement::ForOf { iterable, body, .. } => {
836
+ member_chain_collect_expr(iterable, source, lsp_line, lsp_char, best);
837
+ member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
838
+ }
839
+ Statement::Return { value, .. } => {
840
+ if let Some(e) = value {
841
+ member_chain_collect_expr(e, source, lsp_line, lsp_char, best);
842
+ }
843
+ }
844
+ Statement::Block { statements, .. } => {
845
+ for s in statements {
846
+ member_chain_collect_stmt(s, source, lsp_line, lsp_char, best);
847
+ }
848
+ }
849
+ Statement::FunDecl { params, body, .. } => {
850
+ for p in params {
851
+ member_chain_collect_fun_param(p, source, lsp_line, lsp_char, best);
852
+ }
853
+ member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
854
+ }
855
+ Statement::Switch {
856
+ expr,
857
+ cases,
858
+ default_body,
859
+ ..
860
+ } => {
861
+ member_chain_collect_expr(expr, source, lsp_line, lsp_char, best);
862
+ for (_ce, stmts) in cases {
863
+ for s in stmts {
864
+ member_chain_collect_stmt(s, source, lsp_line, lsp_char, best);
865
+ }
866
+ }
867
+ if let Some(stmts) = default_body {
868
+ for s in stmts {
869
+ member_chain_collect_stmt(s, source, lsp_line, lsp_char, best);
870
+ }
871
+ }
872
+ }
873
+ Statement::DoWhile { body, cond, .. } => {
874
+ member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
875
+ member_chain_collect_expr(cond, source, lsp_line, lsp_char, best);
876
+ }
877
+ Statement::Throw { value, .. } => member_chain_collect_expr(value, source, lsp_line, lsp_char, best),
878
+ Statement::Try {
879
+ body,
880
+ catch_body,
881
+ finally_body,
882
+ ..
883
+ } => {
884
+ member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
885
+ if let Some(cb) = catch_body {
886
+ member_chain_collect_stmt(cb, source, lsp_line, lsp_char, best);
887
+ }
888
+ if let Some(fb) = finally_body {
889
+ member_chain_collect_stmt(fb, source, lsp_line, lsp_char, best);
890
+ }
891
+ }
892
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
893
+ ExportDeclaration::Named(inner) => member_chain_collect_stmt(inner, source, lsp_line, lsp_char, best),
894
+ ExportDeclaration::Default(e) => member_chain_collect_expr(e, source, lsp_line, lsp_char, best),
895
+ },
896
+ Statement::Import { .. }
897
+ | Statement::Break { .. }
898
+ | Statement::Continue { .. }
899
+ | Statement::TypeAlias { .. }
900
+ | Statement::DeclareVar { .. }
901
+ | Statement::DeclareFun { .. } => {}
902
+ }
903
+ }
904
+
905
+ // --- resolve pass ---
906
+
907
+ struct ScopeStack(Vec<HashMap<String, tishlang_ast::Span>>);
908
+
909
+ impl ScopeStack {
910
+ fn new() -> Self {
911
+ Self(vec![HashMap::new()])
912
+ }
913
+ fn fork(&self) -> Self {
914
+ Self(self.0.clone())
915
+ }
916
+ fn push(&mut self) {
917
+ self.0.push(HashMap::new());
918
+ }
919
+ fn pop(&mut self) {
920
+ let _ = self.0.pop();
921
+ }
922
+ fn define(&mut self, name: &str, span: tishlang_ast::Span) {
923
+ if let Some(m) = self.0.last_mut() {
924
+ m.insert(name.to_string(), span);
925
+ }
926
+ }
927
+ fn resolve(&self, name: &str) -> Option<tishlang_ast::Span> {
928
+ for m in self.0.iter().rev() {
929
+ if let Some(s) = m.get(name) {
930
+ return Some(*s);
931
+ }
932
+ }
933
+ None
934
+ }
935
+ }
936
+
937
+ fn define_fun_param_stack(p: &FunParam, scopes: &mut ScopeStack) {
938
+ match p {
939
+ FunParam::Simple(tp) => {
940
+ scopes.define(tp.name.as_ref(), tp.name_span);
941
+ }
942
+ FunParam::Destructure { pattern, .. } => define_pattern_stack(pattern, scopes),
943
+ }
944
+ }
945
+
946
+ fn define_pattern_stack(pattern: &DestructPattern, scopes: &mut ScopeStack) {
947
+ match pattern {
948
+ DestructPattern::Array(elements) => {
949
+ for el in elements {
950
+ if let Some(el) = el {
951
+ match el {
952
+ DestructElement::Ident(n, sp) => scopes.define(n.as_ref(), *sp),
953
+ DestructElement::Pattern(inner) => define_pattern_stack(inner, scopes),
954
+ DestructElement::Rest(n, sp) => scopes.define(n.as_ref(), *sp),
955
+ }
956
+ }
957
+ }
958
+ }
959
+ DestructPattern::Object(props) => {
960
+ for pr in props {
961
+ match &pr.value {
962
+ DestructElement::Ident(n, sp) => scopes.define(n.as_ref(), *sp),
963
+ DestructElement::Pattern(inner) => define_pattern_stack(inner, scopes),
964
+ DestructElement::Rest(n, sp) => scopes.define(n.as_ref(), *sp),
965
+ }
966
+ }
967
+ }
968
+ }
969
+ }
970
+
971
+ fn walk_expr_resolve(
972
+ expr: &Expr,
973
+ scopes: &ScopeStack,
974
+ target: &NameUse,
975
+ ) -> Option<tishlang_ast::Span> {
976
+ let tgt = target.span;
977
+ match expr {
978
+ Expr::Ident { name, span } if *span == tgt && name.as_ref() == target.name.as_ref() => {
979
+ scopes.resolve(name.as_ref())
980
+ }
981
+ Expr::Assign { name, span, value } => {
982
+ let sp = synthetic_name_span(span.start, name.as_ref());
983
+ if sp == tgt && name.as_ref() == target.name.as_ref() {
984
+ return scopes.resolve(name.as_ref());
985
+ }
986
+ walk_expr_resolve(value, scopes, target)
987
+ }
988
+ Expr::CompoundAssign { name, span, value, .. } | Expr::LogicalAssign { name, span, value, .. } => {
989
+ let sp = synthetic_name_span(span.start, name.as_ref());
990
+ if sp == tgt && name.as_ref() == target.name.as_ref() {
991
+ return scopes.resolve(name.as_ref());
992
+ }
993
+ walk_expr_resolve(value, scopes, target)
994
+ }
995
+ Expr::PostfixInc { name, span } | Expr::PostfixDec { name, span } => {
996
+ let sp = synthetic_name_span(span.start, name.as_ref());
997
+ if sp == tgt && name.as_ref() == target.name.as_ref() {
998
+ return scopes.resolve(name.as_ref());
999
+ }
1000
+ None
1001
+ }
1002
+ Expr::PrefixInc { name, span } | Expr::PrefixDec { name, span } => {
1003
+ let sp = synthetic_name_span(span.start, name.as_ref());
1004
+ if sp == tgt && name.as_ref() == target.name.as_ref() {
1005
+ return scopes.resolve(name.as_ref());
1006
+ }
1007
+ None
1008
+ }
1009
+ Expr::Binary { left, right, .. } => walk_expr_resolve(left, scopes, target)
1010
+ .or_else(|| walk_expr_resolve(right, scopes, target)),
1011
+ Expr::Unary { operand, .. } => walk_expr_resolve(operand, scopes, target),
1012
+ Expr::Call { callee, args, .. } => {
1013
+ walk_expr_resolve(callee, scopes, target).or_else(|| {
1014
+ for a in args {
1015
+ let e = match a {
1016
+ CallArg::Expr(e) => e,
1017
+ CallArg::Spread(e) => e,
1018
+ };
1019
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1020
+ return Some(s);
1021
+ }
1022
+ }
1023
+ None
1024
+ })
1025
+ }
1026
+ Expr::New { callee, args, .. } => {
1027
+ walk_expr_resolve(callee, scopes, target).or_else(|| {
1028
+ for a in args {
1029
+ let e = match a {
1030
+ CallArg::Expr(e) => e,
1031
+ CallArg::Spread(e) => e,
1032
+ };
1033
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1034
+ return Some(s);
1035
+ }
1036
+ }
1037
+ None
1038
+ })
1039
+ }
1040
+ Expr::Member { object, .. } => walk_expr_resolve(object, scopes, target),
1041
+ Expr::Index { object, index, .. } => walk_expr_resolve(object, scopes, target)
1042
+ .or_else(|| walk_expr_resolve(index, scopes, target)),
1043
+ Expr::Conditional {
1044
+ cond,
1045
+ then_branch,
1046
+ else_branch,
1047
+ ..
1048
+ } => walk_expr_resolve(cond, scopes, target)
1049
+ .or_else(|| walk_expr_resolve(then_branch, scopes, target))
1050
+ .or_else(|| walk_expr_resolve(else_branch, scopes, target)),
1051
+ Expr::NullishCoalesce { left, right, .. } => walk_expr_resolve(left, scopes, target)
1052
+ .or_else(|| walk_expr_resolve(right, scopes, target)),
1053
+ Expr::Array { elements, .. } => {
1054
+ for el in elements {
1055
+ let e = match el {
1056
+ tishlang_ast::ArrayElement::Expr(e) => e,
1057
+ tishlang_ast::ArrayElement::Spread(e) => e,
1058
+ };
1059
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1060
+ return Some(s);
1061
+ }
1062
+ }
1063
+ None
1064
+ }
1065
+ Expr::Object { props, .. } => {
1066
+ for p in props {
1067
+ let e = match p {
1068
+ tishlang_ast::ObjectProp::KeyValue(_, e) => e,
1069
+ tishlang_ast::ObjectProp::Spread(e) => e,
1070
+ };
1071
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1072
+ return Some(s);
1073
+ }
1074
+ }
1075
+ None
1076
+ }
1077
+ Expr::TypeOf { operand, .. } => walk_expr_resolve(operand, scopes, target),
1078
+ Expr::MemberAssign { object, value, .. } => walk_expr_resolve(object, scopes, target)
1079
+ .or_else(|| walk_expr_resolve(value, scopes, target)),
1080
+ Expr::IndexAssign {
1081
+ object,
1082
+ index,
1083
+ value,
1084
+ ..
1085
+ } => walk_expr_resolve(object, scopes, target)
1086
+ .or_else(|| walk_expr_resolve(index, scopes, target))
1087
+ .or_else(|| walk_expr_resolve(value, scopes, target)),
1088
+ Expr::ArrowFunction { params, body, .. } => {
1089
+ let mut inner = scopes.fork();
1090
+ inner.push();
1091
+ for p in params {
1092
+ define_fun_param_stack(p, &mut inner);
1093
+ }
1094
+ match body {
1095
+ ArrowBody::Expr(e) => walk_expr_resolve(e, &inner, target),
1096
+ ArrowBody::Block(b) => walk_stmt_resolve(b, &mut inner, target),
1097
+ }
1098
+ }
1099
+ Expr::TemplateLiteral { exprs, .. } => {
1100
+ for e in exprs {
1101
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1102
+ return Some(s);
1103
+ }
1104
+ }
1105
+ None
1106
+ }
1107
+ Expr::Await { operand, .. } => walk_expr_resolve(operand, scopes, target),
1108
+ Expr::JsxElement { props, children, .. } => {
1109
+ for p in props {
1110
+ match p {
1111
+ tishlang_ast::JsxProp::Attr { value, .. } => {
1112
+ if let tishlang_ast::JsxAttrValue::Expr(e) = value {
1113
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1114
+ return Some(s);
1115
+ }
1116
+ }
1117
+ }
1118
+ tishlang_ast::JsxProp::Spread(e) => {
1119
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1120
+ return Some(s);
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ for ch in children {
1126
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
1127
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1128
+ return Some(s);
1129
+ }
1130
+ }
1131
+ }
1132
+ None
1133
+ }
1134
+ Expr::JsxFragment { children, .. } => {
1135
+ for ch in children {
1136
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
1137
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1138
+ return Some(s);
1139
+ }
1140
+ }
1141
+ }
1142
+ None
1143
+ }
1144
+ Expr::Ident { .. } => None,
1145
+ Expr::Literal { .. } | Expr::NativeModuleLoad { .. } => None,
1146
+ }
1147
+ }
1148
+
1149
+ fn walk_stmt_resolve(
1150
+ stmt: &Statement,
1151
+ scopes: &mut ScopeStack,
1152
+ target: &NameUse,
1153
+ ) -> Option<tishlang_ast::Span> {
1154
+ let tgt_span = target.span;
1155
+ match stmt {
1156
+ Statement::VarDecl {
1157
+ name,
1158
+ name_span,
1159
+ mutable: _,
1160
+ type_ann: _,
1161
+ init,
1162
+ ..
1163
+ } => {
1164
+ if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1165
+ return Some(*name_span);
1166
+ }
1167
+ if let Some(e) = init {
1168
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1169
+ return Some(s);
1170
+ }
1171
+ }
1172
+ scopes.define(name.as_ref(), *name_span);
1173
+ None
1174
+ }
1175
+ Statement::VarDeclDestructure {
1176
+ pattern,
1177
+ init,
1178
+ ..
1179
+ } => {
1180
+ if let Some(s) = walk_expr_resolve(init, scopes, target) {
1181
+ return Some(s);
1182
+ }
1183
+ define_pattern_stack(pattern, scopes);
1184
+ None
1185
+ }
1186
+ Statement::ExprStmt { expr, .. } => walk_expr_resolve(expr, scopes, target),
1187
+ Statement::If {
1188
+ cond,
1189
+ then_branch,
1190
+ else_branch,
1191
+ ..
1192
+ } => {
1193
+ walk_expr_resolve(cond, scopes, target)
1194
+ .or_else(|| walk_stmt_implicit(then_branch, scopes, target))
1195
+ .or_else(|| else_branch.as_ref().and_then(|b| walk_stmt_implicit(b, scopes, target)))
1196
+ }
1197
+ Statement::While { cond, body, .. } => {
1198
+ walk_expr_resolve(cond, scopes, target).or_else(|| walk_stmt_implicit(body, scopes, target))
1199
+ }
1200
+ Statement::For {
1201
+ init,
1202
+ cond,
1203
+ update,
1204
+ body,
1205
+ ..
1206
+ } => {
1207
+ scopes.push();
1208
+ if let Some(i) = init {
1209
+ if let Some(s) = walk_stmt_resolve(i, scopes, target) {
1210
+ scopes.pop();
1211
+ return Some(s);
1212
+ }
1213
+ }
1214
+ let r = (|| {
1215
+ if let Some(e) = cond {
1216
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1217
+ return Some(s);
1218
+ }
1219
+ }
1220
+ if let Some(e) = update {
1221
+ if let Some(s) = walk_expr_resolve(e, scopes, target) {
1222
+ return Some(s);
1223
+ }
1224
+ }
1225
+ walk_stmt_implicit(body, scopes, target)
1226
+ })();
1227
+ scopes.pop();
1228
+ r
1229
+ }
1230
+ Statement::ForOf {
1231
+ name,
1232
+ name_span,
1233
+ iterable,
1234
+ body,
1235
+ ..
1236
+ } => {
1237
+ if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1238
+ return Some(*name_span);
1239
+ }
1240
+ if let Some(s) = walk_expr_resolve(iterable, scopes, target) {
1241
+ return Some(s);
1242
+ }
1243
+ scopes.push();
1244
+ scopes.define(name.as_ref(), *name_span);
1245
+ let r = walk_stmt_implicit(body, scopes, target);
1246
+ scopes.pop();
1247
+ r
1248
+ }
1249
+ Statement::Return { value, .. } => {
1250
+ value.as_ref().and_then(|e| walk_expr_resolve(e, scopes, target))
1251
+ }
1252
+ Statement::Block { statements, .. } => {
1253
+ scopes.push();
1254
+ let mut out = None;
1255
+ for s in statements {
1256
+ if let Some(x) = walk_stmt_resolve(s, scopes, target) {
1257
+ out = Some(x);
1258
+ break;
1259
+ }
1260
+ }
1261
+ scopes.pop();
1262
+ out
1263
+ }
1264
+ Statement::FunDecl {
1265
+ name,
1266
+ name_span,
1267
+ params,
1268
+ body,
1269
+ ..
1270
+ } => {
1271
+ if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1272
+ return Some(*name_span);
1273
+ }
1274
+ scopes.push();
1275
+ scopes.define(name.as_ref(), *name_span);
1276
+ for p in params {
1277
+ define_fun_param_stack(p, scopes);
1278
+ }
1279
+ let r = walk_stmt_resolve(body, scopes, target);
1280
+ scopes.pop();
1281
+ if r.is_some() {
1282
+ return r;
1283
+ }
1284
+ scopes.define(name.as_ref(), *name_span);
1285
+ None
1286
+ }
1287
+ Statement::Switch {
1288
+ expr,
1289
+ cases,
1290
+ default_body,
1291
+ ..
1292
+ } => {
1293
+ if let Some(s) = walk_expr_resolve(expr, scopes, target) {
1294
+ return Some(s);
1295
+ }
1296
+ scopes.push();
1297
+ let mut out = None;
1298
+ for (_ce, stmts) in cases {
1299
+ for st in stmts {
1300
+ if let Some(x) = walk_stmt_resolve(st, scopes, target) {
1301
+ out = Some(x);
1302
+ break;
1303
+ }
1304
+ }
1305
+ if out.is_some() {
1306
+ break;
1307
+ }
1308
+ }
1309
+ if out.is_none() {
1310
+ if let Some(stmts) = default_body {
1311
+ for st in stmts {
1312
+ if let Some(x) = walk_stmt_resolve(st, scopes, target) {
1313
+ out = Some(x);
1314
+ break;
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1319
+ scopes.pop();
1320
+ out
1321
+ }
1322
+ Statement::DoWhile { body, cond, .. } => walk_stmt_implicit(body, scopes, target)
1323
+ .or_else(|| walk_expr_resolve(cond, scopes, target)),
1324
+ Statement::Throw { value, .. } => walk_expr_resolve(value, scopes, target),
1325
+ Statement::Try {
1326
+ body,
1327
+ catch_param,
1328
+ catch_param_span,
1329
+ catch_body,
1330
+ finally_body,
1331
+ ..
1332
+ } => {
1333
+ if let Some(s) = walk_stmt_resolve(body, scopes, target) {
1334
+ return Some(s);
1335
+ }
1336
+ if let (Some(n), Some(sp)) = (catch_param, catch_param_span) {
1337
+ if *sp == tgt_span && n.as_ref() == target.name.as_ref() {
1338
+ return Some(*sp);
1339
+ }
1340
+ }
1341
+ if let Some(cb) = catch_body {
1342
+ scopes.push();
1343
+ if let (Some(n), Some(sp)) = (catch_param, catch_param_span) {
1344
+ scopes.define(n.as_ref(), *sp);
1345
+ }
1346
+ let r = walk_stmt_resolve(cb, scopes, target);
1347
+ scopes.pop();
1348
+ if r.is_some() {
1349
+ return r;
1350
+ }
1351
+ }
1352
+ if let Some(fb) = finally_body {
1353
+ return walk_stmt_resolve(fb, scopes, target);
1354
+ }
1355
+ None
1356
+ }
1357
+ Statement::Import { specifiers, .. } => {
1358
+ for sp in specifiers {
1359
+ match sp {
1360
+ ImportSpecifier::Named {
1361
+ name,
1362
+ name_span,
1363
+ alias,
1364
+ alias_span,
1365
+ } => {
1366
+ let local = alias.as_ref().map(|a| a.clone()).unwrap_or_else(|| name.clone());
1367
+ let spn = alias_span.as_ref().unwrap_or(name_span);
1368
+ if *spn == tgt_span && local.as_ref() == target.name.as_ref() {
1369
+ return Some(*spn);
1370
+ }
1371
+ scopes.define(local.as_ref(), *spn);
1372
+ }
1373
+ ImportSpecifier::Namespace { name, name_span } => {
1374
+ if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1375
+ return Some(*name_span);
1376
+ }
1377
+ scopes.define(name.as_ref(), *name_span);
1378
+ }
1379
+ ImportSpecifier::Default { name, name_span } => {
1380
+ if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1381
+ return Some(*name_span);
1382
+ }
1383
+ scopes.define(name.as_ref(), *name_span);
1384
+ }
1385
+ }
1386
+ }
1387
+ None
1388
+ }
1389
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
1390
+ ExportDeclaration::Named(inner) => walk_stmt_resolve(inner, scopes, target),
1391
+ ExportDeclaration::Default(e) => walk_expr_resolve(e, scopes, target),
1392
+ },
1393
+ Statement::TypeAlias { name, name_span, .. } => {
1394
+ if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1395
+ return Some(*name_span);
1396
+ }
1397
+ scopes.define(name.as_ref(), *name_span);
1398
+ None
1399
+ }
1400
+ Statement::DeclareVar { name, name_span, .. } => {
1401
+ if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1402
+ return Some(*name_span);
1403
+ }
1404
+ scopes.define(name.as_ref(), *name_span);
1405
+ None
1406
+ }
1407
+ Statement::DeclareFun {
1408
+ name,
1409
+ name_span,
1410
+ params,
1411
+ ..
1412
+ } => {
1413
+ if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
1414
+ return Some(*name_span);
1415
+ }
1416
+ scopes.push();
1417
+ scopes.define(name.as_ref(), *name_span);
1418
+ for p in params {
1419
+ define_fun_param_stack(p, scopes);
1420
+ }
1421
+ scopes.pop();
1422
+ None
1423
+ }
1424
+ Statement::Break { .. } | Statement::Continue { .. } => None,
1425
+ }
1426
+ }
1427
+
1428
+ fn walk_stmt_implicit(
1429
+ stmt: &Statement,
1430
+ scopes: &mut ScopeStack,
1431
+ target: &NameUse,
1432
+ ) -> Option<tishlang_ast::Span> {
1433
+ if matches!(stmt, Statement::Block { .. }) {
1434
+ walk_stmt_resolve(stmt, scopes, target)
1435
+ } else {
1436
+ scopes.push();
1437
+ let r = walk_stmt_resolve(stmt, scopes, target);
1438
+ scopes.pop();
1439
+ r
1440
+ }
1441
+ }
1442
+
1443
+ /// Identifier reference with no binding in lexical scope (same rules as [`definition_span`]).
1444
+ #[derive(Debug, Clone, PartialEq)]
1445
+ pub struct UnresolvedIdentifier {
1446
+ pub name: Arc<str>,
1447
+ pub span: tishlang_ast::Span,
1448
+ }
1449
+
1450
+ /// Names always present on the interpreter root scope (see `tishlang_eval`) and listed in
1451
+ /// `stdlib/builtins.d.tish`. They must not produce "unresolved identifier" diagnostics when
1452
+ /// used without a `let`/`import`.
1453
+ pub fn is_runtime_global_ident(name: &str) -> bool {
1454
+ matches!(
1455
+ name,
1456
+ "console"
1457
+ | "parseInt"
1458
+ | "parseFloat"
1459
+ | "decodeURI"
1460
+ | "encodeURI"
1461
+ | "Boolean"
1462
+ | "isFinite"
1463
+ | "isNaN"
1464
+ | "Infinity"
1465
+ | "NaN"
1466
+ | "Math"
1467
+ | "JSON"
1468
+ | "Object"
1469
+ | "Array"
1470
+ | "String"
1471
+ | "Date"
1472
+ | "Uint8Array"
1473
+ | "AudioContext"
1474
+ | "RegExp"
1475
+ | "setTimeout"
1476
+ | "setInterval"
1477
+ | "clearTimeout"
1478
+ | "clearInterval"
1479
+ )
1480
+ }
1481
+
1482
+ fn record_unresolved(scopes: &ScopeStack, name: &Arc<str>, span: tishlang_ast::Span, out: &mut Vec<UnresolvedIdentifier>) {
1483
+ if is_runtime_global_ident(name.as_ref()) {
1484
+ return;
1485
+ }
1486
+ if scopes.resolve(name.as_ref()).is_none() {
1487
+ out.push(UnresolvedIdentifier {
1488
+ name: name.clone(),
1489
+ span,
1490
+ });
1491
+ }
1492
+ }
1493
+
1494
+ fn check_unresolved_expr(expr: &Expr, scopes: &ScopeStack, out: &mut Vec<UnresolvedIdentifier>) {
1495
+ match expr {
1496
+ Expr::Ident { name, span } => record_unresolved(scopes, name, *span, out),
1497
+ Expr::Assign { name, span, value } => {
1498
+ let sp = synthetic_name_span(span.start, name.as_ref());
1499
+ record_unresolved(scopes, name, sp, out);
1500
+ check_unresolved_expr(value, scopes, out);
1501
+ }
1502
+ Expr::CompoundAssign { name, span, value, .. } | Expr::LogicalAssign { name, span, value, .. } => {
1503
+ let sp = synthetic_name_span(span.start, name.as_ref());
1504
+ record_unresolved(scopes, name, sp, out);
1505
+ check_unresolved_expr(value, scopes, out);
1506
+ }
1507
+ Expr::PostfixInc { name, span } | Expr::PostfixDec { name, span } => {
1508
+ let sp = synthetic_name_span(span.start, name.as_ref());
1509
+ record_unresolved(scopes, name, sp, out);
1510
+ }
1511
+ Expr::PrefixInc { name, span } | Expr::PrefixDec { name, span } => {
1512
+ let sp = synthetic_name_span(span.start, name.as_ref());
1513
+ record_unresolved(scopes, name, sp, out);
1514
+ }
1515
+ Expr::Binary { left, right, .. } => {
1516
+ check_unresolved_expr(left, scopes, out);
1517
+ check_unresolved_expr(right, scopes, out);
1518
+ }
1519
+ Expr::Unary { operand, .. } => check_unresolved_expr(operand, scopes, out),
1520
+ Expr::Call { callee, args, .. } => {
1521
+ check_unresolved_expr(callee, scopes, out);
1522
+ for a in args {
1523
+ let e = match a {
1524
+ CallArg::Expr(e) => e,
1525
+ CallArg::Spread(e) => e,
1526
+ };
1527
+ check_unresolved_expr(e, scopes, out);
1528
+ }
1529
+ }
1530
+ Expr::New { callee, args, .. } => {
1531
+ check_unresolved_expr(callee, scopes, out);
1532
+ for a in args {
1533
+ let e = match a {
1534
+ CallArg::Expr(e) => e,
1535
+ CallArg::Spread(e) => e,
1536
+ };
1537
+ check_unresolved_expr(e, scopes, out);
1538
+ }
1539
+ }
1540
+ Expr::Member { object, prop, .. } => {
1541
+ check_unresolved_expr(object, scopes, out);
1542
+ if let tishlang_ast::MemberProp::Expr(ix) = prop {
1543
+ check_unresolved_expr(ix, scopes, out);
1544
+ }
1545
+ }
1546
+ Expr::Index { object, index, .. } => {
1547
+ check_unresolved_expr(object, scopes, out);
1548
+ check_unresolved_expr(index, scopes, out);
1549
+ }
1550
+ Expr::Conditional {
1551
+ cond,
1552
+ then_branch,
1553
+ else_branch,
1554
+ ..
1555
+ } => {
1556
+ check_unresolved_expr(cond, scopes, out);
1557
+ check_unresolved_expr(then_branch, scopes, out);
1558
+ check_unresolved_expr(else_branch, scopes, out);
1559
+ }
1560
+ Expr::NullishCoalesce { left, right, .. } => {
1561
+ check_unresolved_expr(left, scopes, out);
1562
+ check_unresolved_expr(right, scopes, out);
1563
+ }
1564
+ Expr::Array { elements, .. } => {
1565
+ for el in elements {
1566
+ let e = match el {
1567
+ tishlang_ast::ArrayElement::Expr(e) => e,
1568
+ tishlang_ast::ArrayElement::Spread(e) => e,
1569
+ };
1570
+ check_unresolved_expr(e, scopes, out);
1571
+ }
1572
+ }
1573
+ Expr::Object { props, .. } => {
1574
+ for p in props {
1575
+ let e = match p {
1576
+ tishlang_ast::ObjectProp::KeyValue(_, e) => e,
1577
+ tishlang_ast::ObjectProp::Spread(e) => e,
1578
+ };
1579
+ check_unresolved_expr(e, scopes, out);
1580
+ }
1581
+ }
1582
+ Expr::TypeOf { operand, .. } => check_unresolved_expr(operand, scopes, out),
1583
+ Expr::MemberAssign { object, value, .. } => {
1584
+ check_unresolved_expr(object, scopes, out);
1585
+ check_unresolved_expr(value, scopes, out);
1586
+ }
1587
+ Expr::IndexAssign {
1588
+ object,
1589
+ index,
1590
+ value,
1591
+ ..
1592
+ } => {
1593
+ check_unresolved_expr(object, scopes, out);
1594
+ check_unresolved_expr(index, scopes, out);
1595
+ check_unresolved_expr(value, scopes, out);
1596
+ }
1597
+ Expr::ArrowFunction { params, body, .. } => {
1598
+ let mut inner = scopes.fork();
1599
+ inner.push();
1600
+ for p in params {
1601
+ define_fun_param_stack(p, &mut inner);
1602
+ }
1603
+ match body {
1604
+ ArrowBody::Expr(e) => check_unresolved_expr(e, &inner, out),
1605
+ ArrowBody::Block(b) => check_unresolved_stmt(b, &mut inner, out),
1606
+ }
1607
+ }
1608
+ Expr::TemplateLiteral { exprs, .. } => {
1609
+ for e in exprs {
1610
+ check_unresolved_expr(e, scopes, out);
1611
+ }
1612
+ }
1613
+ Expr::Await { operand, .. } => check_unresolved_expr(operand, scopes, out),
1614
+ Expr::JsxElement { props, children, .. } => {
1615
+ for p in props {
1616
+ match p {
1617
+ tishlang_ast::JsxProp::Attr { value, .. } => {
1618
+ if let tishlang_ast::JsxAttrValue::Expr(e) = value {
1619
+ check_unresolved_expr(e, scopes, out);
1620
+ }
1621
+ }
1622
+ tishlang_ast::JsxProp::Spread(e) => check_unresolved_expr(e, scopes, out),
1623
+ }
1624
+ }
1625
+ for ch in children {
1626
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
1627
+ check_unresolved_expr(e, scopes, out);
1628
+ }
1629
+ }
1630
+ }
1631
+ Expr::JsxFragment { children, .. } => {
1632
+ for ch in children {
1633
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
1634
+ check_unresolved_expr(e, scopes, out);
1635
+ }
1636
+ }
1637
+ }
1638
+ Expr::Literal { .. } | Expr::NativeModuleLoad { .. } => {}
1639
+ }
1640
+ }
1641
+
1642
+ fn check_stmt_implicit_unresolved(stmt: &Statement, scopes: &mut ScopeStack, out: &mut Vec<UnresolvedIdentifier>) {
1643
+ if matches!(stmt, Statement::Block { .. }) {
1644
+ check_unresolved_stmt(stmt, scopes, out);
1645
+ } else {
1646
+ scopes.push();
1647
+ check_unresolved_stmt(stmt, scopes, out);
1648
+ scopes.pop();
1649
+ }
1650
+ }
1651
+
1652
+ fn check_unresolved_stmt(stmt: &Statement, scopes: &mut ScopeStack, out: &mut Vec<UnresolvedIdentifier>) {
1653
+ match stmt {
1654
+ Statement::VarDecl {
1655
+ name,
1656
+ name_span,
1657
+ init,
1658
+ ..
1659
+ } => {
1660
+ if let Some(e) = init {
1661
+ check_unresolved_expr(e, scopes, out);
1662
+ }
1663
+ scopes.define(name.as_ref(), *name_span);
1664
+ }
1665
+ Statement::VarDeclDestructure { pattern, init, .. } => {
1666
+ check_unresolved_expr(init, scopes, out);
1667
+ define_pattern_stack(pattern, scopes);
1668
+ }
1669
+ Statement::ExprStmt { expr, .. } => check_unresolved_expr(expr, scopes, out),
1670
+ Statement::If {
1671
+ cond,
1672
+ then_branch,
1673
+ else_branch,
1674
+ ..
1675
+ } => {
1676
+ check_unresolved_expr(cond, scopes, out);
1677
+ check_stmt_implicit_unresolved(then_branch, scopes, out);
1678
+ if let Some(b) = else_branch {
1679
+ check_stmt_implicit_unresolved(b, scopes, out);
1680
+ }
1681
+ }
1682
+ Statement::While { cond, body, .. } => {
1683
+ check_unresolved_expr(cond, scopes, out);
1684
+ check_stmt_implicit_unresolved(body, scopes, out);
1685
+ }
1686
+ Statement::For {
1687
+ init,
1688
+ cond,
1689
+ update,
1690
+ body,
1691
+ ..
1692
+ } => {
1693
+ scopes.push();
1694
+ if let Some(i) = init {
1695
+ check_unresolved_stmt(i, scopes, out);
1696
+ }
1697
+ if let Some(e) = cond {
1698
+ check_unresolved_expr(e, scopes, out);
1699
+ }
1700
+ if let Some(e) = update {
1701
+ check_unresolved_expr(e, scopes, out);
1702
+ }
1703
+ check_stmt_implicit_unresolved(body, scopes, out);
1704
+ scopes.pop();
1705
+ }
1706
+ Statement::ForOf {
1707
+ name,
1708
+ name_span,
1709
+ iterable,
1710
+ body,
1711
+ ..
1712
+ } => {
1713
+ check_unresolved_expr(iterable, scopes, out);
1714
+ scopes.push();
1715
+ scopes.define(name.as_ref(), *name_span);
1716
+ check_stmt_implicit_unresolved(body, scopes, out);
1717
+ scopes.pop();
1718
+ }
1719
+ Statement::Return { value, .. } => {
1720
+ if let Some(e) = value {
1721
+ check_unresolved_expr(e, scopes, out);
1722
+ }
1723
+ }
1724
+ Statement::Block { statements, .. } => {
1725
+ scopes.push();
1726
+ for s in statements {
1727
+ check_unresolved_stmt(s, scopes, out);
1728
+ }
1729
+ scopes.pop();
1730
+ }
1731
+ Statement::FunDecl {
1732
+ name,
1733
+ name_span,
1734
+ params,
1735
+ body,
1736
+ ..
1737
+ } => {
1738
+ scopes.push();
1739
+ scopes.define(name.as_ref(), *name_span);
1740
+ for p in params {
1741
+ define_fun_param_stack(p, scopes);
1742
+ }
1743
+ check_unresolved_stmt(body, scopes, out);
1744
+ scopes.pop();
1745
+ scopes.define(name.as_ref(), *name_span);
1746
+ }
1747
+ Statement::Switch {
1748
+ expr,
1749
+ cases,
1750
+ default_body,
1751
+ ..
1752
+ } => {
1753
+ check_unresolved_expr(expr, scopes, out);
1754
+ scopes.push();
1755
+ for (_ce, stmts) in cases {
1756
+ for st in stmts {
1757
+ check_unresolved_stmt(st, scopes, out);
1758
+ }
1759
+ }
1760
+ if let Some(stmts) = default_body {
1761
+ for st in stmts {
1762
+ check_unresolved_stmt(st, scopes, out);
1763
+ }
1764
+ }
1765
+ scopes.pop();
1766
+ }
1767
+ Statement::DoWhile { body, cond, .. } => {
1768
+ check_stmt_implicit_unresolved(body, scopes, out);
1769
+ check_unresolved_expr(cond, scopes, out);
1770
+ }
1771
+ Statement::Throw { value, .. } => check_unresolved_expr(value, scopes, out),
1772
+ Statement::Try {
1773
+ body,
1774
+ catch_param,
1775
+ catch_param_span,
1776
+ catch_body,
1777
+ finally_body,
1778
+ ..
1779
+ } => {
1780
+ check_unresolved_stmt(body, scopes, out);
1781
+ if let Some(cb) = catch_body {
1782
+ scopes.push();
1783
+ if let (Some(n), Some(sp)) = (catch_param, catch_param_span) {
1784
+ scopes.define(n.as_ref(), *sp);
1785
+ }
1786
+ check_unresolved_stmt(cb, scopes, out);
1787
+ scopes.pop();
1788
+ }
1789
+ if let Some(fb) = finally_body {
1790
+ check_unresolved_stmt(fb, scopes, out);
1791
+ }
1792
+ }
1793
+ Statement::Import { specifiers, .. } => {
1794
+ for sp in specifiers {
1795
+ match sp {
1796
+ ImportSpecifier::Named {
1797
+ name,
1798
+ name_span,
1799
+ alias,
1800
+ alias_span,
1801
+ } => {
1802
+ let local = alias.as_ref().map(|a| a.clone()).unwrap_or_else(|| name.clone());
1803
+ let spn = alias_span.as_ref().unwrap_or(name_span);
1804
+ scopes.define(local.as_ref(), *spn);
1805
+ }
1806
+ ImportSpecifier::Namespace { name, name_span } => {
1807
+ scopes.define(name.as_ref(), *name_span);
1808
+ }
1809
+ ImportSpecifier::Default { name, name_span } => {
1810
+ scopes.define(name.as_ref(), *name_span);
1811
+ }
1812
+ }
1813
+ }
1814
+ }
1815
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
1816
+ ExportDeclaration::Named(inner) => check_unresolved_stmt(inner, scopes, out),
1817
+ ExportDeclaration::Default(e) => check_unresolved_expr(e, scopes, out),
1818
+ },
1819
+ Statement::TypeAlias { name, name_span, .. } => {
1820
+ scopes.define(name.as_ref(), *name_span);
1821
+ }
1822
+ Statement::DeclareVar { name, name_span, .. } => {
1823
+ scopes.define(name.as_ref(), *name_span);
1824
+ }
1825
+ Statement::DeclareFun {
1826
+ name,
1827
+ name_span,
1828
+ params,
1829
+ ..
1830
+ } => {
1831
+ scopes.push();
1832
+ scopes.define(name.as_ref(), *name_span);
1833
+ for p in params {
1834
+ define_fun_param_stack(p, scopes);
1835
+ }
1836
+ scopes.pop();
1837
+ scopes.define(name.as_ref(), *name_span);
1838
+ }
1839
+ Statement::Break { .. } | Statement::Continue { .. } => {}
1840
+ }
1841
+ }
1842
+
1843
+ /// Collect unresolved simple-name references across `program` (top-level scope accumulates like `definition_span`).
1844
+ pub fn collect_unresolved_identifiers(program: &Program) -> Vec<UnresolvedIdentifier> {
1845
+ let mut out = Vec::new();
1846
+ let mut scopes = ScopeStack::new();
1847
+ for stmt in &program.statements {
1848
+ check_unresolved_stmt(stmt, &mut scopes, &mut out);
1849
+ }
1850
+ out
1851
+ }
1852
+
1853
+ /// Classify an unused binding for editor messaging (mirrors common TS/ESLint groupings).
1854
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1855
+ pub enum UnusedBindingKind {
1856
+ Import,
1857
+ Variable,
1858
+ Parameter,
1859
+ }
1860
+
1861
+ /// A declared name that is never read (same resolution rules as [`reference_spans_for_def`]).
1862
+ #[derive(Debug, Clone, PartialEq)]
1863
+ pub struct UnusedBinding {
1864
+ pub name: Arc<str>,
1865
+ pub span: tishlang_ast::Span,
1866
+ pub kind: UnusedBindingKind,
1867
+ }
1868
+
1869
+ #[derive(Debug, Clone)]
1870
+ struct BindingSite {
1871
+ name: Arc<str>,
1872
+ span: tishlang_ast::Span,
1873
+ kind: UnusedBindingKind,
1874
+ exported: bool,
1875
+ }
1876
+
1877
+ fn enumerate_pattern_bindings(
1878
+ pattern: &DestructPattern,
1879
+ kind: UnusedBindingKind,
1880
+ exported: bool,
1881
+ out: &mut Vec<BindingSite>,
1882
+ ) {
1883
+ match pattern {
1884
+ DestructPattern::Array(elements) => {
1885
+ for el in elements {
1886
+ if let Some(el) = el {
1887
+ match el {
1888
+ DestructElement::Ident(n, sp) => out.push(BindingSite {
1889
+ name: n.clone(),
1890
+ span: *sp,
1891
+ kind,
1892
+ exported,
1893
+ }),
1894
+ DestructElement::Pattern(inner) => {
1895
+ enumerate_pattern_bindings(inner, kind, exported, out);
1896
+ }
1897
+ DestructElement::Rest(n, sp) => out.push(BindingSite {
1898
+ name: n.clone(),
1899
+ span: *sp,
1900
+ kind,
1901
+ exported,
1902
+ }),
1903
+ }
1904
+ }
1905
+ }
1906
+ }
1907
+ DestructPattern::Object(props) => {
1908
+ for pr in props {
1909
+ match &pr.value {
1910
+ DestructElement::Ident(n, sp) => out.push(BindingSite {
1911
+ name: n.clone(),
1912
+ span: *sp,
1913
+ kind,
1914
+ exported,
1915
+ }),
1916
+ DestructElement::Pattern(inner) => {
1917
+ enumerate_pattern_bindings(inner, kind, exported, out);
1918
+ }
1919
+ DestructElement::Rest(n, sp) => out.push(BindingSite {
1920
+ name: n.clone(),
1921
+ span: *sp,
1922
+ kind,
1923
+ exported,
1924
+ }),
1925
+ }
1926
+ }
1927
+ }
1928
+ }
1929
+ }
1930
+
1931
+ fn enumerate_fun_param(p: &FunParam, exported: bool, out: &mut Vec<BindingSite>) {
1932
+ match p {
1933
+ FunParam::Simple(tp) => {
1934
+ out.push(BindingSite {
1935
+ name: tp.name.clone(),
1936
+ span: tp.name_span,
1937
+ kind: UnusedBindingKind::Parameter,
1938
+ exported,
1939
+ });
1940
+ if let Some(e) = &tp.default {
1941
+ enumerate_expr(e, exported, out);
1942
+ }
1943
+ }
1944
+ FunParam::Destructure {
1945
+ pattern,
1946
+ default,
1947
+ ..
1948
+ } => {
1949
+ enumerate_pattern_bindings(pattern, UnusedBindingKind::Parameter, exported, out);
1950
+ if let Some(e) = default {
1951
+ enumerate_expr(e, exported, out);
1952
+ }
1953
+ }
1954
+ }
1955
+ }
1956
+
1957
+ fn enumerate_expr(expr: &Expr, exported: bool, out: &mut Vec<BindingSite>) {
1958
+ match expr {
1959
+ Expr::Literal { .. } | Expr::Ident { .. } | Expr::NativeModuleLoad { .. } => {}
1960
+ Expr::Binary { left, right, .. } => {
1961
+ enumerate_expr(left, exported, out);
1962
+ enumerate_expr(right, exported, out);
1963
+ }
1964
+ Expr::Unary { operand, .. } => enumerate_expr(operand, exported, out),
1965
+ Expr::Call { callee, args, .. } => {
1966
+ enumerate_expr(callee, exported, out);
1967
+ for a in args {
1968
+ let e = match a {
1969
+ CallArg::Expr(e) => e,
1970
+ CallArg::Spread(e) => e,
1971
+ };
1972
+ enumerate_expr(e, exported, out);
1973
+ }
1974
+ }
1975
+ Expr::New { callee, args, .. } => {
1976
+ enumerate_expr(callee, exported, out);
1977
+ for a in args {
1978
+ let e = match a {
1979
+ CallArg::Expr(e) => e,
1980
+ CallArg::Spread(e) => e,
1981
+ };
1982
+ enumerate_expr(e, exported, out);
1983
+ }
1984
+ }
1985
+ Expr::Member { object, prop, .. } => {
1986
+ enumerate_expr(object, exported, out);
1987
+ if let MemberProp::Expr(ix) = prop {
1988
+ enumerate_expr(ix, exported, out);
1989
+ }
1990
+ }
1991
+ Expr::Index { object, index, .. } => {
1992
+ enumerate_expr(object, exported, out);
1993
+ enumerate_expr(index, exported, out);
1994
+ }
1995
+ Expr::Conditional {
1996
+ cond,
1997
+ then_branch,
1998
+ else_branch,
1999
+ ..
2000
+ } => {
2001
+ enumerate_expr(cond, exported, out);
2002
+ enumerate_expr(then_branch, exported, out);
2003
+ enumerate_expr(else_branch, exported, out);
2004
+ }
2005
+ Expr::NullishCoalesce { left, right, .. } => {
2006
+ enumerate_expr(left, exported, out);
2007
+ enumerate_expr(right, exported, out);
2008
+ }
2009
+ Expr::Array { elements, .. } => {
2010
+ for el in elements {
2011
+ let e = match el {
2012
+ tishlang_ast::ArrayElement::Expr(e) => e,
2013
+ tishlang_ast::ArrayElement::Spread(e) => e,
2014
+ };
2015
+ enumerate_expr(e, exported, out);
2016
+ }
2017
+ }
2018
+ Expr::Object { props, .. } => {
2019
+ for p in props {
2020
+ let e = match p {
2021
+ tishlang_ast::ObjectProp::KeyValue(_, e) => e,
2022
+ tishlang_ast::ObjectProp::Spread(e) => e,
2023
+ };
2024
+ enumerate_expr(e, exported, out);
2025
+ }
2026
+ }
2027
+ Expr::Assign { value, .. }
2028
+ | Expr::CompoundAssign { value, .. }
2029
+ | Expr::LogicalAssign { value, .. } => {
2030
+ enumerate_expr(value, exported, out);
2031
+ }
2032
+ Expr::PostfixInc { .. }
2033
+ | Expr::PostfixDec { .. }
2034
+ | Expr::PrefixInc { .. }
2035
+ | Expr::PrefixDec { .. } => {}
2036
+ Expr::TypeOf { operand, .. } => enumerate_expr(operand, exported, out),
2037
+ Expr::MemberAssign { object, value, .. } => {
2038
+ enumerate_expr(object, exported, out);
2039
+ enumerate_expr(value, exported, out);
2040
+ }
2041
+ Expr::IndexAssign {
2042
+ object,
2043
+ index,
2044
+ value,
2045
+ ..
2046
+ } => {
2047
+ enumerate_expr(object, exported, out);
2048
+ enumerate_expr(index, exported, out);
2049
+ enumerate_expr(value, exported, out);
2050
+ }
2051
+ Expr::ArrowFunction { params, body, .. } => {
2052
+ for p in params {
2053
+ enumerate_fun_param(p, exported, out);
2054
+ }
2055
+ match body {
2056
+ ArrowBody::Expr(e) => enumerate_expr(e, exported, out),
2057
+ ArrowBody::Block(b) => enumerate_stmt(b, exported, out),
2058
+ }
2059
+ }
2060
+ Expr::TemplateLiteral { exprs, .. } => {
2061
+ for e in exprs {
2062
+ enumerate_expr(e, exported, out);
2063
+ }
2064
+ }
2065
+ Expr::Await { operand, .. } => enumerate_expr(operand, exported, out),
2066
+ Expr::JsxElement { props, children, .. } => {
2067
+ for p in props {
2068
+ match p {
2069
+ tishlang_ast::JsxProp::Attr { value, .. } => {
2070
+ if let tishlang_ast::JsxAttrValue::Expr(e) = value {
2071
+ enumerate_expr(e, exported, out);
2072
+ }
2073
+ }
2074
+ tishlang_ast::JsxProp::Spread(e) => enumerate_expr(e, exported, out),
2075
+ }
2076
+ }
2077
+ for ch in children {
2078
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
2079
+ enumerate_expr(e, exported, out);
2080
+ }
2081
+ }
2082
+ }
2083
+ Expr::JsxFragment { children, .. } => {
2084
+ for ch in children {
2085
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
2086
+ enumerate_expr(e, exported, out);
2087
+ }
2088
+ }
2089
+ }
2090
+ }
2091
+ }
2092
+
2093
+ fn enumerate_stmt(stmt: &Statement, exported: bool, out: &mut Vec<BindingSite>) {
2094
+ match stmt {
2095
+ Statement::Import { specifiers, .. } => {
2096
+ for sp in specifiers {
2097
+ match sp {
2098
+ ImportSpecifier::Named {
2099
+ name,
2100
+ name_span,
2101
+ alias,
2102
+ alias_span,
2103
+ } => {
2104
+ let local = alias.as_ref().map(|a| a.clone()).unwrap_or_else(|| name.clone());
2105
+ let spn = alias_span.as_ref().unwrap_or(name_span);
2106
+ out.push(BindingSite {
2107
+ name: local,
2108
+ span: *spn,
2109
+ kind: UnusedBindingKind::Import,
2110
+ exported: false,
2111
+ });
2112
+ }
2113
+ ImportSpecifier::Namespace { name, name_span } => {
2114
+ out.push(BindingSite {
2115
+ name: name.clone(),
2116
+ span: *name_span,
2117
+ kind: UnusedBindingKind::Import,
2118
+ exported: false,
2119
+ });
2120
+ }
2121
+ ImportSpecifier::Default { name, name_span } => {
2122
+ out.push(BindingSite {
2123
+ name: name.clone(),
2124
+ span: *name_span,
2125
+ kind: UnusedBindingKind::Import,
2126
+ exported: false,
2127
+ });
2128
+ }
2129
+ }
2130
+ }
2131
+ }
2132
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
2133
+ ExportDeclaration::Named(inner) => enumerate_stmt(inner, true, out),
2134
+ ExportDeclaration::Default(e) => enumerate_expr(e, exported, out),
2135
+ },
2136
+ Statement::VarDecl {
2137
+ name,
2138
+ name_span,
2139
+ init,
2140
+ ..
2141
+ } => {
2142
+ out.push(BindingSite {
2143
+ name: name.clone(),
2144
+ span: *name_span,
2145
+ kind: UnusedBindingKind::Variable,
2146
+ exported,
2147
+ });
2148
+ if let Some(e) = init {
2149
+ enumerate_expr(e, exported, out);
2150
+ }
2151
+ }
2152
+ Statement::VarDeclDestructure { pattern, init, .. } => {
2153
+ enumerate_pattern_bindings(pattern, UnusedBindingKind::Variable, exported, out);
2154
+ enumerate_expr(init, exported, out);
2155
+ }
2156
+ Statement::ExprStmt { expr, .. } => enumerate_expr(expr, exported, out),
2157
+ Statement::If {
2158
+ cond,
2159
+ then_branch,
2160
+ else_branch,
2161
+ ..
2162
+ } => {
2163
+ enumerate_expr(cond, exported, out);
2164
+ enumerate_stmt(then_branch, exported, out);
2165
+ if let Some(b) = else_branch {
2166
+ enumerate_stmt(b, exported, out);
2167
+ }
2168
+ }
2169
+ Statement::While { cond, body, .. } => {
2170
+ enumerate_expr(cond, exported, out);
2171
+ enumerate_stmt(body, exported, out);
2172
+ }
2173
+ Statement::For {
2174
+ init,
2175
+ cond,
2176
+ update,
2177
+ body,
2178
+ ..
2179
+ } => {
2180
+ if let Some(i) = init {
2181
+ enumerate_stmt(i, exported, out);
2182
+ }
2183
+ if let Some(e) = cond {
2184
+ enumerate_expr(e, exported, out);
2185
+ }
2186
+ if let Some(e) = update {
2187
+ enumerate_expr(e, exported, out);
2188
+ }
2189
+ enumerate_stmt(body, exported, out);
2190
+ }
2191
+ Statement::ForOf {
2192
+ name,
2193
+ name_span,
2194
+ iterable,
2195
+ body,
2196
+ ..
2197
+ } => {
2198
+ out.push(BindingSite {
2199
+ name: name.clone(),
2200
+ span: *name_span,
2201
+ kind: UnusedBindingKind::Variable,
2202
+ exported,
2203
+ });
2204
+ enumerate_expr(iterable, exported, out);
2205
+ enumerate_stmt(body, exported, out);
2206
+ }
2207
+ Statement::Return { value, .. } => {
2208
+ if let Some(e) = value {
2209
+ enumerate_expr(e, exported, out);
2210
+ }
2211
+ }
2212
+ Statement::Block { statements, .. } => {
2213
+ for s in statements {
2214
+ enumerate_stmt(s, exported, out);
2215
+ }
2216
+ }
2217
+ Statement::FunDecl {
2218
+ name,
2219
+ name_span,
2220
+ params,
2221
+ rest_param,
2222
+ body,
2223
+ ..
2224
+ } => {
2225
+ out.push(BindingSite {
2226
+ name: name.clone(),
2227
+ span: *name_span,
2228
+ kind: UnusedBindingKind::Variable,
2229
+ exported,
2230
+ });
2231
+ for p in params {
2232
+ enumerate_fun_param(p, exported, out);
2233
+ }
2234
+ if let Some(r) = rest_param {
2235
+ out.push(BindingSite {
2236
+ name: r.name.clone(),
2237
+ span: r.name_span,
2238
+ kind: UnusedBindingKind::Parameter,
2239
+ exported,
2240
+ });
2241
+ }
2242
+ enumerate_stmt(body, exported, out);
2243
+ }
2244
+ Statement::Switch {
2245
+ expr,
2246
+ cases,
2247
+ default_body,
2248
+ ..
2249
+ } => {
2250
+ enumerate_expr(expr, exported, out);
2251
+ for (ce, stmts) in cases {
2252
+ if let Some(e) = ce {
2253
+ enumerate_expr(e, exported, out);
2254
+ }
2255
+ for st in stmts {
2256
+ enumerate_stmt(st, exported, out);
2257
+ }
2258
+ }
2259
+ if let Some(stmts) = default_body {
2260
+ for st in stmts {
2261
+ enumerate_stmt(st, exported, out);
2262
+ }
2263
+ }
2264
+ }
2265
+ Statement::DoWhile { body, cond, .. } => {
2266
+ enumerate_stmt(body, exported, out);
2267
+ enumerate_expr(cond, exported, out);
2268
+ }
2269
+ Statement::Throw { value, .. } => enumerate_expr(value, exported, out),
2270
+ Statement::Try {
2271
+ body,
2272
+ catch_param,
2273
+ catch_param_span,
2274
+ catch_body,
2275
+ finally_body,
2276
+ ..
2277
+ } => {
2278
+ enumerate_stmt(body, exported, out);
2279
+ if let (Some(n), Some(sp), Some(cb)) = (catch_param, catch_param_span, catch_body) {
2280
+ out.push(BindingSite {
2281
+ name: n.clone(),
2282
+ span: *sp,
2283
+ kind: UnusedBindingKind::Variable,
2284
+ exported,
2285
+ });
2286
+ enumerate_stmt(cb, exported, out);
2287
+ }
2288
+ if let Some(fb) = finally_body {
2289
+ enumerate_stmt(fb, exported, out);
2290
+ }
2291
+ }
2292
+ Statement::TypeAlias { name, name_span, .. } => {
2293
+ out.push(BindingSite {
2294
+ name: name.clone(),
2295
+ span: *name_span,
2296
+ kind: UnusedBindingKind::Variable,
2297
+ exported,
2298
+ });
2299
+ }
2300
+ Statement::DeclareVar { name, name_span, .. } => {
2301
+ out.push(BindingSite {
2302
+ name: name.clone(),
2303
+ span: *name_span,
2304
+ kind: UnusedBindingKind::Variable,
2305
+ exported,
2306
+ });
2307
+ }
2308
+ Statement::DeclareFun {
2309
+ name,
2310
+ name_span,
2311
+ params,
2312
+ rest_param,
2313
+ ..
2314
+ } => {
2315
+ out.push(BindingSite {
2316
+ name: name.clone(),
2317
+ span: *name_span,
2318
+ kind: UnusedBindingKind::Variable,
2319
+ exported,
2320
+ });
2321
+ for p in params {
2322
+ enumerate_fun_param(p, exported, out);
2323
+ }
2324
+ if let Some(r) = rest_param {
2325
+ out.push(BindingSite {
2326
+ name: r.name.clone(),
2327
+ span: r.name_span,
2328
+ kind: UnusedBindingKind::Parameter,
2329
+ exported,
2330
+ });
2331
+ }
2332
+ }
2333
+ Statement::Break { .. } | Statement::Continue { .. } => {}
2334
+ }
2335
+ }
2336
+
2337
+ /// Declarations whose values are never read (imports, locals, parameters). Skips `exported` module
2338
+ /// bindings and names starting with `_` (common intentional-unused convention).
2339
+ pub fn collect_unused_bindings(program: &Program, source: &str) -> Vec<UnusedBinding> {
2340
+ let mut sites = Vec::new();
2341
+ for s in &program.statements {
2342
+ enumerate_stmt(s, false, &mut sites);
2343
+ }
2344
+ let mut out = Vec::new();
2345
+ for site in sites {
2346
+ if site.exported || site.name.as_ref().starts_with('_') {
2347
+ continue;
2348
+ }
2349
+ let refs = reference_spans_for_def(program, source, site.name.as_ref(), site.span);
2350
+ if refs.len() == 1 {
2351
+ out.push(UnusedBinding {
2352
+ name: site.name,
2353
+ span: site.span,
2354
+ kind: site.kind,
2355
+ });
2356
+ }
2357
+ }
2358
+ out
2359
+ }
2360
+
2361
+ /// Resolve go-to-definition for cursor position. Returns the defining [`tishlang_ast::Span`].
2362
+ pub fn definition_span(
2363
+ program: &Program,
2364
+ source: &str,
2365
+ lsp_line: u32,
2366
+ lsp_character: u32,
2367
+ ) -> Option<tishlang_ast::Span> {
2368
+ let use_site = name_at_cursor(program, source, lsp_line, lsp_character)?;
2369
+ let mut scopes = ScopeStack::new();
2370
+ for stmt in &program.statements {
2371
+ if let Some(s) = walk_stmt_resolve(stmt, &mut scopes, &use_site) {
2372
+ return Some(s);
2373
+ }
2374
+ }
2375
+ None
2376
+ }
2377
+
2378
+ /// Locals declared one level inside the innermost `Block` that contains the cursor (best-effort completion).
2379
+ pub fn block_locals_containing_cursor(
2380
+ program: &Program,
2381
+ source: &str,
2382
+ lsp_line: u32,
2383
+ lsp_character: u32,
2384
+ ) -> Vec<(Arc<str>, tishlang_ast::Span)> {
2385
+ let mut out = Vec::new();
2386
+ for s in &program.statements {
2387
+ collect_block_locals(s, source, lsp_line, lsp_character, &mut out);
2388
+ }
2389
+ out
2390
+ }
2391
+
2392
+ fn collect_block_locals(
2393
+ stmt: &Statement,
2394
+ source: &str,
2395
+ lsp_line: u32,
2396
+ lsp_char: u32,
2397
+ out: &mut Vec<(Arc<str>, tishlang_ast::Span)>,
2398
+ ) {
2399
+ match stmt {
2400
+ Statement::Block { statements, span } => {
2401
+ if pos::span_contains_lsp_position(source, span, lsp_line, lsp_char) {
2402
+ for st in statements {
2403
+ match st {
2404
+ Statement::VarDecl {
2405
+ name,
2406
+ name_span,
2407
+ ..
2408
+ } => out.push((name.clone(), *name_span)),
2409
+ Statement::FunDecl {
2410
+ name,
2411
+ name_span,
2412
+ ..
2413
+ } => out.push((name.clone(), *name_span)),
2414
+ Statement::TypeAlias {
2415
+ name,
2416
+ name_span,
2417
+ ..
2418
+ } => out.push((name.clone(), *name_span)),
2419
+ Statement::DeclareVar {
2420
+ name,
2421
+ name_span,
2422
+ ..
2423
+ } => out.push((name.clone(), *name_span)),
2424
+ Statement::DeclareFun {
2425
+ name,
2426
+ name_span,
2427
+ ..
2428
+ } => out.push((name.clone(), *name_span)),
2429
+ _ => {}
2430
+ }
2431
+ }
2432
+ }
2433
+ for st in statements {
2434
+ collect_block_locals(st, source, lsp_line, lsp_char, out);
2435
+ }
2436
+ }
2437
+ Statement::FunDecl { body, .. } | Statement::While { body, .. } => {
2438
+ collect_block_locals(body, source, lsp_line, lsp_char, out);
2439
+ }
2440
+ Statement::ForOf {
2441
+ name,
2442
+ name_span,
2443
+ body,
2444
+ span,
2445
+ ..
2446
+ } => {
2447
+ if pos::span_contains_lsp_position(source, span, lsp_line, lsp_char)
2448
+ && pos::span_contains_lsp_position(source, &body.as_ref().span(), lsp_line, lsp_char)
2449
+ {
2450
+ out.push((name.clone(), *name_span));
2451
+ }
2452
+ collect_block_locals(body, source, lsp_line, lsp_char, out);
2453
+ }
2454
+ Statement::If {
2455
+ then_branch,
2456
+ else_branch,
2457
+ ..
2458
+ } => {
2459
+ collect_block_locals(then_branch, source, lsp_line, lsp_char, out);
2460
+ if let Some(b) = else_branch {
2461
+ collect_block_locals(b, source, lsp_line, lsp_char, out);
2462
+ }
2463
+ }
2464
+ Statement::For { init, body, .. } => {
2465
+ if let Some(i) = init {
2466
+ collect_block_locals(i, source, lsp_line, lsp_char, out);
2467
+ }
2468
+ collect_block_locals(body, source, lsp_line, lsp_char, out);
2469
+ }
2470
+ Statement::DoWhile { body, .. } => collect_block_locals(body, source, lsp_line, lsp_char, out),
2471
+ Statement::Try {
2472
+ body,
2473
+ catch_param,
2474
+ catch_param_span,
2475
+ catch_body,
2476
+ finally_body,
2477
+ ..
2478
+ } => {
2479
+ collect_block_locals(body, source, lsp_line, lsp_char, out);
2480
+ if let (Some(n), Some(ps), Some(cb)) = (catch_param, catch_param_span, catch_body) {
2481
+ if pos::span_contains_lsp_position(source, &cb.as_ref().span(), lsp_line, lsp_char) {
2482
+ out.push((n.clone(), *ps));
2483
+ }
2484
+ collect_block_locals(cb, source, lsp_line, lsp_char, out);
2485
+ }
2486
+ if let Some(fb) = finally_body {
2487
+ collect_block_locals(fb, source, lsp_line, lsp_char, out);
2488
+ }
2489
+ }
2490
+ Statement::Switch { cases, default_body, .. } => {
2491
+ for (_ce, stmts) in cases {
2492
+ for st in stmts {
2493
+ collect_block_locals(st, source, lsp_line, lsp_char, out);
2494
+ }
2495
+ }
2496
+ if let Some(stmts) = default_body {
2497
+ for st in stmts {
2498
+ collect_block_locals(st, source, lsp_line, lsp_char, out);
2499
+ }
2500
+ }
2501
+ }
2502
+ Statement::Export { declaration, .. } => {
2503
+ if let ExportDeclaration::Named(inner) = declaration.as_ref() {
2504
+ collect_block_locals(inner, source, lsp_line, lsp_char, out);
2505
+ }
2506
+ }
2507
+ _ => {}
2508
+ }
2509
+ }
2510
+
2511
+ fn param_layer_names(params: &[FunParam], rest_param: &Option<TypedParam>) -> Vec<Arc<str>> {
2512
+ let mut v = Vec::new();
2513
+ for p in params {
2514
+ match p {
2515
+ FunParam::Simple(tp) => v.push(tp.name.clone()),
2516
+ FunParam::Destructure { pattern, .. } => collect_pattern_binding_names(pattern, &mut v),
2517
+ }
2518
+ }
2519
+ if let Some(r) = rest_param {
2520
+ v.push(r.name.clone());
2521
+ }
2522
+ v
2523
+ }
2524
+
2525
+ fn collect_pattern_binding_names(pattern: &DestructPattern, out: &mut Vec<Arc<str>>) {
2526
+ match pattern {
2527
+ DestructPattern::Array(elements) => {
2528
+ for el in elements {
2529
+ if let Some(el) = el {
2530
+ match el {
2531
+ DestructElement::Ident(n, _) => out.push(n.clone()),
2532
+ DestructElement::Pattern(inner) => collect_pattern_binding_names(inner, out),
2533
+ DestructElement::Rest(n, _) => out.push(n.clone()),
2534
+ }
2535
+ }
2536
+ }
2537
+ }
2538
+ DestructPattern::Object(props) => {
2539
+ for pr in props {
2540
+ match &pr.value {
2541
+ DestructElement::Ident(n, _) => out.push(n.clone()),
2542
+ DestructElement::Pattern(inner) => collect_pattern_binding_names(inner, out),
2543
+ DestructElement::Rest(n, _) => out.push(n.clone()),
2544
+ }
2545
+ }
2546
+ }
2547
+ }
2548
+ }
2549
+
2550
+ fn record_callable_stack(
2551
+ stack: &[Vec<Arc<str>>],
2552
+ best: &mut Option<(usize, Vec<Arc<str>>)>,
2553
+ ) {
2554
+ let depth = stack.len();
2555
+ let flat: Vec<Arc<str>> = stack
2556
+ .iter()
2557
+ .rev()
2558
+ .flat_map(|layer| layer.iter().cloned())
2559
+ .collect();
2560
+ match best {
2561
+ None => *best = Some((depth, flat)),
2562
+ Some((bd, _)) if depth > *bd => *best = Some((depth, flat)),
2563
+ _ => {}
2564
+ }
2565
+ }
2566
+
2567
+ fn walk_expr_completion(
2568
+ expr: &Expr,
2569
+ source: &str,
2570
+ lsp_line: u32,
2571
+ lsp_char: u32,
2572
+ stack: &mut Vec<Vec<Arc<str>>>,
2573
+ best: &mut Option<(usize, Vec<Arc<str>>)>,
2574
+ ) {
2575
+ let sp = expr.span();
2576
+ if !pos::span_contains_lsp_position(source, &sp, lsp_line, lsp_char) {
2577
+ return;
2578
+ }
2579
+ match expr {
2580
+ Expr::ArrowFunction { params, body, .. } => {
2581
+ let layer = param_layer_names(params, &None);
2582
+ stack.push(layer);
2583
+ record_callable_stack(stack, best);
2584
+ for p in params {
2585
+ match p {
2586
+ FunParam::Simple(tp) => {
2587
+ if let Some(e) = &tp.default {
2588
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2589
+ }
2590
+ }
2591
+ FunParam::Destructure { default: Some(e), .. } => {
2592
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2593
+ }
2594
+ _ => {}
2595
+ }
2596
+ }
2597
+ match body {
2598
+ ArrowBody::Expr(e) => walk_expr_completion(e, source, lsp_line, lsp_char, stack, best),
2599
+ ArrowBody::Block(b) => {
2600
+ walk_stmt_completion(b, source, lsp_line, lsp_char, stack, best);
2601
+ }
2602
+ }
2603
+ stack.pop();
2604
+ }
2605
+ Expr::Binary { left, right, .. } => {
2606
+ walk_expr_completion(left, source, lsp_line, lsp_char, stack, best);
2607
+ walk_expr_completion(right, source, lsp_line, lsp_char, stack, best);
2608
+ }
2609
+ Expr::Unary { operand, .. } => walk_expr_completion(operand, source, lsp_line, lsp_char, stack, best),
2610
+ Expr::Call { callee, args, .. } => {
2611
+ walk_expr_completion(callee, source, lsp_line, lsp_char, stack, best);
2612
+ for a in args {
2613
+ let e = match a {
2614
+ CallArg::Expr(e) => e,
2615
+ CallArg::Spread(e) => e,
2616
+ };
2617
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2618
+ }
2619
+ }
2620
+ Expr::New { callee, args, .. } => {
2621
+ walk_expr_completion(callee, source, lsp_line, lsp_char, stack, best);
2622
+ for a in args {
2623
+ let e = match a {
2624
+ CallArg::Expr(e) => e,
2625
+ CallArg::Spread(e) => e,
2626
+ };
2627
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2628
+ }
2629
+ }
2630
+ Expr::Member { object, .. } => walk_expr_completion(object, source, lsp_line, lsp_char, stack, best),
2631
+ Expr::Index { object, index, .. } => {
2632
+ walk_expr_completion(object, source, lsp_line, lsp_char, stack, best);
2633
+ walk_expr_completion(index, source, lsp_line, lsp_char, stack, best);
2634
+ }
2635
+ Expr::Conditional {
2636
+ cond,
2637
+ then_branch,
2638
+ else_branch,
2639
+ ..
2640
+ } => {
2641
+ walk_expr_completion(cond, source, lsp_line, lsp_char, stack, best);
2642
+ walk_expr_completion(then_branch, source, lsp_line, lsp_char, stack, best);
2643
+ walk_expr_completion(else_branch, source, lsp_line, lsp_char, stack, best);
2644
+ }
2645
+ Expr::NullishCoalesce { left, right, .. } => {
2646
+ walk_expr_completion(left, source, lsp_line, lsp_char, stack, best);
2647
+ walk_expr_completion(right, source, lsp_line, lsp_char, stack, best);
2648
+ }
2649
+ Expr::Array { elements, .. } => {
2650
+ for el in elements {
2651
+ let e = match el {
2652
+ tishlang_ast::ArrayElement::Expr(e) => e,
2653
+ tishlang_ast::ArrayElement::Spread(e) => e,
2654
+ };
2655
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2656
+ }
2657
+ }
2658
+ Expr::Object { props, .. } => {
2659
+ for p in props {
2660
+ let e = match p {
2661
+ tishlang_ast::ObjectProp::KeyValue(_, e) => e,
2662
+ tishlang_ast::ObjectProp::Spread(e) => e,
2663
+ };
2664
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2665
+ }
2666
+ }
2667
+ Expr::Assign { value, .. }
2668
+ | Expr::CompoundAssign { value, .. }
2669
+ | Expr::LogicalAssign { value, .. } => {
2670
+ walk_expr_completion(value, source, lsp_line, lsp_char, stack, best);
2671
+ }
2672
+ Expr::TypeOf { operand, .. } => walk_expr_completion(operand, source, lsp_line, lsp_char, stack, best),
2673
+ Expr::MemberAssign { object, value, .. } => {
2674
+ walk_expr_completion(object, source, lsp_line, lsp_char, stack, best);
2675
+ walk_expr_completion(value, source, lsp_line, lsp_char, stack, best);
2676
+ }
2677
+ Expr::IndexAssign {
2678
+ object,
2679
+ index,
2680
+ value,
2681
+ ..
2682
+ } => {
2683
+ walk_expr_completion(object, source, lsp_line, lsp_char, stack, best);
2684
+ walk_expr_completion(index, source, lsp_line, lsp_char, stack, best);
2685
+ walk_expr_completion(value, source, lsp_line, lsp_char, stack, best);
2686
+ }
2687
+ Expr::TemplateLiteral { exprs, .. } => {
2688
+ for e in exprs {
2689
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2690
+ }
2691
+ }
2692
+ Expr::Await { operand, .. } => walk_expr_completion(operand, source, lsp_line, lsp_char, stack, best),
2693
+ Expr::JsxElement { props, children, .. } => {
2694
+ for p in props {
2695
+ match p {
2696
+ tishlang_ast::JsxProp::Attr { value, .. } => {
2697
+ if let tishlang_ast::JsxAttrValue::Expr(e) = value {
2698
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2699
+ }
2700
+ }
2701
+ tishlang_ast::JsxProp::Spread(e) => {
2702
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2703
+ }
2704
+ }
2705
+ }
2706
+ for ch in children {
2707
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
2708
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2709
+ }
2710
+ }
2711
+ }
2712
+ Expr::JsxFragment { children, .. } => {
2713
+ for ch in children {
2714
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
2715
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2716
+ }
2717
+ }
2718
+ }
2719
+ Expr::Ident { .. }
2720
+ | Expr::Literal { .. }
2721
+ | Expr::PostfixInc { .. }
2722
+ | Expr::PostfixDec { .. }
2723
+ | Expr::PrefixInc { .. }
2724
+ | Expr::PrefixDec { .. }
2725
+ | Expr::NativeModuleLoad { .. } => {}
2726
+ }
2727
+ }
2728
+
2729
+ fn walk_stmt_completion(
2730
+ stmt: &Statement,
2731
+ source: &str,
2732
+ lsp_line: u32,
2733
+ lsp_char: u32,
2734
+ stack: &mut Vec<Vec<Arc<str>>>,
2735
+ best: &mut Option<(usize, Vec<Arc<str>>)>,
2736
+ ) {
2737
+ let st_span = stmt.span();
2738
+ if !pos::span_contains_lsp_position(source, &st_span, lsp_line, lsp_char) {
2739
+ return;
2740
+ }
2741
+ match stmt {
2742
+ Statement::VarDecl { init: Some(e), .. } => {
2743
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2744
+ }
2745
+ Statement::VarDecl { .. } => {}
2746
+ Statement::VarDeclDestructure { init, .. } => {
2747
+ walk_expr_completion(init, source, lsp_line, lsp_char, stack, best);
2748
+ }
2749
+ Statement::ExprStmt { expr, .. } => {
2750
+ walk_expr_completion(expr, source, lsp_line, lsp_char, stack, best);
2751
+ }
2752
+ Statement::If {
2753
+ cond,
2754
+ then_branch,
2755
+ else_branch,
2756
+ ..
2757
+ } => {
2758
+ walk_expr_completion(cond, source, lsp_line, lsp_char, stack, best);
2759
+ walk_stmt_completion(then_branch, source, lsp_line, lsp_char, stack, best);
2760
+ if let Some(b) = else_branch {
2761
+ walk_stmt_completion(b, source, lsp_line, lsp_char, stack, best);
2762
+ }
2763
+ }
2764
+ Statement::While { cond, body, .. } => {
2765
+ walk_expr_completion(cond, source, lsp_line, lsp_char, stack, best);
2766
+ walk_stmt_completion(body, source, lsp_line, lsp_char, stack, best);
2767
+ }
2768
+ Statement::For {
2769
+ init,
2770
+ cond,
2771
+ update,
2772
+ body,
2773
+ ..
2774
+ } => {
2775
+ if let Some(i) = init {
2776
+ walk_stmt_completion(i, source, lsp_line, lsp_char, stack, best);
2777
+ }
2778
+ if let Some(e) = cond {
2779
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2780
+ }
2781
+ if let Some(e) = update {
2782
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2783
+ }
2784
+ walk_stmt_completion(body, source, lsp_line, lsp_char, stack, best);
2785
+ }
2786
+ Statement::ForOf {
2787
+ name,
2788
+ iterable,
2789
+ body,
2790
+ ..
2791
+ } => {
2792
+ walk_expr_completion(iterable, source, lsp_line, lsp_char, stack, best);
2793
+ let body_sp = body.as_ref().span();
2794
+ if pos::span_contains_lsp_position(source, &body_sp, lsp_line, lsp_char) {
2795
+ stack.push(vec![name.clone()]);
2796
+ record_callable_stack(stack, best);
2797
+ walk_stmt_completion(body, source, lsp_line, lsp_char, stack, best);
2798
+ stack.pop();
2799
+ }
2800
+ }
2801
+ Statement::Return { value: Some(e), .. } => {
2802
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2803
+ }
2804
+ Statement::Return { .. } => {}
2805
+ Statement::Block { statements, .. } => {
2806
+ for s in statements {
2807
+ walk_stmt_completion(s, source, lsp_line, lsp_char, stack, best);
2808
+ }
2809
+ }
2810
+ Statement::FunDecl {
2811
+ params,
2812
+ rest_param,
2813
+ body,
2814
+ ..
2815
+ } => {
2816
+ let layer = param_layer_names(params, rest_param);
2817
+ stack.push(layer);
2818
+ record_callable_stack(stack, best);
2819
+ for p in params {
2820
+ match p {
2821
+ FunParam::Simple(tp) => {
2822
+ if let Some(e) = &tp.default {
2823
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2824
+ }
2825
+ }
2826
+ FunParam::Destructure { default: Some(e), .. } => {
2827
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2828
+ }
2829
+ _ => {}
2830
+ }
2831
+ }
2832
+ walk_stmt_completion(body, source, lsp_line, lsp_char, stack, best);
2833
+ stack.pop();
2834
+ }
2835
+ Statement::Switch {
2836
+ expr,
2837
+ cases,
2838
+ default_body,
2839
+ ..
2840
+ } => {
2841
+ walk_expr_completion(expr, source, lsp_line, lsp_char, stack, best);
2842
+ for (_ce, stmts) in cases {
2843
+ for s in stmts {
2844
+ walk_stmt_completion(s, source, lsp_line, lsp_char, stack, best);
2845
+ }
2846
+ }
2847
+ if let Some(stmts) = default_body {
2848
+ for s in stmts {
2849
+ walk_stmt_completion(s, source, lsp_line, lsp_char, stack, best);
2850
+ }
2851
+ }
2852
+ }
2853
+ Statement::DoWhile { body, cond, .. } => {
2854
+ walk_stmt_completion(body, source, lsp_line, lsp_char, stack, best);
2855
+ walk_expr_completion(cond, source, lsp_line, lsp_char, stack, best);
2856
+ }
2857
+ Statement::Throw { value, .. } => {
2858
+ walk_expr_completion(value, source, lsp_line, lsp_char, stack, best);
2859
+ }
2860
+ Statement::Try {
2861
+ body,
2862
+ catch_param,
2863
+ catch_body,
2864
+ finally_body,
2865
+ ..
2866
+ } => {
2867
+ walk_stmt_completion(body, source, lsp_line, lsp_char, stack, best);
2868
+ if let (Some(n), Some(cb)) = (catch_param, catch_body) {
2869
+ let csp = cb.as_ref().span();
2870
+ if pos::span_contains_lsp_position(source, &csp, lsp_line, lsp_char) {
2871
+ stack.push(vec![n.clone()]);
2872
+ record_callable_stack(stack, best);
2873
+ walk_stmt_completion(cb, source, lsp_line, lsp_char, stack, best);
2874
+ stack.pop();
2875
+ }
2876
+ }
2877
+ if let Some(fb) = finally_body {
2878
+ walk_stmt_completion(fb, source, lsp_line, lsp_char, stack, best);
2879
+ }
2880
+ }
2881
+ Statement::Import { .. }
2882
+ | Statement::Break { .. }
2883
+ | Statement::Continue { .. }
2884
+ | Statement::TypeAlias { .. }
2885
+ | Statement::DeclareVar { .. }
2886
+ | Statement::DeclareFun { .. } => {}
2887
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
2888
+ ExportDeclaration::Named(inner) => {
2889
+ walk_stmt_completion(inner, source, lsp_line, lsp_char, stack, best);
2890
+ }
2891
+ ExportDeclaration::Default(e) => {
2892
+ walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
2893
+ }
2894
+ },
2895
+ }
2896
+ }
2897
+
2898
+ /// Callable parameter names visible at the cursor (innermost layer first in the returned `Vec`).
2899
+ pub fn callable_param_names_at_cursor(
2900
+ program: &Program,
2901
+ source: &str,
2902
+ lsp_line: u32,
2903
+ lsp_char: u32,
2904
+ ) -> Vec<Arc<str>> {
2905
+ let mut best: Option<(usize, Vec<Arc<str>>)> = None;
2906
+ let mut stack: Vec<Vec<Arc<str>>> = Vec::new();
2907
+ for s in &program.statements {
2908
+ walk_stmt_completion(s, source, lsp_line, lsp_char, &mut stack, &mut best);
2909
+ }
2910
+ best.map(|(_, n)| n).unwrap_or_default()
2911
+ }
2912
+
2913
+ /// Value names for completion: inner callable parameters, then block-local `let`/`fn`, then module bindings.
2914
+ pub fn completion_value_names_at_cursor(
2915
+ program: &Program,
2916
+ source: &str,
2917
+ lsp_line: u32,
2918
+ lsp_char: u32,
2919
+ ) -> Vec<Arc<str>> {
2920
+ use std::collections::HashSet;
2921
+ let mut seen = HashSet::<String>::new();
2922
+ let mut out = Vec::new();
2923
+ let push = |n: Arc<str>, out: &mut Vec<Arc<str>>, seen: &mut HashSet<String>| {
2924
+ if seen.insert(n.to_string()) {
2925
+ out.push(n);
2926
+ }
2927
+ };
2928
+ for n in callable_param_names_at_cursor(program, source, lsp_line, lsp_char) {
2929
+ push(n, &mut out, &mut seen);
2930
+ }
2931
+ for (n, _) in block_locals_containing_cursor(program, source, lsp_line, lsp_char) {
2932
+ push(n, &mut out, &mut seen);
2933
+ }
2934
+ for (n, _) in shallow_module_bindings(program) {
2935
+ push(n, &mut out, &mut seen);
2936
+ }
2937
+ out
2938
+ }
2939
+
2940
+ /// Top-level value bindings (completion baseline). Inner scopes handled in LSP via [`definition_span`].
2941
+ pub fn shallow_module_bindings(program: &Program) -> Vec<(Arc<str>, tishlang_ast::Span)> {
2942
+ let mut out = Vec::new();
2943
+ for s in &program.statements {
2944
+ match s {
2945
+ Statement::VarDecl {
2946
+ name,
2947
+ name_span,
2948
+ ..
2949
+ } => out.push((name.clone(), *name_span)),
2950
+ Statement::FunDecl {
2951
+ name,
2952
+ name_span,
2953
+ ..
2954
+ } => out.push((name.clone(), *name_span)),
2955
+ Statement::Import { specifiers, .. } => {
2956
+ for sp in specifiers {
2957
+ match sp {
2958
+ ImportSpecifier::Named {
2959
+ name,
2960
+ name_span,
2961
+ alias,
2962
+ alias_span,
2963
+ } => {
2964
+ let local = alias.as_ref().map(|a| a.clone()).unwrap_or_else(|| name.clone());
2965
+ let spn = alias_span.as_ref().unwrap_or(name_span);
2966
+ out.push((local, *spn));
2967
+ }
2968
+ ImportSpecifier::Namespace { name, name_span } => {
2969
+ out.push((name.clone(), *name_span));
2970
+ }
2971
+ ImportSpecifier::Default { name, name_span } => {
2972
+ out.push((name.clone(), *name_span));
2973
+ }
2974
+ }
2975
+ }
2976
+ }
2977
+ Statement::Export { declaration, .. } => {
2978
+ if let ExportDeclaration::Named(inner) = declaration.as_ref() {
2979
+ match inner.as_ref() {
2980
+ Statement::VarDecl {
2981
+ name,
2982
+ name_span,
2983
+ ..
2984
+ } => out.push((name.clone(), *name_span)),
2985
+ Statement::FunDecl {
2986
+ name,
2987
+ name_span,
2988
+ ..
2989
+ } => out.push((name.clone(), *name_span)),
2990
+ _ => {}
2991
+ }
2992
+ }
2993
+ }
2994
+ _ => {}
2995
+ }
2996
+ }
2997
+ out
2998
+ }
2999
+
3000
+ /// All spans (definition + uses) that resolve to `def_span` for `name`.
3001
+ pub fn reference_spans_for_def(
3002
+ program: &Program,
3003
+ source: &str,
3004
+ name: &str,
3005
+ def_span: tishlang_ast::Span,
3006
+ ) -> Vec<tishlang_ast::Span> {
3007
+ let mut out = vec![def_span];
3008
+ for s in &program.statements {
3009
+ refs_stmt(s, program, source, name, def_span, &mut out);
3010
+ }
3011
+ out.sort_by_key(|s| (s.start.0, s.start.1, s.end.0, s.end.1));
3012
+ out.dedup_by(|a, b| a == b);
3013
+ out
3014
+ }
3015
+
3016
+ fn refs_stmt(
3017
+ stmt: &Statement,
3018
+ program: &Program,
3019
+ source: &str,
3020
+ name: &str,
3021
+ def_span: tishlang_ast::Span,
3022
+ out: &mut Vec<tishlang_ast::Span>,
3023
+ ) {
3024
+ match stmt {
3025
+ Statement::VarDecl { init, .. } => {
3026
+ if let Some(e) = init {
3027
+ refs_expr(e, program, source, name, def_span, out);
3028
+ }
3029
+ }
3030
+ Statement::VarDeclDestructure { init, .. } => refs_expr(init, program, source, name, def_span, out),
3031
+ Statement::ExprStmt { expr, .. } => refs_expr(expr, program, source, name, def_span, out),
3032
+ Statement::If {
3033
+ cond,
3034
+ then_branch,
3035
+ else_branch,
3036
+ ..
3037
+ } => {
3038
+ refs_expr(cond, program, source, name, def_span, out);
3039
+ refs_stmt(then_branch, program, source, name, def_span, out);
3040
+ if let Some(b) = else_branch {
3041
+ refs_stmt(b, program, source, name, def_span, out);
3042
+ }
3043
+ }
3044
+ Statement::While { cond, body, .. } => {
3045
+ refs_expr(cond, program, source, name, def_span, out);
3046
+ refs_stmt(body, program, source, name, def_span, out);
3047
+ }
3048
+ Statement::For {
3049
+ init,
3050
+ cond,
3051
+ update,
3052
+ body,
3053
+ ..
3054
+ } => {
3055
+ if let Some(i) = init {
3056
+ refs_stmt(i, program, source, name, def_span, out);
3057
+ }
3058
+ if let Some(e) = cond {
3059
+ refs_expr(e, program, source, name, def_span, out);
3060
+ }
3061
+ if let Some(e) = update {
3062
+ refs_expr(e, program, source, name, def_span, out);
3063
+ }
3064
+ refs_stmt(body, program, source, name, def_span, out);
3065
+ }
3066
+ Statement::ForOf {
3067
+ iterable,
3068
+ body,
3069
+ ..
3070
+ } => {
3071
+ refs_expr(iterable, program, source, name, def_span, out);
3072
+ refs_stmt(body, program, source, name, def_span, out);
3073
+ }
3074
+ Statement::Return { value, .. } => {
3075
+ if let Some(e) = value {
3076
+ refs_expr(e, program, source, name, def_span, out);
3077
+ }
3078
+ }
3079
+ Statement::Block { statements, .. } => {
3080
+ for s in statements {
3081
+ refs_stmt(s, program, source, name, def_span, out);
3082
+ }
3083
+ }
3084
+ Statement::FunDecl { params, body, .. } => {
3085
+ for p in params {
3086
+ if let FunParam::Simple(tp) = p {
3087
+ if let Some(e) = &tp.default {
3088
+ refs_expr(e, program, source, name, def_span, out);
3089
+ }
3090
+ } else if let FunParam::Destructure { default: Some(e), .. } = p {
3091
+ refs_expr(e, program, source, name, def_span, out);
3092
+ }
3093
+ }
3094
+ refs_stmt(body, program, source, name, def_span, out);
3095
+ }
3096
+ Statement::Switch {
3097
+ expr,
3098
+ cases,
3099
+ default_body,
3100
+ ..
3101
+ } => {
3102
+ refs_expr(expr, program, source, name, def_span, out);
3103
+ for (_ce, stmts) in cases {
3104
+ for s in stmts {
3105
+ refs_stmt(s, program, source, name, def_span, out);
3106
+ }
3107
+ }
3108
+ if let Some(stmts) = default_body {
3109
+ for s in stmts {
3110
+ refs_stmt(s, program, source, name, def_span, out);
3111
+ }
3112
+ }
3113
+ }
3114
+ Statement::DoWhile { body, cond, .. } => {
3115
+ refs_stmt(body, program, source, name, def_span, out);
3116
+ refs_expr(cond, program, source, name, def_span, out);
3117
+ }
3118
+ Statement::Throw { value, .. } => refs_expr(value, program, source, name, def_span, out),
3119
+ Statement::Try {
3120
+ body,
3121
+ catch_body,
3122
+ finally_body,
3123
+ ..
3124
+ } => {
3125
+ refs_stmt(body, program, source, name, def_span, out);
3126
+ if let Some(cb) = catch_body {
3127
+ refs_stmt(cb, program, source, name, def_span, out);
3128
+ }
3129
+ if let Some(fb) = finally_body {
3130
+ refs_stmt(fb, program, source, name, def_span, out);
3131
+ }
3132
+ }
3133
+ Statement::Import { .. }
3134
+ | Statement::Export { .. }
3135
+ | Statement::Break { .. }
3136
+ | Statement::Continue { .. }
3137
+ | Statement::TypeAlias { .. }
3138
+ | Statement::DeclareVar { .. }
3139
+ | Statement::DeclareFun { .. } => {}
3140
+ }
3141
+ }
3142
+
3143
+ fn refs_expr(
3144
+ expr: &Expr,
3145
+ program: &Program,
3146
+ source: &str,
3147
+ name: &str,
3148
+ def_span: tishlang_ast::Span,
3149
+ out: &mut Vec<tishlang_ast::Span>,
3150
+ ) {
3151
+ let maybe_push = |span: &tishlang_ast::Span, n: &Arc<str>, out: &mut Vec<tishlang_ast::Span>| {
3152
+ if n.as_ref() != name {
3153
+ return;
3154
+ }
3155
+ let Some((l, c)) = pos::lsp_position_for_span_start(source, span) else {
3156
+ return;
3157
+ };
3158
+ if definition_span(program, source, l, c) == Some(def_span) {
3159
+ out.push(*span);
3160
+ }
3161
+ };
3162
+
3163
+ match expr {
3164
+ Expr::Ident { name: n, span } => maybe_push(span, n, out),
3165
+ Expr::Binary { left, right, .. } => {
3166
+ refs_expr(left, program, source, name, def_span, out);
3167
+ refs_expr(right, program, source, name, def_span, out);
3168
+ }
3169
+ Expr::Unary { operand, .. } => refs_expr(operand, program, source, name, def_span, out),
3170
+ Expr::Call { callee, args, .. } => {
3171
+ refs_expr(callee, program, source, name, def_span, out);
3172
+ for a in args {
3173
+ let e = match a {
3174
+ CallArg::Expr(e) => e,
3175
+ CallArg::Spread(e) => e,
3176
+ };
3177
+ refs_expr(e, program, source, name, def_span, out);
3178
+ }
3179
+ }
3180
+ Expr::New { callee, args, .. } => {
3181
+ refs_expr(callee, program, source, name, def_span, out);
3182
+ for a in args {
3183
+ let e = match a {
3184
+ CallArg::Expr(e) => e,
3185
+ CallArg::Spread(e) => e,
3186
+ };
3187
+ refs_expr(e, program, source, name, def_span, out);
3188
+ }
3189
+ }
3190
+ Expr::Member { object, .. } => refs_expr(object, program, source, name, def_span, out),
3191
+ Expr::Index { object, index, .. } => {
3192
+ refs_expr(object, program, source, name, def_span, out);
3193
+ refs_expr(index, program, source, name, def_span, out);
3194
+ }
3195
+ Expr::Conditional {
3196
+ cond,
3197
+ then_branch,
3198
+ else_branch,
3199
+ ..
3200
+ } => {
3201
+ refs_expr(cond, program, source, name, def_span, out);
3202
+ refs_expr(then_branch, program, source, name, def_span, out);
3203
+ refs_expr(else_branch, program, source, name, def_span, out);
3204
+ }
3205
+ Expr::NullishCoalesce { left, right, .. } => {
3206
+ refs_expr(left, program, source, name, def_span, out);
3207
+ refs_expr(right, program, source, name, def_span, out);
3208
+ }
3209
+ Expr::Array { elements, .. } => {
3210
+ for el in elements {
3211
+ match el {
3212
+ tishlang_ast::ArrayElement::Expr(e) => {
3213
+ refs_expr(e, program, source, name, def_span, out)
3214
+ }
3215
+ tishlang_ast::ArrayElement::Spread(e) => {
3216
+ refs_expr(e, program, source, name, def_span, out)
3217
+ }
3218
+ }
3219
+ }
3220
+ }
3221
+ Expr::Object { props, .. } => {
3222
+ for p in props {
3223
+ match p {
3224
+ tishlang_ast::ObjectProp::KeyValue(_, e) => {
3225
+ refs_expr(e, program, source, name, def_span, out)
3226
+ }
3227
+ tishlang_ast::ObjectProp::Spread(e) => {
3228
+ refs_expr(e, program, source, name, def_span, out)
3229
+ }
3230
+ }
3231
+ }
3232
+ }
3233
+ Expr::Assign { name: n, span, value } => {
3234
+ let sp = synthetic_name_span(span.start, n.as_ref());
3235
+ maybe_push(&sp, n, out);
3236
+ refs_expr(value, program, source, name, def_span, out);
3237
+ }
3238
+ Expr::TypeOf { operand, .. } => refs_expr(operand, program, source, name, def_span, out),
3239
+ Expr::PostfixInc { name: n, span } | Expr::PostfixDec { name: n, span } => {
3240
+ let sp = synthetic_name_span(span.start, n.as_ref());
3241
+ maybe_push(&sp, n, out);
3242
+ }
3243
+ Expr::PrefixInc { name: n, span } | Expr::PrefixDec { name: n, span } => {
3244
+ let sp = synthetic_name_span(span.start, n.as_ref());
3245
+ maybe_push(&sp, n, out);
3246
+ }
3247
+ Expr::CompoundAssign { name: n, span, value, .. }
3248
+ | Expr::LogicalAssign { name: n, span, value, .. } => {
3249
+ let sp = synthetic_name_span(span.start, n.as_ref());
3250
+ maybe_push(&sp, n, out);
3251
+ refs_expr(value, program, source, name, def_span, out);
3252
+ }
3253
+ Expr::MemberAssign { object, value, .. } => {
3254
+ refs_expr(object, program, source, name, def_span, out);
3255
+ refs_expr(value, program, source, name, def_span, out);
3256
+ }
3257
+ Expr::IndexAssign {
3258
+ object,
3259
+ index,
3260
+ value,
3261
+ ..
3262
+ } => {
3263
+ refs_expr(object, program, source, name, def_span, out);
3264
+ refs_expr(index, program, source, name, def_span, out);
3265
+ refs_expr(value, program, source, name, def_span, out);
3266
+ }
3267
+ Expr::ArrowFunction { params, body, .. } => {
3268
+ for p in params {
3269
+ if let FunParam::Simple(tp) = p {
3270
+ if let Some(e) = &tp.default {
3271
+ refs_expr(e, program, source, name, def_span, out);
3272
+ }
3273
+ } else if let FunParam::Destructure { default: Some(e), .. } = p {
3274
+ refs_expr(e, program, source, name, def_span, out);
3275
+ }
3276
+ }
3277
+ match body {
3278
+ ArrowBody::Expr(e) => refs_expr(e, program, source, name, def_span, out),
3279
+ ArrowBody::Block(b) => refs_stmt(b, program, source, name, def_span, out),
3280
+ }
3281
+ }
3282
+ Expr::TemplateLiteral { exprs, .. } => {
3283
+ for e in exprs {
3284
+ refs_expr(e, program, source, name, def_span, out);
3285
+ }
3286
+ }
3287
+ Expr::Await { operand, .. } => refs_expr(operand, program, source, name, def_span, out),
3288
+ Expr::JsxElement { props, children, .. } => {
3289
+ for p in props {
3290
+ match p {
3291
+ tishlang_ast::JsxProp::Attr { value, .. } => {
3292
+ if let tishlang_ast::JsxAttrValue::Expr(e) = value {
3293
+ refs_expr(e, program, source, name, def_span, out);
3294
+ }
3295
+ }
3296
+ tishlang_ast::JsxProp::Spread(e) => refs_expr(e, program, source, name, def_span, out),
3297
+ }
3298
+ }
3299
+ for ch in children {
3300
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
3301
+ refs_expr(e, program, source, name, def_span, out);
3302
+ }
3303
+ }
3304
+ }
3305
+ Expr::JsxFragment { children, .. } => {
3306
+ for ch in children {
3307
+ if let tishlang_ast::JsxChild::Expr(e) = ch {
3308
+ refs_expr(e, program, source, name, def_span, out);
3309
+ }
3310
+ }
3311
+ }
3312
+ Expr::Literal { .. } | Expr::NativeModuleLoad { .. } => {}
3313
+ }
3314
+ }
3315
+
3316
+ #[cfg(test)]
3317
+ mod tests {
3318
+ use super::*;
3319
+ use tishlang_parser::parse;
3320
+
3321
+ #[test]
3322
+ fn resolves_second_line_reference() {
3323
+ let src = "let a = 1\nlet b = a\n";
3324
+ let program = parse(src).expect("parse");
3325
+ let ds = definition_span(&program, src, 1, 8).expect("def");
3326
+ assert_eq!(ds.start.0, 1);
3327
+ }
3328
+
3329
+ #[test]
3330
+ fn inner_scope_shadow() {
3331
+ let src = "let x = 1\n{\n let x = 2\n x\n}\n";
3332
+ let program = parse(src).expect("parse");
3333
+ let inner = definition_span(&program, src, 3, 2).expect("inner x def");
3334
+ assert_eq!(inner.start.0, 3);
3335
+ }
3336
+
3337
+ #[test]
3338
+ fn completion_includes_fn_params() {
3339
+ let src = "fn f(foo, bar) {\n bar\n}\n";
3340
+ let program = parse(src).expect("parse");
3341
+ let names = completion_value_names_at_cursor(&program, src, 1, 2);
3342
+ assert!(names.iter().any(|n| n.as_ref() == "foo"), "names={names:?}");
3343
+ assert!(names.iter().any(|n| n.as_ref() == "bar"));
3344
+ }
3345
+
3346
+ #[test]
3347
+ fn unresolved_unknown_ident() {
3348
+ let src = "let x = nope\n";
3349
+ let program = parse(src).expect("parse");
3350
+ let u = collect_unresolved_identifiers(&program);
3351
+ assert_eq!(u.len(), 1);
3352
+ assert_eq!(u[0].name.as_ref(), "nope");
3353
+ }
3354
+
3355
+ #[test]
3356
+ fn console_global_not_unresolved() {
3357
+ let src = "console.log(1)\n";
3358
+ let program = parse(src).expect("parse");
3359
+ let u = collect_unresolved_identifiers(&program);
3360
+ assert!(u.is_empty(), "u={u:?}");
3361
+ }
3362
+
3363
+ #[test]
3364
+ fn resolved_ident_not_unresolved() {
3365
+ let src = "let x = 1\nlet y = x\n";
3366
+ let program = parse(src).expect("parse");
3367
+ let u = collect_unresolved_identifiers(&program);
3368
+ assert!(u.is_empty(), "u={u:?}");
3369
+ }
3370
+
3371
+ #[test]
3372
+ fn unused_import_named() {
3373
+ let src = "import { a, b } from \"./m\"\nlet x = a\nx\n";
3374
+ let program = parse(src).expect("parse");
3375
+ let u = collect_unused_bindings(&program, src);
3376
+ assert_eq!(u.len(), 1);
3377
+ assert_eq!(u[0].name.as_ref(), "b");
3378
+ assert_eq!(u[0].kind, UnusedBindingKind::Import);
3379
+ }
3380
+
3381
+ #[test]
3382
+ fn unused_let_underscore_ignored() {
3383
+ let src = "let _x = 1\n";
3384
+ let program = parse(src).expect("parse");
3385
+ let u = collect_unused_bindings(&program, src);
3386
+ assert!(u.is_empty(), "u={u:?}");
3387
+ }
3388
+
3389
+ #[test]
3390
+ fn unused_local_let() {
3391
+ let src = "let dead = 1\n";
3392
+ let program = parse(src).expect("parse");
3393
+ let u = collect_unused_bindings(&program, src);
3394
+ assert_eq!(u.len(), 1);
3395
+ assert_eq!(u[0].name.as_ref(), "dead");
3396
+ assert_eq!(u[0].kind, UnusedBindingKind::Variable);
3397
+ }
3398
+
3399
+ #[test]
3400
+ fn export_not_reported_unused() {
3401
+ let src = "export let x = 1\n";
3402
+ let program = parse(src).expect("parse");
3403
+ let u = collect_unused_bindings(&program, src);
3404
+ assert!(u.is_empty(), "u={u:?}");
3405
+ }
3406
+
3407
+ #[test]
3408
+ fn member_access_chain_three_deep() {
3409
+ let src = "let x = a.b.c\n";
3410
+ let program = parse(src).expect("parse");
3411
+ let ch = member_access_chain_at_cursor(&program, src, 0, 12).expect("chain");
3412
+ assert_eq!(ch.root_local.as_ref(), "a");
3413
+ assert_eq!(ch.members.len(), 2);
3414
+ assert_eq!(ch.members[0].as_ref(), "b");
3415
+ assert_eq!(ch.members[1].as_ref(), "c");
3416
+ }
3417
+
3418
+ #[test]
3419
+ fn name_at_cursor_prefers_member_prop() {
3420
+ let src = "let x = a.b.c\n";
3421
+ let program = parse(src).expect("parse");
3422
+ let nu = name_at_cursor(&program, src, 0, 12).expect("name use");
3423
+ assert_eq!(nu.name.as_ref(), "c");
3424
+ }
3425
+
3426
+ #[test]
3427
+ fn member_access_chain_across_lines() {
3428
+ let src = "fn main()\n let x = a.b\n .c\n";
3429
+ let program = parse(src).expect("parse");
3430
+ let ch = member_access_chain_at_cursor(&program, src, 2, 5).expect("chain");
3431
+ assert_eq!(ch.root_local.as_ref(), "a");
3432
+ assert_eq!(ch.members.len(), 2);
3433
+ assert_eq!(ch.members[0].as_ref(), "b");
3434
+ assert_eq!(ch.members[1].as_ref(), "c");
3435
+ }
3436
+ }