Skip to main content

bynk_ts/
name_closure.rs

1//! Name closure: every name a module's tree uses is bound in that module
2//! (#1831).
3//!
4//! [`unbound_names`] walks a [`TsProgram`] and returns each name it uses that
5//! the module neither imports, declares, nor gets from TypeScript's, the
6//! Workers runtime's or Node's globals. Two positions are checked:
7//!
8//! - **type position**: a [`TsType::Named`], or the first segment of a
9//!   qualified `ns.T`;
10//! - **value position**: an identifier expression (`TsExpr::Ident`, first
11//!   segment of `a.b`), an object shorthand `{ x }`.
12//!
13//! A non-empty result is a `TS2304`/`TS2552` (`Cannot find name`) that `tsc`
14//! would report, found without `tsc`. That is the defect family of #1736,
15//! #1778, #1815, #1818, #1823 and #1829: the emitter names a type, or calls a
16//! codec, that the module never imports.
17//!
18//! **Scope is module-wide, not lexical.** A parameter, local or generic
19//! parameter declared anywhere in the module counts as bound everywhere in
20//! it, and a destructuring pattern binds every identifier in it. Each
21//! approximation can only hide a defect, never invent one.
22//!
23//! **Opaque text is mostly a blind spot.** `Raw` and `Verbatim` statements
24//! and `VerbatimExpr` expressions are pre-rendered TypeScript. A name they
25//! *declare* or *import* is found line by line (a line starting `import`,
26//! `type`, `interface`, `class`, `enum`, `function`, `const`, `let` or `var`).
27//! Of the names they *use*, only a generic type application (`Name<…>`) is
28//! checked (#1977: a kernel helper's parameter types are opaque text); any
29//! other use is not. Measured over the positive corpus at #1831, opaque text was 0% of
30//! `index.ts`/`compose.ts`, about 14% of unit modules and about 26% of
31//! `handlers.ts` and test modules: chiefly service handler bodies, which
32//! `bynk-emit` still renders to text. That part of a module stays `tsc`'s
33//! job (#1823 lived there).
34
35use std::collections::{BTreeMap, BTreeSet};
36
37use crate::program::{
38    TsArrowBody, TsBindingName, TsDecl, TsExpr, TsObjectEntry, TsParam, TsProgram, TsStmt,
39    TsStmtKind, TsType, TsTypeMember,
40};
41
42/// Type names TypeScript, the Workers runtime or Node provide globally, plus
43/// the primitive keywords. A name here is never reported.
44const GLOBAL_TYPES: &[&str] = &[
45    // Keywords.
46    "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    // `x as const`: the const assertion names no type (I4's generated client,
62    // #1970, asserts its `CONTRACTS` literal this way).
63    "const",
64    // ECMAScript / lib.dom globals the emitter names.
65    "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.Response` and the like: the qualifier itself.
115    "globalThis",
116];
117
118/// Value names the ECMAScript, Workers and Node globals provide.
119const 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
176/// Every name `program` uses, in type or value position, that it does not
177/// bind, sorted and deduplicated.
178pub 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
195/// #2045: each `(specifier, name)` a structured named import brings in as a
196/// value: every entry of an `import { … }` but its `type X` ones, none of an
197/// `import type { … }`. Namespace and opaque-text imports are not reported.
198pub 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
219/// #2045: every name `program` uses as a value that only a type-only import
220/// binds (`import { type Status }` then `Status.Active`): a `TS1361` (`cannot
221/// be used as a value because it was imported using 'import type'`). Opaque
222/// text is not searched for uses, as in [`unbound_names`].
223pub 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/// #2036: a member a module names through one of its namespace imports:
236/// `handlers.Batch` in type position, `handlers.__deserialise_Batch(…)` in
237/// value position.
238#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
239pub struct NamespaceMemberUse {
240    /// The namespace import's specifier (`./handlers.js`).
241    pub from: String,
242    /// The member named (`Batch`).
243    pub member: String,
244}
245
246/// #2036: every member `program` names through a namespace import
247/// (`import * as ns`, `import type * as ns`), sorted and deduplicated. A
248/// member the target module does not export is a `TS2694`/`TS2339` that
249/// `tsc` would report (`Namespace 'handlers' has no exported member`), which
250/// [`unbound_names`] cannot see: the namespace itself is bound. Checking it
251/// needs the target module too, so a caller pairs this with
252/// [`module_exports`]. Opaque text is not searched for uses.
253pub 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/// #2036: what a module exports, for [`namespace_member_uses`]' members.
272#[derive(Debug, Default, Clone, PartialEq, Eq)]
273pub struct ModuleExports {
274    /// Every name exported directly or re-exported by name.
275    pub names: BTreeSet<String>,
276    /// The specifier of each `export * from "…"`, whose module's exports are
277    /// this one's too (a split context's `handlers.ts`, #1820).
278    pub star_from: Vec<String>,
279}
280
281/// #2036: the names `program` exports. Structured exports are read with
282/// [`TsStmt::exported_names`]; opaque text line by line, as [`unbound_names`]
283/// reads its declarations: a line starting `export` and then a declaration
284/// keyword or a `{ … }` list. Over-reporting an export can only hide a
285/// defect.
286pub 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
304/// The names opaque text exports, line by line (see [`module_exports`]).
305fn 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        // `export type { A, B }` lists types, as `export { A, B }` lists names.
315        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    /// #2036: each namespace import's alias and specifier.
356    namespaces: BTreeMap<String, String>,
357    /// #2036: every `head.member` named in type or value position.
358    qualified: BTreeSet<(String, String)>,
359    /// #2045: names bound only by a type-only import (`import { type X }`,
360    /// `import type { X }`, `import type * as ns`), and the names bound any
361    /// other way. A value use of the first and not the second is `TS1361`.
362    type_only: BTreeSet<String>,
363    value_bound: BTreeSet<String>,
364}
365
366/// The identifier a piece of text starts with: `value` of `value.name`,
367/// `path` of `path: string = "$"`. `None` when the text does not start with
368/// one (a literal, a parenthesised expression).
369fn 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    /// Bind a declared name. A destructuring pattern (`[k, v]`, `{ a, b: c }`)
385    /// binds every identifier in it; over-binding can only hide a defect.
386    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    /// #2045: bind a name a type-only import brings in.
402    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    /// A destructuring pattern's names: `a`, `a: b` (binds `b`), `a = 1`.
410    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    /// An import specifier: `X`, `type X`, `X as Y` or `type X as Y` binds
438    /// the local name.
439    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    /// Lexical scan of opaque text for the names it declares or imports,
457    /// line by line: only a line that *starts* a declaration (`type`,
458    /// `interface`, `class`, `enum`, `function`, after any `export`/`declare`/
459    /// `default`/`async`) or an `import` counts, so a cast like `x as T` in a
460    /// function body is never mistaken for an import alias.
461    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    /// Opaque text: the names it declares or imports, and the generic types
506    /// it applies.
507    fn opaque(&mut self, text: &str) {
508        self.bind_opaque(text);
509        self.use_opaque_generics(text);
510    }
511
512    /// #1977: the one *use* of a name opaque text is checked for — a generic
513    /// type application, a capitalised identifier immediately followed by `<`
514    /// (`MapEntry<string, number>` in a kernel helper's parameter types). That
515    /// shape is a type in every position the emitter writes it, and it never
516    /// collides with a comparison (`a < b` is spaced, its operands lower-case).
517    /// An identifier after a `.` is a member or the tail of a qualified or
518    /// inline-import spelling (`globalThis.Promise<T>`, `import("./m").T<U>`),
519    /// whose head, if any, is not this identifier; text inside string,
520    /// template and comment literals is skipped. Wider checks of opaque text
521    /// (an annotation `x: T`, a bare value use) collide with object keys,
522    /// locals and ternaries, so they stay `tsc`'s job.
523    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                // A name may carry its arguments as text (`ns.T<number>`).
570                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            // `export { a, b };` exports names this module binds (#1852),
769            // so each is a use: one never imported or declared is unbound.
770            // A `type X` entry (#2036) uses `X`, and `X as Y` uses `X`.
771            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    /// `export function <name>(value: <param_ty>): __JsonValue { return <call>(value); }`
951    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        // #1829's shape: a codec's parameter type the module never imports.
979        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        // #1815's shape: a codec helper called without its import.
989        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        // #1852: a Workers context's `export { … };` of the commons codecs it
999        // imports. A name exported without being imported is unbound.
1000        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        // The first version scanned for `import` anywhere and read `x as Note`
1156        // in a codec body as an alias, hiding #1829.
1157        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        // #1977's shape: a kernel helper typed with a generic record the
1168        // module never imports.
1169        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        // #2036: `export { type Batch };` of an imported consumed type.
1202        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        // #2036: `handlers.Batch` in type position and
1217        // `handlers.__deserialise_Batch(…)` in value position; a qualified
1218        // name whose head is no namespace import is not a member use.
1219        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        // #2045: `import { type Status }` then `Status.Active` is TS1361; a
1306        // type position, or a name also bound as a value, is not.
1307        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}