Skip to main content

lanekeep_types/
declarations.rs

1//! One parsed declaration file, and what it says about a name.
2//!
3//! Every node kind read here was checked against `tree-sitter-typescript` 0.23.2's
4//! `node-types.json`, which is where the fields are *declared*. A hand-written sample cannot
5//! stand in for that: a sample with zero `ERROR` nodes still omits whatever the author did not
6//! think to write, and AGENTS.md records four wrong claims about this grammar produced exactly
7//! that way. The kinds a statement can declare with are read through the resolver's
8//! [`BindingResolver::declares`], which owns that walk — this file's own table, kept in
9//! parallel with it, drifted once already (#229) — and the resolver is reached through the
10//! trait, the way the oracle reaches `declaration_of`, so this crate names no language crate.
11
12use std::fmt;
13use std::sync::Arc;
14
15use lanekeep_core::FilePath;
16use lanekeep_core::tracked::ContentHash;
17use lanekeep_lang::binding::{Binding, BindingResolver, ImportedName};
18use tree_sitter::{Node, Tree};
19
20/// A declaration file this run has read and parsed.
21pub struct Declaration {
22    /// Where it was read from, relative to the project root.
23    pub path: FilePath,
24    /// The resolver this file's declarations are read with: the one the provider was probed
25    /// with, so a declaration file and the file that imported it agree on what declares a
26    /// name.
27    resolver: Arc<dyn BindingResolver>,
28    /// Its text, which every byte range in `tree` indexes.
29    pub source: String,
30    /// Its parse.
31    pub tree: Tree,
32    /// What its bytes hashed to when it was read.
33    ///
34    /// Compared, within a run, against the hash of the bytes the *asking* file's own
35    /// `FileAccess` reads: two accesses over one path can see two different files when the
36    /// file is rewritten mid-run, which is routine under `--watch`, and serving this parse
37    /// against the other access's hash would write a cache entry that describes neither
38    /// version. See `BuiltinProvider::declaration`. Load-bearing across requests too: a
39    /// provider held for a session (#191) drops an entry whose hash moved.
40    pub hash: ContentHash,
41    /// Whether its parse carries a fault anywhere — an `ERROR` node, or a `MISSING` token
42    /// the parser inserted where one was expected. `Node::has_error` at the root counts
43    /// both; an unclosed brace produces the second and no `ERROR` at all.
44    ///
45    /// `tree_sitter::Parser::parse` answers a tree for any UTF-8 input, so a file this
46    /// provider could not really read is indistinguishable from one it read cleanly unless
47    /// the question is asked here. The arms still answer whatever the tree does hold — a
48    /// declaration outside the damaged span is a real declaration — and it is `complete()`
49    /// that turns this into the honest label on a partial answer.
50    ///
51    /// **The per-declaration granularity is the reached node's own `has_error()`**, asked
52    /// by the walk of the declaration it ends at; this whole-file flag is what a nameless
53    /// import keeps, and what a name the walk cannot follow to a declaration falls back to.
54    pub has_error: bool,
55}
56
57impl fmt::Debug for Declaration {
58    /// `source` is summarized rather than printed, the same call the oracle's own `Debug`
59    /// makes: a whole declaration file in every log line the value appears in is not one
60    /// anybody can read, and its length identifies which file this is as well as the bytes do.
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.debug_struct("Declaration")
63            .field("path", &self.path)
64            .field("source_len", &self.source.len())
65            .field("hash", &self.hash)
66            .field("has_error", &self.has_error)
67            .finish_non_exhaustive()
68    }
69}
70
71impl Declaration {
72    /// Parse a declaration file that has already been read through a tracked access.
73    ///
74    /// Takes the parser rather than making one: a provider owns exactly one for the run, and
75    /// a constructor that made its own would put a second parser behind an API that reads
76    /// like it could not.
77    #[must_use]
78    pub fn parse(
79        path: FilePath,
80        source: String,
81        parser: &mut tree_sitter::Parser,
82        resolver: Arc<dyn BindingResolver>,
83    ) -> Option<Self> {
84        let hash = ContentHash::new(*blake3::hash(source.as_bytes()).as_bytes());
85        let tree = parser.parse(&source, None)?;
86        let has_error = tree.root_node().has_error();
87        Some(Self {
88            path,
89            resolver,
90            source,
91            tree,
92            hash,
93            has_error,
94        })
95    }
96}
97
98/// Where a file that declares an export sends the name it was asked about.
99///
100/// Four variants rather than a bare node, because three of the shapes the grammar produces
101/// are not nodes in this file at all: a named re-export and a star re-export both name another
102/// module, and a namespace re-export names a whole module object that has no declaration to
103/// point at.
104#[derive(Debug)]
105pub enum Exported<'d> {
106    /// Declared in this file, at this node.
107    Here(Node<'d>),
108    /// Re-exported by name: `export { A as B } from './x'`, asked about `B`, yields `A`.
109    From {
110        /// The module specifier exactly as written.
111        specifier: String,
112        /// The name to ask that module for.
113        name: String,
114    },
115    /// `export * as ns from './x'` — bound to the whole module object, which has no single
116    /// declaration and no type this oracle can build.
117    Namespace {
118        /// The module specifier exactly as written.
119        specifier: String,
120    },
121    /// Not named here. Each of these `export * from` sources may have it, in source order.
122    Star(Vec<String>),
123}
124
125/// Where the file's own file-level export named `name` leads.
126///
127/// Explicit exports first, in source order, and the `export *` fallback only when nothing
128/// explicit answered — which is what TypeScript itself does, and what keeps a name a file
129/// declares from being answered by a different declaration elsewhere that happens to share
130/// its spelling.
131#[must_use]
132pub fn find_export<'d>(decl: &'d Declaration, name: &str) -> Option<Exported<'d>> {
133    let root = decl.tree.root_node();
134    let mut cursor = root.walk();
135    let mut stars = Vec::new();
136
137    for statement in root.named_children(&mut cursor) {
138        if statement.kind() != "export_statement" {
139            continue;
140        }
141        let source = statement
142            .child_by_field_name("source")
143            .map(|node| unquote(text(decl, node)).to_owned());
144
145        if let Some(specifier) = source {
146            if let Some(clause) = named_child_of_kind(statement, "export_clause") {
147                if let Some(exported) = clause_target(decl, clause, name, &specifier) {
148                    return Some(exported);
149                }
150                continue;
151            }
152            if let Some(namespace) = named_child_of_kind(statement, "namespace_export") {
153                if namespace
154                    .named_child(0)
155                    .is_some_and(|n| unquote(text(decl, n)) == name)
156                {
157                    return Some(Exported::Namespace { specifier });
158                }
159                continue;
160            }
161            // `export * from 'm'`: a source, no clause, no namespace. Collected rather than
162            // followed, so an explicit export later in the file still wins.
163            stars.push(specifier);
164            continue;
165        }
166
167        // A local re-export, `export { A }` or `export { A as B }`. The local name is either
168        // declared in this file or bound by one of its imports — `import { A } from './a';
169        // export { A };` is the two-statement barrel, and the chain continues into `./a`
170        // under the module's own spelling of the name. The resolver says which, the way it
171        // answers a use anywhere else: a walk outward from the specifier's own identifier.
172        if let Some(clause) = named_child_of_kind(statement, "export_clause")
173            && let Some(local) = local_clause_node(decl, clause, name)
174        {
175            if let Some(node) = declared_here(decl, unquote(text(decl, local))) {
176                return Some(Exported::Here(node));
177            }
178            if let Some(Binding::Import {
179                module,
180                name: imported,
181            }) = decl.resolver.resolve(&decl.tree, &decl.source, local)
182            {
183                return Some(match imported {
184                    ImportedName::Named(exported) => Exported::From {
185                        specifier: module,
186                        name: exported,
187                    },
188                    ImportedName::Default => Exported::From {
189                        specifier: module,
190                        name: "default".to_owned(),
191                    },
192                    ImportedName::Namespace => Exported::Namespace { specifier: module },
193                });
194            }
195        }
196
197        let is_default = anonymous_child(statement, "default");
198        // `export = X`, whose only marker is the `=` token: no field, an `expression` child.
199        // Treated as the default export, which is what an `import D from 'm'` binds under
200        // `esModuleInterop` — the spelling every consumer of such a module writes.
201        let is_export_assignment = anonymous_child(statement, "=");
202
203        if let Some(declaration) = statement.child_by_field_name("declaration") {
204            let wanted = if is_default { "default" } else { name };
205            if is_default && name == "default" {
206                return Some(Exported::Here(unwrap_ambient(declaration)));
207            }
208            if !is_default && let Some(node) = declares(decl, declaration, wanted) {
209                return Some(Exported::Here(node));
210            }
211            continue;
212        }
213
214        if (is_default || is_export_assignment) && name == "default" {
215            let value = statement
216                .child_by_field_name("value")
217                .or_else(|| statement.named_children(&mut statement.walk()).next())?;
218            // `export default Big` names a declaration; `export default 1` is the value
219            // itself, which the oracle types directly.
220            if value.kind() == "identifier"
221                && let Some(node) = declared_here(decl, text(decl, value))
222            {
223                return Some(Exported::Here(node));
224            }
225            return Some(Exported::Here(value));
226        }
227    }
228
229    (!stars.is_empty()).then_some(Exported::Star(stars))
230}
231
232/// The declaration of `name` at this file's top level, exported or not.
233///
234/// Both spellings, because a chain ends at whichever one the declaring file used:
235/// `export declare class Big {}` and `declare class Big {}` + `export default Big` name the
236/// same thing, and a walk that reached only through `export_statement` would find the first
237/// and lose the second.
238#[must_use]
239pub fn declared_here<'d>(decl: &'d Declaration, name: &str) -> Option<Node<'d>> {
240    declared_in(decl.resolver.as_ref(), &decl.tree, &decl.source, name)
241}
242
243/// The declaration of `name` at a parsed file's top level, exported or not.
244///
245/// [`declared_here`] is this over a [`Declaration`]; this is the same walk over a tree the
246/// provider does not own — the file under check, which the engine already parsed and which
247/// must not be parsed a second time — so the resolver comes in from the caller, which holds
248/// the provider's.
249#[must_use]
250pub(crate) fn declared_in<'t>(
251    resolver: &dyn BindingResolver,
252    tree: &'t Tree,
253    source: &'t str,
254    name: &str,
255) -> Option<Node<'t>> {
256    let root = tree.root_node();
257    let mut cursor = root.walk();
258    for statement in root.named_children(&mut cursor) {
259        let candidate = if statement.kind() == "export_statement" {
260            match statement.child_by_field_name("declaration") {
261                Some(declaration) => declaration,
262                // A re-export carries no declaration of its own; the name may still be
263                // declared further down, so skip rather than give up on the file.
264                None => continue,
265            }
266        } else {
267            statement
268        };
269        if let Some(found) = resolver.declares(source, candidate, name) {
270            return Some(found);
271        }
272    }
273    None
274}
275
276/// The name a declaration node declares, when it declares one.
277///
278/// `None` for an anonymous default export — `export default 1`, `export default () => {}` —
279/// which is a real shape rather than a gap: it has no name, so a `symbolOf` following a chain
280/// to it has nothing better to report than `default`.
281///
282/// `None` for a destructured declarator too: `export const { e } = { e: 2 }` binds a
283/// *pattern*, whose text is `{ e }` — and reporting a shape where a spelling belongs would
284/// put `exported: "{ e }"` on a `Symbol`, which a rule comparing against a required export
285/// name would read as a mismatch on conforming code. `walk_export` falls back to the name
286/// the chain was asked for, which is what a shorthand destructuring exports.
287///
288/// A dotted namespace's name is its first segment: `namespace A.B {}` declares `A`, the
289/// same name the resolver binds for it, and `A.B` is a spelling no `import` can write.
290#[must_use]
291pub fn declared_name(decl: &Declaration, node: Node<'_>) -> Option<String> {
292    let node = unwrap_ambient(node);
293    node.child_by_field_name("name")
294        .filter(|name| {
295            matches!(
296                name.kind(),
297                "identifier" | "type_identifier" | "nested_identifier" | "string"
298            )
299        })
300        .map(|name| unquote(text(decl, first_segment(name))).to_owned())
301}
302
303/// The node whose text is the declared name: the leftmost segment of a `nested_identifier`.
304///
305/// `namespace A.B {}` is shorthand for `namespace A { namespace B {} }` — the enclosing
306/// scope sees `A`, and `lanekeep-lang-js` binds it so. The grammar aliases every inner level
307/// of a dotted name to `member_expression`, so `google.maps.places` is a `nested_identifier`
308/// over the member expression `google.maps`; both kinds carry an `object` field. Anything
309/// that is not nested is its own name.
310fn first_segment(name: Node<'_>) -> Node<'_> {
311    let mut node = name;
312    while matches!(node.kind(), "nested_identifier" | "member_expression") {
313        match node.child_by_field_name("object") {
314            Some(object) => node = object,
315            None => break,
316        }
317    }
318    node
319}
320
321/// Whether `declaration` declares `name`, and where.
322///
323/// The resolver's walk — one table for both crates, where a second once drifted (#229).
324/// Imports are filtered out on the resolver's side, so an `import_statement` never answers
325/// as a declaration here.
326fn declares<'d>(decl: &'d Declaration, declaration: Node<'d>, name: &str) -> Option<Node<'d>> {
327    decl.resolver.declares(&decl.source, declaration, name)
328}
329
330/// Step through `ambient_declaration`, which wraps the declaration `declare` applies to.
331///
332/// The first named child that is not a `comment`: a comment is a named extra in this
333/// grammar, so `declare /** doc */ class C {}` puts one ahead of the class, and a `.d.ts`
334/// writes that shape all the time.
335fn unwrap_ambient(node: Node<'_>) -> Node<'_> {
336    if node.kind() != "ambient_declaration" {
337        return node;
338    }
339    let mut cursor = node.walk();
340    node.named_children(&mut cursor)
341        .find(|child| child.kind() != "comment")
342        .unwrap_or(node)
343}
344
345/// The re-export target for `name` inside `export { … } from 'm'`.
346fn clause_target<'d>(
347    decl: &'d Declaration,
348    clause: Node<'d>,
349    name: &str,
350    specifier: &str,
351) -> Option<Exported<'d>> {
352    let mut cursor = clause.walk();
353    for specifier_node in clause.named_children(&mut cursor) {
354        if specifier_node.kind() != "export_specifier" {
355            continue;
356        }
357        let exported = specifier_node.child_by_field_name("name")?;
358        let visible = specifier_node
359            .child_by_field_name("alias")
360            .unwrap_or(exported);
361        if unquote(text(decl, visible)) == name {
362            return Some(Exported::From {
363                specifier: specifier.to_owned(),
364                name: unquote(text(decl, exported)).to_owned(),
365            });
366        }
367    }
368    None
369}
370
371/// The `name` node of the `export_specifier` in a local `export { … }` that exports `name`.
372///
373/// The node rather than its text, because the resolver resolves a *node* by walking outward
374/// from it — which is what lets [`find_export`] ask whether the local name is an import
375/// binding rather than a declaration.
376fn local_clause_node<'d>(decl: &Declaration, clause: Node<'d>, name: &str) -> Option<Node<'d>> {
377    let mut cursor = clause.walk();
378    for specifier in clause.named_children(&mut cursor) {
379        if specifier.kind() != "export_specifier" {
380            continue;
381        }
382        let local = specifier.child_by_field_name("name")?;
383        let visible = specifier.child_by_field_name("alias").unwrap_or(local);
384        if unquote(text(decl, visible)) == name {
385            return Some(local);
386        }
387    }
388    None
389}
390
391/// The file and name a chain of re-exports ends at.
392///
393/// `name` is the name the *declaring* file uses, which is what `symbolOf` reports as
394/// `exported` — so `export { Decimal as Big }` asked about `Big` answers `Decimal`, and a
395/// rule comparing against a required export name compares against the real one rather than
396/// against whatever spelling the last hop chose.
397#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
398pub struct ExportTarget {
399    /// The declaring file, relative to the project root.
400    pub file: FilePath,
401    /// The name it declares the export under.
402    pub name: String,
403}
404
405/// The node an already-resolved [`ExportTarget`] names, in its own file.
406///
407/// `declared_here` first, because a chain ends at whichever spelling the declaring file
408/// used and `declare class Big {}` is not an `export_statement` at all. `find_export` is
409/// the fallback for the one shape that has no name to look up: an anonymous default.
410///
411/// A node the parser only partly read — `has_error()`, which counts a `MISSING` token as
412/// well as an `ERROR` — answers `None` rather than the node: a damaged declaration has no
413/// shape worth typing. `walk_export` makes the same refusal for the chain it walks; this is
414/// the refusal for the callers that look a name up here directly, so the two cannot
415/// disagree about one node.
416#[must_use]
417pub(crate) fn target_node<'d>(decl: &'d Declaration, name: &str) -> Option<Node<'d>> {
418    declared_here(decl, name)
419        .or_else(|| match find_export(decl, name) {
420            Some(Exported::Here(node)) => Some(node),
421            _ => None,
422        })
423        .filter(|node| !node.has_error())
424}
425
426/// The first named child of `kind`, when there is one.
427fn named_child_of_kind<'d>(node: Node<'d>, kind: &str) -> Option<Node<'d>> {
428    let mut cursor = node.walk();
429    node.named_children(&mut cursor).find(|c| c.kind() == kind)
430}
431
432/// Whether an anonymous token of this text is a direct child.
433///
434/// `default` and `=` are the only markers separating three otherwise identical shapes of
435/// `export_statement`, and neither is a field — the grammar writes them as bare tokens
436/// (`common/define-grammar.js:329`, `tree-sitter-javascript`'s `grammar.js:185`).
437fn anonymous_child(node: Node<'_>, token: &str) -> bool {
438    let mut cursor = node.walk();
439    node.children(&mut cursor)
440        .any(|child| !child.is_named() && child.kind() == token)
441}
442
443/// The source text of a node.
444fn text<'d>(decl: &'d Declaration, node: Node<'_>) -> &'d str {
445    text_of(&decl.source, node)
446}
447
448/// The source text of a node, read from a source string directly rather than a
449/// [`Declaration`] — what [`imports_with_names`] needs over a tree it does not own.
450fn text_of<'t>(source: &'t str, node: Node<'_>) -> &'t str {
451    source.get(node.byte_range()).unwrap_or("")
452}
453
454/// Drop the quotes a string module name or a string export name carries.
455///
456/// `export_specifier`'s `name` and `alias` may be a `string` as well as an `identifier`
457/// (`node-types.json`), and so may a module's `source`, so every read of one goes through
458/// this rather than through four places that each remember to.
459fn unquote(text: &str) -> &str {
460    let bytes = text.as_bytes();
461    match (bytes.first(), bytes.last()) {
462        (Some(b'"' | b'\''), Some(b'"' | b'\'')) if text.len() >= 2 => &text[1..text.len() - 1],
463        _ => text,
464    }
465}
466
467/// One import statement's specifier, with the names it binds.
468///
469/// `complete`'s per-name walk needs more than the bare specifier the old whole-file walk
470/// returned: an `ERROR` verdict is now decided per *reached* declaration, and which
471/// declarations an import reaches is exactly its name list. A nameless import —
472/// side-effect, `import * as ns`, `export *` — binds no single declaration to reach (a
473/// namespace binds the whole module object, which the caller judges the same way) and
474/// keeps the whole-file verdict; see the caller.
475#[derive(Debug)]
476pub(crate) struct ImportedSpecifier {
477    /// The module specifier exactly as written, quotes stripped.
478    pub specifier: String,
479    /// The names the statement binds, in source order — empty for a nameless one.
480    pub names: Vec<ImportedName>,
481}
482
483/// Every module specifier this file imports from, in source order, with the names each
484/// statement binds.
485///
486/// `import_statement`'s `source` field, plus `export_statement`'s: a barrel file re-exporting
487/// what it never imports is exactly as dependent on those modules, and a completeness answer
488/// that ignored them would call such a file complete while knowing nothing about it.
489///
490/// The name lists are read off the grammar the same way `JsBindingResolver`'s
491/// `import_binding` reads them — the two were dumped side by side against
492/// `tree-sitter-typescript` 0.23.2 rather than trusted from a sample, which is where the
493/// default/namespace/named split below comes from.
494///
495/// `import x = require('m')` needs its own fallback: `node-types.json` marks
496/// `import_statement`'s own `source` field `required: false` and puts the field this shape
497/// actually carries on its `import_require_clause` child instead — so a bare
498/// `child_by_field_name("source")` on the statement itself answers nothing for exactly this
499/// one shape, silently dropping it from the count. It binds one name by assignment rather
500/// than by an `import_clause`, so it contributes an empty name list.
501#[must_use]
502pub(crate) fn imports_with_names(tree: &Tree, source: &str) -> Vec<ImportedSpecifier> {
503    let root = tree.root_node();
504    let mut cursor = root.walk();
505    root.named_children(&mut cursor)
506        .filter(|statement| matches!(statement.kind(), "import_statement" | "export_statement"))
507        .filter_map(|statement| {
508            let specifier = statement.child_by_field_name("source").or_else(|| {
509                named_child_of_kind(statement, "import_require_clause")
510                    .and_then(|clause| clause.child_by_field_name("source"))
511            })?;
512            Some(ImportedSpecifier {
513                specifier: unquote(text_of(source, specifier)).to_owned(),
514                names: bound_names(statement, source),
515            })
516        })
517        .collect()
518}
519
520/// The names one `import_statement` or `export_statement` binds from its `source` module.
521///
522/// A statement without a source module — a local `export { A }` — binds nothing from
523/// anywhere, and never reaches this function.
524fn bound_names(statement: Node<'_>, source: &str) -> Vec<ImportedName> {
525    match statement.kind() {
526        "import_statement" => {
527            let Some(clause) = named_child_of_kind(statement, "import_clause") else {
528                return Vec::new();
529            };
530            let mut names = Vec::new();
531            let mut cursor = clause.walk();
532            for child in clause.children(&mut cursor) {
533                match child.kind() {
534                    // `import d from 'm'` — and the `d` of `import d, * as ns from 'm'`.
535                    "identifier" => names.push(ImportedName::Default),
536                    // `import * as ns from 'm'`
537                    "namespace_import" => names.push(ImportedName::Namespace),
538                    // `import { a, b as c } from 'm'`
539                    "named_imports" => {
540                        let mut inner = child.walk();
541                        for specifier in child
542                            .children(&mut inner)
543                            .filter(|s| s.kind() == "import_specifier")
544                        {
545                            // The module's own spelling, not the local alias: the walk that
546                            // follows asks the declaring module what it exports.
547                            if let Some(exported) = specifier.child_by_field_name("name") {
548                                names.push(ImportedName::Named(
549                                    unquote(text_of(source, exported)).to_owned(),
550                                ));
551                            }
552                        }
553                    }
554                    _ => {}
555                }
556            }
557            names
558        }
559        "export_statement" => {
560            // `export { A as B } from 'm'` — `A` is the name in the *other* module, which is
561            // the one a walk from here asks it for.
562            if let Some(clause) = named_child_of_kind(statement, "export_clause") {
563                let mut names = Vec::new();
564                let mut cursor = clause.walk();
565                for specifier in clause
566                    .named_children(&mut cursor)
567                    .filter(|s| s.kind() == "export_specifier")
568                {
569                    if let Some(exported) = specifier.child_by_field_name("name") {
570                        names.push(ImportedName::Named(
571                            unquote(text_of(source, exported)).to_owned(),
572                        ));
573                    }
574                }
575                return names;
576            }
577            // `export * as ns from 'm'`
578            if named_child_of_kind(statement, "namespace_export").is_some() {
579                return vec![ImportedName::Namespace];
580            }
581            // Bare `export * from 'm'`: nothing is bound, so nothing is reached.
582            Vec::new()
583        }
584        _ => Vec::new(),
585    }
586}