1use std::collections::{BTreeMap, BTreeSet};
36
37use crate::program::{
38 TsArrowBody, TsBindingName, TsDecl, TsExpr, TsObjectEntry, TsParam, TsProgram, TsStmt,
39 TsStmtKind, TsType, TsTypeMember,
40};
41
42const GLOBAL_TYPES: &[&str] = &[
45 "string",
47 "number",
48 "boolean",
49 "bigint",
50 "symbol",
51 "object",
52 "unknown",
53 "never",
54 "void",
55 "undefined",
56 "null",
57 "any",
58 "this",
59 "true",
60 "false",
61 "const",
64 "Array",
66 "ReadonlyArray",
67 "Record",
68 "Partial",
69 "Readonly",
70 "Required",
71 "Pick",
72 "Omit",
73 "Exclude",
74 "Extract",
75 "NonNullable",
76 "ReturnType",
77 "Parameters",
78 "Awaited",
79 "Promise",
80 "PromiseLike",
81 "Map",
82 "ReadonlyMap",
83 "Set",
84 "ReadonlySet",
85 "WeakMap",
86 "Date",
87 "Error",
88 "RegExp",
89 "Uint8Array",
90 "ArrayBuffer",
91 "Iterable",
92 "IterableIterator",
93 "AsyncIterable",
94 "AsyncIterableIterator",
95 "AsyncGenerator",
96 "Generator",
97 "JSON",
98 "Response",
99 "Request",
100 "Headers",
101 "URL",
102 "URLSearchParams",
103 "ReadableStream",
104 "WritableStream",
105 "TextEncoder",
106 "TextDecoder",
107 "AbortSignal",
108 "WebSocket",
109 "MessageEvent",
110 "Blob",
111 "FormData",
112 "Function",
113 "PropertyKey",
114 "globalThis",
116];
117
118const GLOBAL_VALUES: &[&str] = &[
120 "globalThis",
121 "undefined",
122 "NaN",
123 "Infinity",
124 "this",
125 "arguments",
126 "true",
127 "false",
128 "null",
129 "JSON",
130 "Object",
131 "Number",
132 "String",
133 "Boolean",
134 "Array",
135 "Promise",
136 "Math",
137 "Date",
138 "Error",
139 "TypeError",
140 "RangeError",
141 "SyntaxError",
142 "Symbol",
143 "BigInt",
144 "Map",
145 "Set",
146 "WeakMap",
147 "Reflect",
148 "Intl",
149 "Uint8Array",
150 "ArrayBuffer",
151 "TextEncoder",
152 "TextDecoder",
153 "URL",
154 "URLSearchParams",
155 "Response",
156 "Request",
157 "Headers",
158 "crypto",
159 "console",
160 "structuredClone",
161 "setTimeout",
162 "clearTimeout",
163 "queueMicrotask",
164 "atob",
165 "btoa",
166 "fetch",
167 "encodeURIComponent",
168 "decodeURIComponent",
169 "parseInt",
170 "parseFloat",
171 "isNaN",
172 "isFinite",
173 "AbortController",
174];
175
176pub fn unbound_names(program: &TsProgram) -> Vec<String> {
179 let mut w = Walk::default();
180 for s in &program.stmts {
181 w.stmt(s);
182 }
183 let types = w
184 .used_types
185 .iter()
186 .filter(|n| !w.bound.contains(*n) && !GLOBAL_TYPES.contains(&n.as_str()));
187 let values = w
188 .used_values
189 .iter()
190 .filter(|n| !w.bound.contains(*n) && !GLOBAL_VALUES.contains(&n.as_str()));
191 let all: BTreeSet<String> = types.chain(values).cloned().collect();
192 all.into_iter().collect()
193}
194
195pub fn value_imports(program: &TsProgram) -> Vec<(String, String)> {
199 let mut out = Vec::new();
200 for stmt in &program.stmts {
201 if let TsStmtKind::Decl(TsDecl::Import {
202 type_only: false,
203 names,
204 from,
205 }) = &stmt.kind
206 {
207 out.extend(
208 names
209 .iter()
210 .map(|n| n.trim())
211 .filter(|n| !n.starts_with("type "))
212 .map(|n| (from.clone(), n.to_string())),
213 );
214 }
215 }
216 out
217}
218
219pub fn type_only_value_uses(program: &TsProgram) -> Vec<String> {
224 let mut w = Walk::default();
225 for s in &program.stmts {
226 w.stmt(s);
227 }
228 w.used_values
229 .iter()
230 .filter(|n| w.type_only.contains(*n) && !w.value_bound.contains(*n))
231 .cloned()
232 .collect()
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
239pub struct NamespaceMemberUse {
240 pub from: String,
242 pub member: String,
244}
245
246pub fn namespace_member_uses(program: &TsProgram) -> Vec<NamespaceMemberUse> {
254 let mut w = Walk::default();
255 for s in &program.stmts {
256 w.stmt(s);
257 }
258 let uses: BTreeSet<NamespaceMemberUse> = w
259 .qualified
260 .iter()
261 .filter_map(|(head, member)| {
262 w.namespaces.get(head).map(|from| NamespaceMemberUse {
263 from: from.clone(),
264 member: member.clone(),
265 })
266 })
267 .collect();
268 uses.into_iter().collect()
269}
270
271#[derive(Debug, Default, Clone, PartialEq, Eq)]
273pub struct ModuleExports {
274 pub names: BTreeSet<String>,
276 pub star_from: Vec<String>,
279}
280
281pub fn module_exports(program: &TsProgram) -> ModuleExports {
287 let mut out = ModuleExports::default();
288 for stmt in &program.stmts {
289 match &stmt.kind {
290 TsStmtKind::Decl(TsDecl::ReExportAll { from }) => out.star_from.push(from.clone()),
291 TsStmtKind::Decl(_) => {
292 out.names
293 .extend(stmt.exported_names().iter().map(|e| e.name.to_string()));
294 }
295 TsStmtKind::Raw(text) | TsStmtKind::Verbatim { text, .. } => {
296 opaque_exports(text, &mut out.names)
297 }
298 _ => {}
299 }
300 }
301 out
302}
303
304fn opaque_exports(text: &str, out: &mut BTreeSet<String>) {
306 for line in text.lines() {
307 let Some(rest) = line.trim_start().strip_prefix("export ") else {
308 continue;
309 };
310 let mut rest = rest.trim_start();
311 for prefix in ["declare ", "async "] {
312 rest = rest.strip_prefix(prefix).unwrap_or(rest).trim_start();
313 }
314 let list = rest
316 .strip_prefix("type ")
317 .map(str::trim_start)
318 .unwrap_or(rest)
319 .strip_prefix('{');
320 if let Some(list) = list {
321 let inner = list.split('}').next().unwrap_or("");
322 for spec in inner.split(',') {
323 let spec = spec.trim();
324 let spec = spec.strip_prefix("type ").unwrap_or(spec).trim();
325 if let Some(name) = leading_ident(spec.rsplit(" as ").next().unwrap_or(spec)) {
326 out.insert(name.to_string());
327 }
328 }
329 continue;
330 }
331 for keyword in [
332 "function ",
333 "const ",
334 "let ",
335 "var ",
336 "class ",
337 "interface ",
338 "type ",
339 "enum ",
340 ] {
341 if let Some(after) = rest.strip_prefix(keyword)
342 && let Some(name) = leading_ident(after)
343 {
344 out.insert(name.to_string());
345 }
346 }
347 }
348}
349
350#[derive(Default)]
351struct Walk {
352 bound: BTreeSet<String>,
353 used_types: BTreeSet<String>,
354 used_values: BTreeSet<String>,
355 namespaces: BTreeMap<String, String>,
357 qualified: BTreeSet<(String, String)>,
359 type_only: BTreeSet<String>,
363 value_bound: BTreeSet<String>,
364}
365
366fn leading_ident(text: &str) -> Option<&str> {
370 let text = text.trim_start();
371 let end = text
372 .char_indices()
373 .find(|(_, c)| !(c.is_alphanumeric() || *c == '_' || *c == '$'))
374 .map_or(text.len(), |(i, _)| i);
375 let ident = &text[..end];
376 ident
377 .chars()
378 .next()
379 .filter(|c| c.is_alphabetic() || *c == '_' || *c == '$')
380 .map(|_| ident)
381}
382
383impl Walk {
384 fn bind(&mut self, name: &str) {
387 let name = name.trim_start();
388 if name.starts_with('[') || name.starts_with('{') {
389 for word in name.split(|c: char| !(c.is_alphanumeric() || c == '_' || c == '$')) {
390 if let Some(ident) = leading_ident(word) {
391 self.bound.insert(ident.to_string());
392 self.value_bound.insert(ident.to_string());
393 }
394 }
395 } else if let Some(ident) = leading_ident(name) {
396 self.bound.insert(ident.to_string());
397 self.value_bound.insert(ident.to_string());
398 }
399 }
400
401 fn bind_type_only(&mut self, name: &str) {
403 if let Some(ident) = leading_ident(name) {
404 self.bound.insert(ident.to_string());
405 self.type_only.insert(ident.to_string());
406 }
407 }
408
409 fn bind_pattern(&mut self, names: &[String]) {
411 for n in names {
412 let local = n.split('=').next().unwrap_or(n);
413 let local = local.rsplit(':').next().unwrap_or(local);
414 self.bind(local);
415 }
416 }
417
418 fn binding(&mut self, name: &TsBindingName) {
419 match name {
420 TsBindingName::Ident(n) => self.bind(n),
421 TsBindingName::ObjectPattern(names) => self.bind_pattern(names),
422 }
423 }
424
425 fn use_value(&mut self, text: &str) {
426 if let Some(ident) = leading_ident(text) {
427 self.used_values.insert(ident.to_string());
428 }
429 }
430
431 fn bind_all(&mut self, names: &[String]) {
432 for n in names {
433 self.bind(n);
434 }
435 }
436
437 fn bind_specifier(&mut self, spec: &str, decl_type_only: bool) {
440 let spec = spec.trim();
441 let (spec, type_only) = match spec.strip_prefix("type ") {
442 Some(rest) => (rest.trim(), true),
443 None => (spec, decl_type_only),
444 };
445 let local = spec.rsplit(" as ").next().unwrap_or(spec).trim();
446 if local.is_empty() {
447 return;
448 }
449 if type_only {
450 self.bind_type_only(local);
451 } else {
452 self.bind(local);
453 }
454 }
455
456 fn bind_opaque(&mut self, text: &str) {
462 for line in text.lines() {
463 let mut rest = line.trim_start();
464 for prefix in ["export ", "declare ", "default ", "async "] {
465 rest = rest.strip_prefix(prefix).unwrap_or(rest).trim_start();
466 }
467 if let Some(import) = rest.strip_prefix("import ") {
468 let head = import.split(" from ").next().unwrap_or("");
469 let braces = head
470 .find('{')
471 .and_then(|open| head[open..].find('}').map(|close| (open, open + close)));
472 if let Some((open, close)) = braces {
473 for spec in head[open + 1..close].split(',') {
474 self.bind_specifier(spec, false);
475 }
476 } else if let Some(alias) = head.trim().strip_prefix("* as ") {
477 self.bind(alias.trim());
478 }
479 continue;
480 }
481 for keyword in [
482 "type ",
483 "interface ",
484 "class ",
485 "enum ",
486 "function ",
487 "const ",
488 "let ",
489 "var ",
490 ] {
491 if let Some(after) = rest.strip_prefix(keyword) {
492 let after = after.trim_start();
493 if let Some(pattern) = after.strip_prefix('{') {
494 let inner = pattern.split('}').next().unwrap_or("");
495 let names: Vec<String> = inner.split(',').map(str::to_string).collect();
496 self.bind_pattern(&names);
497 } else {
498 self.bind(after);
499 }
500 }
501 }
502 }
503 }
504
505 fn opaque(&mut self, text: &str) {
508 self.bind_opaque(text);
509 self.use_opaque_generics(text);
510 }
511
512 fn use_opaque_generics(&mut self, text: &str) {
524 let bytes = text.as_bytes();
525 let ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b'$';
526 let mut i = 0;
527 while i < bytes.len() {
528 let b = bytes[i];
529 match b {
530 b'"' | b'\'' | b'`' => {
531 i += 1;
532 while i < bytes.len() && bytes[i] != b {
533 i += if bytes[i] == b'\\' { 2 } else { 1 };
534 }
535 i += 1;
536 }
537 b'/' if bytes.get(i + 1) == Some(&b'/') => {
538 while i < bytes.len() && bytes[i] != b'\n' {
539 i += 1;
540 }
541 }
542 b'/' if bytes.get(i + 1) == Some(&b'*') => {
543 i += 2;
544 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
545 i += 1;
546 }
547 i += 2;
548 }
549 _ if ident(b) => {
550 let start = i;
551 while i < bytes.len() && ident(bytes[i]) {
552 i += 1;
553 }
554 let after_dot = start > 0 && bytes[start - 1] == b'.';
555 if !after_dot && b.is_ascii_uppercase() && bytes.get(i) == Some(&b'<') {
556 self.used_types.insert(text[start..i].to_string());
557 }
558 }
559 _ => i += 1,
560 }
561 }
562 }
563
564 fn ty(&mut self, t: &TsType) {
565 match t {
566 TsType::Named { name, type_args } => {
567 let mut segments = name.splitn(2, '.');
568 let head = segments.next().unwrap_or(name).trim();
569 if let Some(member) = segments.next().and_then(leading_ident) {
571 self.qualified
572 .insert((head.to_string(), member.to_string()));
573 }
574 if head
575 .chars()
576 .next()
577 .is_some_and(|c| c.is_alphabetic() || c == '_' || c == '$')
578 && head
579 .chars()
580 .all(|c| c.is_alphanumeric() || c == '_' || c == '$')
581 {
582 self.used_types.insert(head.to_string());
583 }
584 for a in type_args {
585 self.ty(a);
586 }
587 }
588 TsType::Array { element, .. } => self.ty(element),
589 TsType::Object(members) => self.members(members),
590 TsType::Fn { params, ret } => {
591 for p in params {
592 self.ty(p);
593 }
594 self.ty(ret);
595 }
596 TsType::Union { members, .. } => {
597 for m in members {
598 self.ty(m);
599 }
600 }
601 TsType::Intersection(members) => {
602 for m in members {
603 self.ty(m);
604 }
605 }
606 }
607 }
608
609 fn members(&mut self, members: &[TsTypeMember]) {
610 for m in members {
611 match m {
612 TsTypeMember::Prop { ty, .. } => self.ty(ty),
613 TsTypeMember::Method {
614 generics,
615 params,
616 ret,
617 ..
618 } => {
619 self.bind_all(generics);
620 self.params(params);
621 self.ty(ret);
622 }
623 TsTypeMember::Index {
624 key_ty, value_ty, ..
625 } => {
626 self.ty(key_ty);
627 self.ty(value_ty);
628 }
629 }
630 }
631 }
632
633 fn params(&mut self, params: &[TsParam]) {
634 for p in params {
635 self.bind(&p.name);
636 if let Some(t) = &p.ty {
637 self.ty(t);
638 }
639 }
640 }
641
642 fn opt_ty(&mut self, t: &Option<TsType>) {
643 if let Some(t) = t {
644 self.ty(t);
645 }
646 }
647
648 fn stmts(&mut self, stmts: &[TsStmt]) {
649 for s in stmts {
650 self.stmt(s);
651 }
652 }
653
654 fn stmt(&mut self, stmt: &TsStmt) {
655 match &stmt.kind {
656 TsStmtKind::Verbatim { text, .. } | TsStmtKind::Raw(text) => self.opaque(text),
657 TsStmtKind::Decl(decl) => self.decl(decl),
658 TsStmtKind::Const { name, ty, init } => {
659 self.binding(name);
660 self.opt_ty(ty);
661 self.expr(init);
662 }
663 TsStmtKind::Let { name, ty, init } => {
664 self.binding(name);
665 self.opt_ty(ty);
666 if let Some(e) = init {
667 self.expr(e);
668 }
669 }
670 TsStmtKind::ExprStmt(e) | TsStmtKind::Throw(e) | TsStmtKind::Increment(e) => {
671 self.expr(e)
672 }
673 TsStmtKind::Return(e) => {
674 if let Some(e) = e {
675 self.expr(e);
676 }
677 }
678 TsStmtKind::If {
679 cond,
680 then_branch,
681 else_branch,
682 same_line_else: _,
683 } => {
684 self.expr(cond);
685 self.stmt(then_branch);
686 if let Some(e) = else_branch {
687 self.stmt(e);
688 }
689 }
690 TsStmtKind::ForOf {
691 binding,
692 iter,
693 body,
694 } => {
695 self.bind(binding);
696 self.expr(iter);
697 self.stmt(body);
698 }
699 TsStmtKind::For {
700 name,
701 init,
702 test,
703 body,
704 } => {
705 self.bind(name);
706 self.expr(init);
707 self.expr(test);
708 self.stmt(body);
709 }
710 TsStmtKind::TryCatch {
711 try_block,
712 catch_param,
713 catch_block,
714 } => {
715 if let Some(p) = catch_param {
716 self.bind(p);
717 }
718 self.stmt(try_block);
719 self.stmt(catch_block);
720 }
721 TsStmtKind::Block(stmts) | TsStmtKind::InlineBlock(stmts) => self.stmts(stmts),
722 TsStmtKind::Assign { target, value } => {
723 self.expr(target);
724 self.expr(value);
725 }
726 TsStmtKind::Switch {
727 discriminant,
728 cases,
729 } => {
730 self.expr(discriminant);
731 for case in cases {
732 if let Some(t) = &case.test {
733 self.expr(t);
734 }
735 self.stmts(&case.body);
736 }
737 }
738 TsStmtKind::Continue
739 | TsStmtKind::Comment(_)
740 | TsStmtKind::DocComment(_)
741 | TsStmtKind::Blank => {}
742 }
743 }
744
745 fn decl(&mut self, decl: &TsDecl) {
746 match decl {
747 TsDecl::Import {
748 names, type_only, ..
749 } => {
750 for n in names {
751 self.bind_specifier(n, *type_only);
752 }
753 }
754 TsDecl::ImportNamespace {
755 alias,
756 from,
757 type_only,
758 } => {
759 self.namespaces.insert(alias.clone(), from.clone());
760 if *type_only {
761 self.bind_type_only(alias)
762 } else {
763 self.bind(alias)
764 }
765 }
766 TsDecl::ImportDefault { alias, .. } => self.bind(alias),
767 TsDecl::ReExport { .. } | TsDecl::ReExportAll { .. } => {}
768 TsDecl::ExportNames { names } => {
772 for n in names {
773 let n = n.trim();
774 match n.strip_prefix("type ") {
775 Some(t) => {
776 if let Some(ident) = leading_ident(t) {
777 self.used_types.insert(ident.to_string());
778 }
779 }
780 None => self.use_value(n),
781 }
782 }
783 }
784 TsDecl::Export(inner) => self.decl(inner),
785 TsDecl::Interface {
786 name,
787 type_params,
788 members,
789 } => {
790 self.bind(name);
791 self.bind_all(type_params);
792 self.members(members);
793 }
794 TsDecl::TypeAlias {
795 name,
796 type_params,
797 ty,
798 } => {
799 self.bind(name);
800 self.bind_all(type_params);
801 self.ty(ty);
802 }
803 TsDecl::ConstDecl { name, ty, init } => {
804 self.bind(name);
805 self.opt_ty(ty);
806 self.expr(init);
807 }
808 TsDecl::DeclareConst { name, ty } => {
809 self.bind(name);
810 self.ty(ty);
811 }
812 TsDecl::Class {
813 name,
814 fields,
815 constructor,
816 methods,
817 } => {
818 self.bind(name);
819 for f in fields {
820 self.ty(&f.ty);
821 }
822 if let Some(ctor) = constructor {
823 self.params(&ctor.params);
824 self.stmts(&ctor.body);
825 }
826 for m in methods {
827 self.params(&m.params);
828 self.opt_ty(&m.return_type);
829 self.stmts(&m.body);
830 }
831 }
832 TsDecl::Function {
833 name,
834 generics,
835 params,
836 return_type,
837 body,
838 ..
839 } => {
840 self.bind(name);
841 self.bind_all(generics);
842 self.params(params);
843 self.opt_ty(return_type);
844 self.stmts(body);
845 }
846 TsDecl::ExportDefault(e) => self.expr(e),
847 }
848 }
849
850 fn expr(&mut self, expr: &TsExpr) {
851 match expr {
852 TsExpr::Ident(name) => self.use_value(name),
853 TsExpr::Lit(_) => {}
854 TsExpr::VerbatimExpr(text, _) => self.opaque(text),
855 TsExpr::Member { object, property } | TsExpr::OptionalMember { object, property } => {
856 if let TsExpr::Ident(head) = object.as_ref() {
857 self.qualified.insert((head.clone(), property.clone()));
858 }
859 self.expr(object)
860 }
861 TsExpr::Index { object, index } | TsExpr::OptionalIndex { object, index } => {
862 self.expr(object);
863 self.expr(index);
864 }
865 TsExpr::Arrow {
866 params,
867 generics,
868 return_type,
869 body,
870 ..
871 } => {
872 self.bind_all(generics);
873 self.params(params);
874 self.opt_ty(return_type);
875 match body.as_ref() {
876 TsArrowBody::Expr(e) => self.expr(e),
877 TsArrowBody::Block(stmts) => self.stmts(stmts),
878 }
879 }
880 TsExpr::Call { callee, args } | TsExpr::New { callee, args } => {
881 self.expr(callee);
882 for a in args {
883 self.expr(a);
884 }
885 }
886 TsExpr::Object { entries, .. } => {
887 for e in entries {
888 match e {
889 TsObjectEntry::Prop(_, v) | TsObjectEntry::Spread(v) => self.expr(v),
890 TsObjectEntry::Shorthand(name) => self.use_value(name),
891 TsObjectEntry::Method { params, body, .. } => {
892 self.params(params);
893 self.stmts(body);
894 }
895 }
896 }
897 }
898 TsExpr::Array { items, .. } => {
899 for i in items {
900 self.expr(i);
901 }
902 }
903 TsExpr::TemplateLit { exprs, .. } => {
904 for e in exprs {
905 self.expr(e);
906 }
907 }
908 TsExpr::Await(e) | TsExpr::Paren(e) | TsExpr::Unary { expr: e, .. } => self.expr(e),
909 TsExpr::As { expr, ty } => {
910 self.expr(expr);
911 self.ty(ty);
912 }
913 TsExpr::Binary { left, right, .. } => {
914 self.expr(left);
915 self.expr(right);
916 }
917 TsExpr::Conditional {
918 test,
919 consequent,
920 alternate,
921 } => {
922 self.expr(test);
923 self.expr(consequent);
924 self.expr(alternate);
925 }
926 }
927 }
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use crate::program::{TsArrowBody, VerbatimOrigin};
934
935 fn program(stmts: Vec<TsStmt>) -> TsProgram {
936 TsProgram { stmts }
937 }
938
939 fn import(names: &[&str]) -> TsStmt {
940 TsStmt::decl(
941 TsDecl::Import {
942 type_only: false,
943 names: names.iter().map(|n| n.to_string()).collect(),
944 from: "./m.js".to_string(),
945 },
946 None,
947 )
948 }
949
950 fn codec(name: &str, param_ty: &str, call: &str) -> TsStmt {
952 TsStmt::decl(
953 TsDecl::Export(Box::new(TsDecl::Function {
954 name: name.to_string(),
955 generics: vec![],
956 params: vec![TsParam {
957 name: "value".to_string(),
958 ty: Some(TsType::named(param_ty)),
959 optional: false,
960 }],
961 return_type: Some(TsType::named("__JsonValue")),
962 body: vec![TsStmt::return_stmt(
963 Some(TsExpr::Call {
964 callee: Box::new(TsExpr::Ident(call.to_string())),
965 args: vec![TsExpr::Ident("value".to_string())],
966 }),
967 None,
968 )],
969 is_async: false,
970 inline: false,
971 })),
972 None,
973 )
974 }
975
976 #[test]
977 fn a_type_named_but_never_imported_is_unbound() {
978 let p = program(vec![
980 import(&["type __JsonValue", "__serialise_Name"]),
981 codec("__serialise_Note", "Note", "__serialise_Name"),
982 ]);
983 assert_eq!(unbound_names(&p), ["Note"]);
984 }
985
986 #[test]
987 fn a_function_called_but_never_imported_is_unbound() {
988 let p = program(vec![
990 import(&["type __JsonValue", "type Amount"]),
991 codec("__serialise_Quote", "Amount", "__serialise_Amount"),
992 ]);
993 assert_eq!(unbound_names(&p), ["__serialise_Amount"]);
994 }
995
996 #[test]
997 fn a_bare_export_list_uses_each_name_it_exports() {
998 let p = program(vec![
1001 import(&["__serialise_Money"]),
1002 TsStmt::decl(
1003 TsDecl::ExportNames {
1004 names: vec![
1005 "__serialise_Money".to_string(),
1006 "__deserialise_Money".to_string(),
1007 ],
1008 },
1009 None,
1010 ),
1011 ]);
1012 assert_eq!(unbound_names(&p), ["__deserialise_Money"]);
1013 }
1014
1015 #[test]
1016 fn imports_bind_their_local_names_including_aliases_and_type_only() {
1017 let p = program(vec![
1018 import(&[
1019 "type __JsonValue",
1020 "Note as __CommonsNote",
1021 "__serialise_Base",
1022 ]),
1023 codec("__serialise_Note", "__CommonsNote", "__serialise_Base"),
1024 ]);
1025 assert!(unbound_names(&p).is_empty(), "{:?}", unbound_names(&p));
1026 }
1027
1028 #[test]
1029 fn a_namespace_import_binds_the_qualifier_of_a_qualified_name() {
1030 let p = program(vec![
1031 TsStmt::decl(
1032 TsDecl::ImportNamespace {
1033 type_only: true,
1034 alias: "handlers".to_string(),
1035 from: "./handlers.js".to_string(),
1036 },
1037 None,
1038 ),
1039 TsStmt::let_stmt(
1040 TsBindingName::Ident("o".to_string()),
1041 Some(TsType::named("handlers.Order")),
1042 None,
1043 None,
1044 ),
1045 TsStmt::let_stmt(
1046 TsBindingName::Ident("p".to_string()),
1047 Some(TsType::named("other.Order")),
1048 None,
1049 None,
1050 ),
1051 ]);
1052 assert_eq!(unbound_names(&p), ["other"]);
1053 }
1054
1055 #[test]
1056 fn declarations_in_the_module_bind_their_names() {
1057 let p = program(vec![
1058 import(&["type __JsonValue"]),
1059 TsStmt::decl(
1060 TsDecl::TypeAlias {
1061 name: "Note".to_string(),
1062 type_params: vec![],
1063 ty: TsType::named("string"),
1064 },
1065 None,
1066 ),
1067 codec("__serialise_Note", "Note", "__serialise_Note"),
1068 ]);
1069 assert!(unbound_names(&p).is_empty(), "{:?}", unbound_names(&p));
1070 }
1071
1072 #[test]
1073 fn parameters_locals_and_destructured_names_are_bound() {
1074 let p = program(vec![TsStmt::decl(
1075 TsDecl::Function {
1076 name: "f".to_string(),
1077 generics: vec!["T".to_string()],
1078 params: vec![TsParam {
1079 name: "path: string = \"$\"".to_string(),
1080 ty: Some(TsType::named("T")),
1081 optional: false,
1082 }],
1083 return_type: None,
1084 body: vec![
1085 TsStmt::const_stmt(
1086 TsBindingName::ObjectPattern(vec!["a".to_string(), "b: c".to_string()]),
1087 None,
1088 TsExpr::Ident("path".to_string()),
1089 None,
1090 ),
1091 TsStmt::for_of(
1092 "[k, v]",
1093 TsExpr::Ident("c".to_string()),
1094 TsStmt::block(
1095 vec![
1096 TsStmt::expr_stmt(TsExpr::Ident("k.length".to_string()), None),
1097 TsStmt::expr_stmt(TsExpr::Ident("v".to_string()), None),
1098 TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None),
1099 ],
1100 None,
1101 ),
1102 None,
1103 ),
1104 ],
1105 is_async: false,
1106 inline: false,
1107 },
1108 None,
1109 )]);
1110 assert!(unbound_names(&p).is_empty(), "{:?}", unbound_names(&p));
1111 }
1112
1113 #[test]
1114 fn globals_and_literals_are_bound() {
1115 let p = program(vec![TsStmt::expr_stmt(
1116 TsExpr::Arrow {
1117 params: vec![],
1118 return_type: Some(TsType::named_with_args(
1119 "Promise",
1120 vec![TsType::named("globalThis.Response")],
1121 )),
1122 generics: vec![],
1123 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Call {
1124 callee: Box::new(TsExpr::Ident("JSON.stringify".to_string())),
1125 args: vec![TsExpr::Ident("false".to_string())],
1126 }))),
1127 is_async: true,
1128 },
1129 None,
1130 )]);
1131 assert!(unbound_names(&p).is_empty(), "{:?}", unbound_names(&p));
1132 }
1133
1134 #[test]
1135 fn opaque_text_binds_what_it_declares_or_imports_line_by_line() {
1136 let p = program(vec![
1137 TsStmt::raw(
1138 "import { type __JsonValue, Amount as __A } from \"./m.js\";\n\
1139 export const __serialise_Amount = (v: __A): __JsonValue => v;",
1140 None,
1141 ),
1142 TsStmt::verbatim(
1143 VerbatimOrigin::NotYetConverted,
1144 "export type Note = string;\nexport function __serialise_Note(n: Note) { return n; }",
1145 None,
1146 ),
1147 codec("__serialise_Quote", "Note", "__serialise_Amount"),
1148 codec("__serialise_Other", "__A", "__serialise_Note"),
1149 ]);
1150 assert!(unbound_names(&p).is_empty(), "{:?}", unbound_names(&p));
1151 }
1152
1153 #[test]
1154 fn a_cast_in_opaque_text_is_not_mistaken_for_an_import_alias() {
1155 let p = program(vec![
1158 import(&["type __JsonValue", "__serialise_Base"]),
1159 TsStmt::raw("// imports are below\nconst y = { z: x as Note };", None),
1160 codec("__serialise_Note", "Note", "__serialise_Base"),
1161 ]);
1162 assert_eq!(unbound_names(&p), ["Note"]);
1163 }
1164
1165 #[test]
1166 fn a_generic_type_applied_in_opaque_text_must_be_bound() {
1167 let p = program(vec![TsStmt::raw(
1170 "return ((__xs: readonly MapEntry<string, number>[], __acc: number) => __acc)(all, 0);",
1171 None,
1172 )]);
1173 assert_eq!(unbound_names(&p), ["MapEntry"]);
1174 }
1175
1176 #[test]
1177 fn opaque_generics_skip_members_comparisons_globals_and_literals() {
1178 let p = program(vec![
1179 import(&["type Page"]),
1180 TsStmt::raw(
1181 "const a: globalThis.Promise<void> = f();\n\
1182 const b: import(\"./m.js\").Note<string> = g();\n\
1183 const c = __i < __xs.length && N < 3;\n\
1184 const d: Record<string, Page<number>> = {};\n\
1185 const e = \"Missing<T>\" + `Other<${x}>`; // Gone<T>\n\
1186 /* Absent<T> */",
1187 None,
1188 ),
1189 ]);
1190 assert!(unbound_names(&p).is_empty(), "{:?}", unbound_names(&p));
1191 }
1192
1193 #[test]
1194 fn a_name_used_only_inside_opaque_text_is_not_checked() {
1195 let p = program(vec![TsStmt::raw("return __serialise_Missing(x);", None)]);
1196 assert!(unbound_names(&p).is_empty());
1197 }
1198
1199 #[test]
1200 fn a_type_only_export_entry_uses_its_name() {
1201 let export = |name: &str| {
1203 TsStmt::decl(
1204 TsDecl::ExportNames {
1205 names: vec![format!("type {name}")],
1206 },
1207 None,
1208 )
1209 };
1210 let p = program(vec![import(&["Batch"]), export("Batch"), export("Missing")]);
1211 assert_eq!(unbound_names(&p), ["Missing"]);
1212 }
1213
1214 #[test]
1215 fn namespace_members_are_reported_with_their_specifier() {
1216 let p = program(vec![
1220 TsStmt::decl(
1221 TsDecl::ImportNamespace {
1222 type_only: false,
1223 alias: "handlers".to_string(),
1224 from: "./handlers.js".to_string(),
1225 },
1226 None,
1227 ),
1228 TsStmt::let_stmt(
1229 TsBindingName::Ident("b".to_string()),
1230 Some(TsType::named("handlers.Batch<number>")),
1231 Some(TsExpr::Call {
1232 callee: Box::new(TsExpr::Member {
1233 object: Box::new(TsExpr::Ident("handlers".to_string())),
1234 property: "__deserialise_Batch".to_string(),
1235 }),
1236 args: vec![],
1237 }),
1238 None,
1239 ),
1240 TsStmt::let_stmt(
1241 TsBindingName::Ident("c".to_string()),
1242 Some(TsType::named("globalThis.Response")),
1243 None,
1244 None,
1245 ),
1246 ]);
1247 let uses: Vec<(String, String)> = namespace_member_uses(&p)
1248 .into_iter()
1249 .map(|u| (u.from, u.member))
1250 .collect();
1251 assert_eq!(
1252 uses,
1253 [
1254 ("./handlers.js".to_string(), "Batch".to_string()),
1255 (
1256 "./handlers.js".to_string(),
1257 "__deserialise_Batch".to_string()
1258 ),
1259 ]
1260 );
1261 }
1262
1263 #[test]
1264 fn module_exports_reads_structured_and_opaque_exports_and_star_re_exports() {
1265 let p = program(vec![
1266 TsStmt::decl(
1267 TsDecl::ExportNames {
1268 names: vec!["type Batch".to_string(), "a as b".to_string()],
1269 },
1270 None,
1271 ),
1272 codec("__serialise_Note", "Note", "__serialise_Name"),
1273 TsStmt::raw(
1274 "export async function run() {}\nexport { x, type Y };\nexport type { A, B as C };\nexport type D = number;\nconst hidden = 1;",
1275 None,
1276 ),
1277 TsStmt::decl(
1278 TsDecl::ReExportAll {
1279 from: "./handlers/a.js".to_string(),
1280 },
1281 None,
1282 ),
1283 ]);
1284 let exports = module_exports(&p);
1285 let names: Vec<&str> = exports.names.iter().map(String::as_str).collect();
1286 assert_eq!(
1287 names,
1288 [
1289 "A",
1290 "Batch",
1291 "C",
1292 "D",
1293 "Y",
1294 "__serialise_Note",
1295 "b",
1296 "run",
1297 "x"
1298 ]
1299 );
1300 assert_eq!(exports.star_from, ["./handlers/a.js"]);
1301 }
1302
1303 #[test]
1304 fn a_type_only_import_used_as_a_value_is_reported() {
1305 let member = |object: &str| TsExpr::Member {
1308 object: Box::new(TsExpr::Ident(object.to_string())),
1309 property: "Active".to_string(),
1310 };
1311 let p = program(vec![
1312 import(&["type Status", "type Batch", "Kept"]),
1313 TsStmt::let_stmt(
1314 TsBindingName::Ident("a".to_string()),
1315 Some(TsType::named("Batch")),
1316 Some(member("Status")),
1317 None,
1318 ),
1319 TsStmt::let_stmt(
1320 TsBindingName::Ident("b".to_string()),
1321 None,
1322 Some(member("Kept")),
1323 None,
1324 ),
1325 ]);
1326 assert_eq!(type_only_value_uses(&p), ["Status"]);
1327 assert!(unbound_names(&p).is_empty());
1328 }
1329
1330 #[test]
1331 fn value_imports_skip_type_only_entries_and_declarations() {
1332 let p = program(vec![
1333 import(&["type Status", "Batch"]),
1334 TsStmt::decl(
1335 TsDecl::Import {
1336 type_only: true,
1337 names: vec!["Receipt".to_string()],
1338 from: "./r.js".to_string(),
1339 },
1340 None,
1341 ),
1342 ]);
1343 assert_eq!(
1344 value_imports(&p),
1345 [("./m.js".to_string(), "Batch".to_string())]
1346 );
1347 }
1348}